rusty_cdk/
deploy.rs

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