rusty_cdk/
deploy.rs

1use aws_config::stalled_stream_protection::StalledStreamProtectionConfig;
2use aws_config::SdkConfig;
3use aws_sdk_cloudformation::types::{Capability, StackStatus, Tag};
4use aws_sdk_cloudformation::Client;
5use rusty_cdk_core::stack::{Asset, Stack};
6use rusty_cdk_core::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::process::exit;
10use std::sync::Arc;
11use std::time::Duration;
12use aws_sdk_cloudformation::error::{ProvideErrorMetadata, SdkError};
13use tokio::time::sleep;
14
15#[derive(Debug)]
16pub enum DeployError {
17    SynthError(String),
18    StackCreateError(String),
19    StackUpdateError(String),
20    AssetError(String),
21    UnknownError(String),
22}
23
24impl Error for DeployError {}
25
26impl Display for DeployError {
27    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
28        match self {
29            DeployError::SynthError(_) => f.write_str("unable to synth"),
30            DeployError::StackCreateError(_) => f.write_str("unable to create stack"),
31            DeployError::StackUpdateError(_) => f.write_str("unable to update stack"),
32            DeployError::AssetError(_) => f.write_str("unable to handle asset"),
33            DeployError::UnknownError(_) => f.write_str("unknown error"),
34        }
35    }
36}
37
38async fn get_existing_template(client: &Client, stack_name: &str) -> Option<String> {
39    match client.describe_stacks().stack_name(stack_name).send().await {
40        Ok(_) => {
41            let template = client.get_template().stack_name(stack_name).send().await;
42            template.unwrap().template_body
43        }
44        Err(_) => None,
45    }
46}
47
48/// Deploys a stack to AWS using CloudFormation.
49///
50/// This function handles the complete deployment lifecycle:
51/// - Uploading Lambda function assets to S3
52/// - Creating or updating the CloudFormation stack
53/// - Monitoring deployment progress with real-time status updates
54///
55/// It exits with code 0 on success, 1 on failure
56/// 
57/// For a deployment method that returns a Result, see `deploy_with_result`
58///
59/// # Parameters
60///
61/// * `name` - The CloudFormation stack name (alphanumeric characters and hyphens only)
62/// * `stack` - The stack to deploy, created using `StackBuilder`
63///
64/// # Tags
65///
66/// If tags were added to the stack using `StackBuilder::add_tag()`, they will be
67/// applied to the CloudFormation stack and propagated to resources where supported.
68///
69/// # Example
70///
71/// ```no_run
72/// use rusty_cdk::deploy;
73/// use rusty_cdk::stack::StackBuilder;
74/// use rusty_cdk::sqs::QueueBuilder;
75/// use rusty_cdk_macros::string_with_only_alphanumerics_and_hyphens;
76/// use rusty_cdk::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
77///
78/// #[tokio::main]
79/// async fn main() {
80///
81/// let mut stack_builder = StackBuilder::new();
82///     QueueBuilder::new("my-queue")
83///         .standard_queue()
84///         .build(&mut stack_builder);
85///
86///     let stack = stack_builder.build().expect("Stack to build successfully");
87///
88///     deploy(string_with_only_alphanumerics_and_hyphens!("my-application-stack"), stack).await;
89/// }
90/// ```
91///
92/// # AWS Credentials
93///
94/// This function requires valid AWS credentials configured through:
95/// - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
96/// - AWS credentials file (`~/.aws/credentials`)
97/// - IAM role (when running on EC2, ECS, Lambda, etc.)
98/// - ...
99///
100/// The AWS credentials must have permissions for:
101/// - `cloudformation:CreateStack`, `cloudformation:UpdateStack`, `cloudformation:DescribeStacks`, `cloudformation:GetTemplate`
102/// - `s3:PutObject` (for Lambda asset uploads)
103/// - IAM permissions if creating roles (`iam:CreateRole`, `iam:PutRolePolicy`, etc.)
104/// - Service-specific permissions for resources being created
105pub async fn deploy(name: StringWithOnlyAlphaNumericsAndHyphens, mut stack: Stack) {
106    let name = name.0;
107    let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
108        // https://github.com/awslabs/aws-sdk-rust/issues/1146
109        .stalled_stream_protection(StalledStreamProtectionConfig::disabled())
110        .load()
111        .await;
112
113    let assets = stack.get_assets();
114    assets.iter().for_each(|a| {
115        println!("uploading {}", a);
116    });
117
118    match upload_assets(assets, &config).await {
119        Ok(_) => {}
120        Err(e) => {
121            eprintln!("{e:#?}");
122            exit(1);
123        }
124    }
125
126    let cloudformation_client = aws_sdk_cloudformation::Client::new(&config);
127
128    match create_or_update_stack(&name, &mut stack, &cloudformation_client).await {
129        Ok(_) => {}
130        Err(e) => {
131            eprintln!("{e:#?}");
132            exit(1);
133        }
134    }
135
136    loop {
137        let status = cloudformation_client.describe_stacks().stack_name(&name).send().await;
138        let mut stacks = status
139            .expect("to get a describe stacks result")
140            .stacks
141            .expect("to have a list of stacks");
142        let first_stack = stacks.get_mut(0).expect("to find our stack");
143        let status = first_stack.stack_status.take().expect("stack to have status");
144
145        match status {
146            StackStatus::CreateComplete => {
147                println!("creation completed successfully!");
148                exit(0);
149            }
150            StackStatus::CreateFailed => {
151                println!("creation failed");
152                exit(1);
153            }
154            StackStatus::CreateInProgress => {
155                println!("creating...");
156            }
157            StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
158                println!("update completed successfully!");
159                exit(0);
160            }
161            StackStatus::UpdateRollbackComplete
162            | StackStatus::UpdateRollbackCompleteCleanupInProgress
163            | StackStatus::UpdateRollbackFailed
164            | StackStatus::UpdateRollbackInProgress
165            | StackStatus::UpdateFailed => {
166                println!("update failed");
167                exit(1);
168            }
169            StackStatus::UpdateInProgress => {
170                println!("updating...");
171            }
172            _ => {
173                println!("encountered unexpected cloudformation status: {status}");
174                exit(1);
175            }
176        }
177
178        sleep(Duration::from_secs(10)).await;
179    }
180}
181
182/// Deploys a stack to AWS using CloudFormation.
183///
184/// This function handles the complete deployment lifecycle:
185/// - Uploading Lambda function assets to S3
186/// - Creating or updating the CloudFormation stack
187/// - Monitoring deployment progress
188///
189/// It returns a `Result`. In case of error, a `DeployError` is returned.
190/// It exits with code 0 on success, 1 on failure
191///
192/// For a deployment method that shows updates and exits on failure, see `deploy`
193///
194/// # Parameters
195///
196/// * `name` - The CloudFormation stack name (alphanumeric characters and hyphens only)
197/// * `stack` - The stack to deploy, created using `StackBuilder`
198///
199/// # Tags
200///
201/// If tags were added to the stack using `StackBuilder::add_tag()`, they will be
202/// applied to the CloudFormation stack and propagated to resources where supported.
203///
204/// # Example
205///
206/// ```no_run
207/// use rusty_cdk::deploy;
208/// use rusty_cdk::stack::StackBuilder;
209/// use rusty_cdk::sqs::QueueBuilder;
210/// use rusty_cdk_macros::string_with_only_alphanumerics_and_hyphens;
211/// use rusty_cdk::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
212///
213/// #[tokio::main]
214/// async fn main() {
215///
216/// use rusty_cdk::deploy_with_result;
217/// let mut stack_builder = StackBuilder::new();
218///     QueueBuilder::new("my-queue")
219///         .standard_queue()
220///         .build(&mut stack_builder);
221///
222///     let stack = stack_builder.build().expect("Stack to build successfully");
223///
224///     let result = deploy_with_result(string_with_only_alphanumerics_and_hyphens!("my-application-stack"), stack).await;
225/// }
226/// ```
227///
228/// # AWS Credentials
229///
230/// This function requires valid AWS credentials configured through:
231/// - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
232/// - AWS credentials file (`~/.aws/credentials`)
233/// - IAM role (when running on EC2, ECS, Lambda, etc.)
234/// - ...
235///
236/// The AWS credentials must have permissions for:
237/// - `cloudformation:CreateStack`, `cloudformation:UpdateStack`, `cloudformation:DescribeStacks`, `cloudformation:GetTemplate`
238/// - `s3:PutObject` (for Lambda asset uploads)
239/// - IAM permissions if creating roles (`iam:CreateRole`, `iam:PutRolePolicy`, etc.)
240/// - Service-specific permissions for resources being created
241pub async fn deploy_with_result(name: StringWithOnlyAlphaNumericsAndHyphens, mut stack: Stack) -> Result<(), DeployError> {
242    let name = name.0;
243    let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
244        // https://github.com/awslabs/aws-sdk-rust/issues/1146
245        .stalled_stream_protection(StalledStreamProtectionConfig::disabled())
246        .load()
247        .await;
248
249    upload_assets(stack.get_assets(), &config).await?;
250
251    let cloudformation_client = Client::new(&config);
252
253    create_or_update_stack(&name, &mut stack, &cloudformation_client).await?;
254
255    loop {
256        let status = cloudformation_client.describe_stacks().stack_name(&name).send().await;
257        let mut stacks = status
258            .expect("to get a describe stacks result")
259            .stacks
260            .expect("to have a list of stacks");
261        let first_stack = stacks.get_mut(0).expect("to find our stack");
262        let status = first_stack.stack_status.take().expect("stack to have status");
263
264        match status {
265            StackStatus::CreateComplete => {
266                return Ok(());
267            }
268            StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
269                return Ok(());
270            }
271            StackStatus::CreateInProgress => {}
272            StackStatus::UpdateInProgress => {}
273            StackStatus::CreateFailed => {
274                return Err(DeployError::StackCreateError(format!("{status}")));
275            }
276            StackStatus::UpdateRollbackComplete
277            | StackStatus::UpdateRollbackCompleteCleanupInProgress
278            | StackStatus::UpdateRollbackFailed
279            | StackStatus::UpdateRollbackInProgress
280            | StackStatus::UpdateFailed => {
281                return Err(DeployError::StackUpdateError(format!("{status}")));
282            }
283            _ => {
284                return Err(DeployError::UnknownError(format!("{status}")));
285            }
286        }
287
288        sleep(Duration::from_secs(10)).await;
289    }
290}
291
292async fn create_or_update_stack(name: &String, stack: &mut Stack, cloudformation_client: &Client) -> Result<(), DeployError> {
293    let existing_template = get_existing_template(cloudformation_client, name).await;
294    let tags = stack.get_tags();
295    let tags = if tags.is_empty() {
296        None
297    } else {
298        Some(tags.into_iter().map(|v| Tag::builder().key(v.0).value(v.1).build()).collect())
299    };
300
301    match existing_template {
302        Some(existing) => {
303            let body = stack
304                .synth_for_existing(&existing)
305                .map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
306
307            return match cloudformation_client
308                .update_stack()
309                .stack_name(name)
310                .template_body(body)
311                .capabilities(Capability::CapabilityNamedIam)
312                .set_tags(tags)
313                .send()
314                .await {
315                Ok(_) => Ok(()),
316                Err(e) => {
317                    match e {
318                        SdkError::ServiceError(ref s) => {
319                            let update_stack_error = s.err();
320                            if update_stack_error.message().map(|v| v.contains("No updates are to be performed")).unwrap_or(false) {
321                                Ok(())   
322                            } else {
323                                Err(DeployError::StackUpdateError(format!("{e:?}")))
324                            }
325                        }
326                        _ => {
327                            Err(DeployError::StackUpdateError(format!("{e:?}")))
328                        }
329                    }
330                }
331            }
332        }
333        None => {
334            let body = stack.synth().map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
335
336            cloudformation_client
337                .create_stack()
338                .stack_name(name)
339                .template_body(body)
340                .capabilities(Capability::CapabilityNamedIam)
341                .set_tags(tags)
342                .send()
343                .await
344                .map_err(|e| DeployError::StackCreateError(format!("{e:?}")))?;
345        }
346    }
347    Ok(())
348}
349
350async fn upload_assets(assets: Vec<Asset>, config: &SdkConfig) -> Result<(), DeployError> {
351    let s3_client = Arc::new(aws_sdk_s3::Client::new(config));
352
353    let tasks: Vec<_> = assets
354        .into_iter()
355        .map(|a| {
356            let s3_client = s3_client.clone();
357            tokio::spawn(async move {
358                let body = aws_sdk_s3::primitives::ByteStream::from_path(a.path).await;
359                s3_client
360                    .put_object()
361                    .bucket(a.s3_bucket)
362                    .key(a.s3_key)
363                    .body(body.unwrap())
364                    .send()
365                    .await
366                    .unwrap();
367            })
368        })
369        .collect();
370
371    for task in tasks {
372        task.await.map_err(|e| DeployError::AssetError(format!("{e:?}")))?;
373    }
374    Ok(())
375}