rusty_cdk/
deploy.rs

1use aws_config::stalled_stream_protection::StalledStreamProtectionConfig;
2use aws_sdk_cloudformation::types::{Capability, StackStatus, Tag};
3use rusty_cdk_core::stack::Stack;
4use serde::Deserialize;
5use std::collections::HashMap;
6use std::process::exit;
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::time::sleep;
10use rusty_cdk_core::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
11
12#[derive(Deserialize)]
13struct StackOnlyMetadata {
14    #[serde(rename = "Metadata")]
15    pub(crate) metadata: HashMap<String, String>,
16}
17
18async fn get_existing_template(client: &aws_sdk_cloudformation::Client, stack_name: &str) -> Option<String> {
19    match client.describe_stacks().stack_name(stack_name).send().await {
20        Ok(_) => {
21            let template = client.get_template()
22                .stack_name(stack_name)
23                .send()
24                .await;
25            template.unwrap().template_body
26        }
27        Err(_) => {
28            None
29        }
30    }
31}
32
33/// Deploys a stack to AWS using CloudFormation.
34///
35/// This function handles the complete deployment lifecycle:
36/// - Uploading Lambda function assets to S3
37/// - Creating or updating the CloudFormation stack
38/// - Monitoring deployment progress with real-time status updates
39/// 
40/// It exits with code 0 on success, 1 on failure
41///
42/// # Parameters
43///
44/// * `name` - The CloudFormation stack name (alphanumeric characters and hyphens only)
45/// * `stack` - The stack to deploy, created using `StackBuilder`
46///
47/// # Tags
48///
49/// If tags were added to the stack using `StackBuilder::add_tag()`, they will be
50/// applied to the CloudFormation stack and propagated to resources where supported.
51///
52/// # Example
53///
54/// ```no_run
55/// use rusty_cdk::deploy;
56/// use rusty_cdk::stack::StackBuilder;
57/// use rusty_cdk::sqs::QueueBuilder;
58/// use rusty_cdk_macros::string_with_only_alphanumerics_and_hyphens;
59/// use rusty_cdk::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
60///
61/// #[tokio::main]
62/// async fn main() {
63///
64/// let mut stack_builder = StackBuilder::new();
65///     QueueBuilder::new("my-queue")
66///         .standard_queue()
67///         .build(&mut stack_builder);
68///
69///     let stack = stack_builder.build().expect("Stack to build successfully");
70///
71///     deploy(string_with_only_alphanumerics_and_hyphens!("my-application-stack"), stack).await;
72/// }
73/// ```
74///
75/// # AWS Credentials
76///
77/// This function requires valid AWS credentials configured through:
78/// - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
79/// - AWS credentials file (`~/.aws/credentials`)
80/// - IAM role (when running on EC2, ECS, Lambda, etc.)
81/// - ...
82///
83/// The AWS credentials must have permissions for:
84/// - `cloudformation:CreateStack`, `cloudformation:UpdateStack`, `cloudformation:DescribeStacks`, `cloudformation:GetTemplate`
85/// - `s3:PutObject` (for Lambda asset uploads)
86/// - IAM permissions if creating roles (`iam:CreateRole`, `iam:PutRolePolicy`, etc.)
87/// - Service-specific permissions for resources being created
88pub async fn deploy(name: StringWithOnlyAlphaNumericsAndHyphens, mut stack: Stack) {
89    let name = name.0;
90    let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
91        // https://github.com/awslabs/aws-sdk-rust/issues/1146
92        .stalled_stream_protection(StalledStreamProtectionConfig::disabled())
93        .load()
94        .await;
95    let s3_client = Arc::new(aws_sdk_s3::Client::new(&config));
96
97    let tasks: Vec<_> = stack
98        .get_assets()
99        .into_iter()
100        .map(|a| {
101            println!("uploading asset {} to {}/{}", a.path, a.s3_bucket, a.s3_key);
102            let s3_client = s3_client.clone();
103            tokio::spawn(async move {
104                let body = aws_sdk_s3::primitives::ByteStream::from_path(a.path).await;
105                s3_client
106                    .put_object()
107                    .bucket(a.s3_bucket)
108                    .key(a.s3_key)
109                    .body(body.unwrap()) //
110                    .send()
111                    .await
112                    .unwrap();
113            })
114        })
115        .collect();
116
117    for task in tasks {
118        task.await.unwrap();
119    }
120
121    let cloudformation_client = aws_sdk_cloudformation::Client::new(&config);
122    let existing_template = get_existing_template(&cloudformation_client, &name).await;
123    let tags = stack.get_tags();
124    let tags = if tags.is_empty() {
125        None
126    } else {
127        Some(tags.into_iter()
128            .map(|v| Tag::builder().key(v.0).value(v.1).build())
129            .collect())
130    };
131
132    match existing_template {
133        Some(existing) => {
134            let meta: StackOnlyMetadata = serde_json::from_str(existing.as_str()).expect("an existing stack should have our 'id' metadata");
135            stack.update_resource_ids_for_existing_stack(meta.metadata);
136            let body = get_template_or_exit(&stack);
137
138            match cloudformation_client
139                .update_stack()
140                .stack_name(&name)
141                .template_body(body)
142                .capabilities(Capability::CapabilityNamedIam)
143                .set_tags(tags)
144                .send()
145                .await
146            {
147                Ok(_) => println!("stack {name} update started"),
148                Err(e) => eprintln!("an error occurred while creating the stack: {e:#?}"),
149            }
150        }
151        None => {
152            let body = get_template_or_exit(&stack);
153
154            match cloudformation_client
155                .create_stack()
156                .stack_name(&name)
157                .template_body(body)
158                .capabilities(Capability::CapabilityNamedIam)
159                .set_tags(tags)
160                .send()
161                .await
162            {
163                Ok(_) => println!("stack {name} creation started"),
164                Err(e) => {
165                    eprintln!("an error occurred while creating the stack: {e:#?}");
166                    exit(1);
167                }
168            }
169        }
170    }
171
172    loop {
173        let status = cloudformation_client.describe_stacks().stack_name(&name).send().await;
174        let mut stacks = status
175            .expect("to get a describe stacks result")
176            .stacks
177            .expect("to have a list of stacks");
178        let first_stack = stacks.get_mut(0).expect("to find our stack");
179        let status = first_stack.stack_status.take().expect("stack to have status");
180
181        match status {
182            StackStatus::CreateComplete => {
183                println!("creation completed successfully!");
184                exit(0);
185            }
186            StackStatus::CreateFailed => {
187                println!("creation failed");
188                exit(1);
189            }
190            StackStatus::CreateInProgress => {
191                println!("creating...");
192            }
193            StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
194                println!("update completed successfully!");
195                exit(0);
196            }
197            StackStatus::UpdateRollbackComplete
198            | StackStatus::UpdateRollbackCompleteCleanupInProgress
199            | StackStatus::UpdateRollbackFailed
200            | StackStatus::UpdateRollbackInProgress
201            | StackStatus::UpdateFailed => {
202                println!("update failed");
203                exit(1);
204            }
205            StackStatus::UpdateInProgress => {
206                println!("updating...");
207            }
208            _ => {
209                println!("encountered unexpected cloudformation status: {status}");
210                exit(1);
211            }
212        }
213
214        sleep(Duration::from_secs(10)).await;
215    }
216}
217
218fn get_template_or_exit(stack: &Stack) -> String {
219    match stack.synth() {
220        Ok(b) => b,
221        Err(e) => {
222            eprintln!("{e:#?}");
223            exit(1);
224        }
225    }
226}