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// TODO here and elsewhere: remove the 'normal' deploy/diff methods - use cargo rusty for those
16
17#[derive(Debug)]
18pub enum DeployError {
19 SynthError(String),
20 StackCreateError(String),
21 StackUpdateError(String),
22 AssetError(String),
23 UnknownError(String),
24}
25
26impl Error for DeployError {}
27
28impl Display for DeployError {
29 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
30 match self {
31 DeployError::SynthError(_) => f.write_str("unable to synth"),
32 DeployError::StackCreateError(_) => f.write_str("unable to create stack"),
33 DeployError::StackUpdateError(_) => f.write_str("unable to update stack"),
34 DeployError::AssetError(_) => f.write_str("unable to handle asset"),
35 DeployError::UnknownError(_) => f.write_str("unknown error"),
36 }
37 }
38}
39
40/// Deploys a stack to AWS using CloudFormation.
41///
42/// This function handles the complete deployment lifecycle:
43/// - Uploading Lambda function assets to S3
44/// - Creating or updating the CloudFormation stack
45/// - Monitoring deployment progress with real-time status updates
46///
47/// It exits with code 0 on success, 1 on failure
48///
49/// For a deployment method that returns a Result, see `deploy_with_result`
50///
51/// # Parameters
52///
53/// * `name` - The CloudFormation stack name (alphanumeric characters and hyphens only)
54/// * `stack` - The stack to deploy, created using `StackBuilder`
55///
56/// # Tags
57///
58/// If tags were added to the stack using `StackBuilder::add_tag()`, they will be
59/// applied to the CloudFormation stack and propagated to resources where supported.
60///
61/// # Example
62///
63/// ```no_run
64/// use rusty_cdk::deploy;
65/// use rusty_cdk::stack::StackBuilder;
66/// use rusty_cdk::sqs::QueueBuilder;
67/// use rusty_cdk_macros::string_with_only_alphanumerics_and_hyphens;
68/// use rusty_cdk::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
69///
70/// #[tokio::main]
71/// async fn main() {
72///
73/// let mut stack_builder = StackBuilder::new();
74/// QueueBuilder::new("my-queue")
75/// .standard_queue()
76/// .build(&mut stack_builder);
77///
78/// let stack = stack_builder.build().expect("Stack to build successfully");
79///
80/// deploy(string_with_only_alphanumerics_and_hyphens!("my-application-stack"), stack).await;
81/// }
82/// ```
83///
84/// # AWS Credentials
85///
86/// This function requires valid AWS credentials configured through:
87/// - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
88/// - AWS credentials file (`~/.aws/credentials`)
89/// - IAM role (when running on EC2, ECS, Lambda, etc.)
90/// - ...
91///
92/// The AWS credentials must have permissions for:
93/// - `cloudformation:CreateStack`, `cloudformation:UpdateStack`, `cloudformation:DescribeStacks`, `cloudformation:GetTemplate`
94/// - `s3:PutObject` (for Lambda asset uploads)
95/// - IAM permissions if creating roles (`iam:CreateRole`, `iam:PutRolePolicy`, etc.)
96/// - Service-specific permissions for resources being created
97pub async fn deploy(name: StringWithOnlyAlphaNumericsAndHyphens, stack: Stack) {
98 match deploy_with_result(name, stack, true).await {
99 Ok(message) => println!("{message}"),
100 Err(e) => {
101 eprintln!("{e}");
102 exit(1);
103 }
104 }
105}
106
107/// Deploys a stack to AWS using CloudFormation.
108///
109/// This function handles the complete deployment lifecycle:
110/// - Uploading Lambda function assets to S3
111/// - Creating or updating the CloudFormation stack
112/// - Monitoring deployment progress
113///
114/// It returns a `Result`. In case of error, a `DeployError` is returned.
115///
116/// For a deployment method that shows updates and exits on failure, see `deploy`
117///
118/// # Parameters
119///
120/// * `name` - The CloudFormation stack name (alphanumeric characters and hyphens only)
121/// * `stack` - The stack to deploy, created using `StackBuilder`
122///
123/// # Tags
124///
125/// If tags were added to the stack using `StackBuilder::add_tag()`, they will be
126/// applied to the CloudFormation stack and propagated to resources where supported.
127///
128/// # Example
129///
130/// ```no_run
131/// use rusty_cdk::deploy;
132/// use rusty_cdk::stack::StackBuilder;
133/// use rusty_cdk::sqs::QueueBuilder;
134/// use rusty_cdk_macros::string_with_only_alphanumerics_and_hyphens;
135/// use rusty_cdk::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
136///
137/// #[tokio::main]
138/// async fn main() {
139///
140/// use rusty_cdk::deploy_with_result;
141/// let mut stack_builder = StackBuilder::new();
142/// QueueBuilder::new("my-queue")
143/// .standard_queue()
144/// .build(&mut stack_builder);
145///
146/// let stack = stack_builder.build().expect("Stack to build successfully");
147///
148/// let result = deploy_with_result(string_with_only_alphanumerics_and_hyphens!("my-application-stack"), stack, false).await;
149/// }
150/// ```
151///
152/// # AWS Credentials
153///
154/// This function requires valid AWS credentials configured through:
155/// - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
156/// - AWS credentials file (`~/.aws/credentials`)
157/// - IAM role (when running on EC2, ECS, Lambda, etc.)
158/// - ...
159///
160/// The AWS credentials must have permissions for:
161/// - `cloudformation:CreateStack`, `cloudformation:UpdateStack`, `cloudformation:DescribeStacks`, `cloudformation:GetTemplate`
162/// - `s3:PutObject` (for Lambda asset uploads)
163/// - IAM permissions if creating roles (`iam:CreateRole`, `iam:PutRolePolicy`, etc.)
164/// - Service-specific permissions for resources being created
165pub async fn deploy_with_result(name: StringWithOnlyAlphaNumericsAndHyphens, mut stack: Stack, print_progress: bool) -> Result<String, DeployError> {
166 let name = name.0;
167 let config = load_config(true).await;
168
169 upload_assets(stack.get_assets(), &config).await?;
170
171 let cloudformation_client = Client::new(&config);
172
173 create_or_update_stack(&name, &mut stack, &cloudformation_client).await?;
174
175 loop {
176 let status = get_stack_status(&name, &cloudformation_client).await.expect("status to be available for stack");
177
178 match status {
179 StackStatus::CreateComplete => {
180 return Ok("created successfully".to_string());
181 }
182 StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
183 return Ok("updated successfully".to_string());
184 }
185 StackStatus::CreateInProgress => {
186 if print_progress {
187 println!("creating...");
188 }
189 }
190 StackStatus::UpdateInProgress => {
191 if print_progress {
192 println!("updating...");
193 }
194 }
195 StackStatus::CreateFailed => {
196 return Err(DeployError::StackCreateError(format!("{status}")));
197 }
198 StackStatus::UpdateRollbackComplete
199 | StackStatus::UpdateRollbackCompleteCleanupInProgress
200 | StackStatus::UpdateRollbackFailed
201 | StackStatus::UpdateRollbackInProgress
202 | StackStatus::UpdateFailed => {
203 return Err(DeployError::StackUpdateError(format!("{status}")));
204 }
205 _ => {
206 return Err(DeployError::UnknownError(format!("{status}")));
207 }
208 }
209
210 sleep(Duration::from_secs(10)).await;
211 }
212}
213
214async fn create_or_update_stack(name: &String, stack: &mut Stack, cloudformation_client: &Client) -> Result<(), DeployError> {
215 let existing_template = get_existing_template(cloudformation_client, name).await;
216 let tags = stack.get_tags();
217 let tags = if tags.is_empty() {
218 None
219 } else {
220 Some(tags.into_iter().map(|v| Tag::builder().key(v.0).value(v.1).build()).collect())
221 };
222
223 match existing_template {
224 Some(existing) => {
225 let body = stack
226 .synth_for_existing(&existing)
227 .map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
228
229 return match cloudformation_client
230 .update_stack()
231 .stack_name(name)
232 .template_body(body)
233 .capabilities(Capability::CapabilityNamedIam)
234 .set_tags(tags)
235 .send()
236 .await {
237 Ok(_) => Ok(()),
238 Err(e) => {
239 match e {
240 SdkError::ServiceError(ref s) => {
241 let update_stack_error = s.err();
242 if update_stack_error.message().map(|v| v.contains("No updates are to be performed")).unwrap_or(false) {
243 Ok(())
244 } else {
245 Err(DeployError::StackUpdateError(format!("{e:?}")))
246 }
247 }
248 _ => {
249 Err(DeployError::StackUpdateError(format!("{e:?}")))
250 }
251 }
252 }
253 }
254 }
255 None => {
256 let body = stack.synth().map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
257
258 cloudformation_client
259 .create_stack()
260 .stack_name(name)
261 .template_body(body)
262 .capabilities(Capability::CapabilityNamedIam)
263 .set_tags(tags)
264 .send()
265 .await
266 .map_err(|e| DeployError::StackCreateError(format!("{e:?}")))?;
267 }
268 }
269 Ok(())
270}
271
272async fn upload_assets(assets: Vec<Asset>, config: &SdkConfig) -> Result<(), DeployError> {
273 let s3_client = Arc::new(aws_sdk_s3::Client::new(config));
274
275 let tasks: Vec<_> = assets
276 .into_iter()
277 .map(|a| {
278 let s3_client = s3_client.clone();
279 tokio::spawn(async move {
280 let body = aws_sdk_s3::primitives::ByteStream::from_path(a.path).await;
281 s3_client
282 .put_object()
283 .bucket(a.s3_bucket)
284 .key(a.s3_key)
285 .body(body.unwrap())
286 .send()
287 .await
288 .unwrap();
289 })
290 })
291 .collect();
292
293 for task in tasks {
294 task.await.map_err(|e| DeployError::AssetError(format!("{e:?}")))?;
295 }
296 Ok(())
297}