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