Skip to main content

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