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