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}