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