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::Client;
4use aws_sdk_cloudformation::error::{ProvideErrorMetadata, SdkError};
5use aws_sdk_cloudformation::types::{Capability, StackStatus, Tag};
6use rusty_cdk_core::stack::{Asset, Stack};
7use rusty_cdk_core::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
8use std::error::Error;
9use std::fmt::{Display, Formatter};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::time::sleep;
13
14#[derive(Debug)]
15pub enum DeployError {
16    SynthError(String),
17    StackCreateError(String),
18    StackUpdateError(String),
19    AssetError(String),
20    UnknownError(String),
21}
22
23impl Error for DeployError {}
24
25impl Display for DeployError {
26    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
27        match self {
28            DeployError::SynthError(_) => f.write_str("unable to synth"),
29            DeployError::StackCreateError(_) => f.write_str("unable to create stack"),
30            DeployError::StackUpdateError(_) => f.write_str("unable to update stack"),
31            DeployError::AssetError(_) => f.write_str("unable to handle asset"),
32            DeployError::UnknownError(_) => f.write_str("unknown error"),
33        }
34    }
35}
36
37/// Deploys a stack to AWS using CloudFormation.
38///
39/// This function handles the complete deployment lifecycle:
40/// - Uploading Lambda function assets to S3
41/// - Creating or updating the CloudFormation stack
42/// - Monitoring deployment progress
43///
44/// It returns a `Result`. In case of error, a `DeployError` is returned.
45///
46/// For a deployment method that shows updates and exits on failure, see `deploy`
47///
48/// # Parameters
49///
50/// * `name` - The CloudFormation stack name (alphanumeric characters and hyphens only)
51/// * `stack` - The stack to deploy, created using `StackBuilder`
52/// * `print_progress` - Print progress updates to standard out
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///     let result = deploy(string_with_only_alphanumerics_and_hyphens!("my-application-stack"), stack, false).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`
92/// - `cloudformation:UpdateStack`
93/// - `cloudformation:DescribeStacks`
94/// - `cloudformation:GetTemplate`
95/// - `s3:PutObject` (if you have Lambdas)
96/// - IAM permissions for creating roles
97/// - Service-specific permissions for resources being created
98pub async fn deploy(
99    name: StringWithOnlyAlphaNumericsAndHyphens,
100    mut stack: Stack,
101    print_progress: bool,
102) -> Result<String, DeployError> {
103    let name = name.0;
104    let config = load_config(true).await;
105
106    upload_assets(stack.get_assets(), &config).await?;
107
108    let cloudformation_client = Client::new(&config);
109
110    create_or_update_stack(&name, &mut stack, &cloudformation_client).await?;
111
112    loop {
113        let status = get_stack_status(&name, &cloudformation_client)
114            .await
115            .expect("status to be available for stack");
116
117        match status {
118            StackStatus::CreateComplete => {
119                return Ok("created successfully".to_string());
120            }
121            StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
122                return Ok("updated successfully".to_string());
123            }
124            StackStatus::CreateInProgress => {
125                if print_progress {
126                    println!("creating...");
127                }
128            }
129            StackStatus::UpdateInProgress => {
130                if print_progress {
131                    println!("updating...");
132                }
133            }
134            StackStatus::CreateFailed => {
135                return Err(DeployError::StackCreateError(format!("{status}")));
136            }
137            StackStatus::UpdateRollbackComplete
138            | StackStatus::UpdateRollbackCompleteCleanupInProgress
139            | StackStatus::UpdateRollbackFailed
140            | StackStatus::UpdateRollbackInProgress
141            | StackStatus::UpdateFailed => {
142                return Err(DeployError::StackUpdateError(format!("{status}")));
143            }
144            _ => {
145                return Err(DeployError::UnknownError(format!("{status}")));
146            }
147        }
148
149        sleep(Duration::from_secs(10)).await;
150    }
151}
152
153async fn create_or_update_stack(name: &String, stack: &mut Stack, cloudformation_client: &Client) -> Result<(), DeployError> {
154    let existing_template = get_existing_template(cloudformation_client, name).await;
155    let tags = stack.get_tags();
156    let tags = if tags.is_empty() {
157        None
158    } else {
159        Some(tags.into_iter().map(|v| Tag::builder().key(v.0).value(v.1).build()).collect())
160    };
161
162    match existing_template {
163        Some(existing) => {
164            let body = stack
165                .synth_for_existing(&existing)
166                .map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
167
168            return match cloudformation_client
169                .update_stack()
170                .stack_name(name)
171                .template_body(body)
172                .capabilities(Capability::CapabilityNamedIam)
173                .set_tags(tags)
174                .send()
175                .await
176            {
177                Ok(_) => Ok(()),
178                Err(e) => match e {
179                    SdkError::ServiceError(ref s) => {
180                        let update_stack_error = s.err();
181                        if update_stack_error
182                            .message()
183                            .map(|v| v.contains("No updates are to be performed"))
184                            .unwrap_or(false)
185                        {
186                            Ok(())
187                        } else {
188                            Err(DeployError::StackUpdateError(format!("{e:?}")))
189                        }
190                    }
191                    _ => Err(DeployError::StackUpdateError(format!("{e:?}"))),
192                },
193            };
194        }
195        None => {
196            let body = stack.synth().map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
197
198            cloudformation_client
199                .create_stack()
200                .stack_name(name)
201                .template_body(body)
202                .capabilities(Capability::CapabilityNamedIam)
203                .set_tags(tags)
204                .send()
205                .await
206                .map_err(|e| DeployError::StackCreateError(format!("{e:?}")))?;
207        }
208    }
209    Ok(())
210}
211
212async fn upload_assets(assets: Vec<Asset>, config: &SdkConfig) -> Result<(), DeployError> {
213    let s3_client = Arc::new(aws_sdk_s3::Client::new(config));
214
215    let tasks: Vec<_> = assets
216        .into_iter()
217        .map(|a| {
218            let s3_client = s3_client.clone();
219            tokio::spawn(async move {
220                let body = aws_sdk_s3::primitives::ByteStream::from_path(a.path).await;
221                s3_client
222                    .put_object()
223                    .bucket(a.s3_bucket)
224                    .key(a.s3_key)
225                    .body(body.unwrap())
226                    .send()
227                    .await
228                    .unwrap();
229            })
230        })
231        .collect();
232
233    for task in tasks {
234        task.await.map_err(|e| DeployError::AssetError(format!("{e:?}")))?;
235    }
236    Ok(())
237}