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