diff --git a/.doc_gen/metadata/auto-scaling_metadata.yaml b/.doc_gen/metadata/auto-scaling_metadata.yaml index c2317ac717c..621e9dd3be0 100644 --- a/.doc_gen/metadata/auto-scaling_metadata.yaml +++ b/.doc_gen/metadata/auto-scaling_metadata.yaml @@ -33,7 +33,7 @@ auto-scaling_Hello: Rust: versions: - sdk_version: 1 - github: rust_dev_preview/examples/autoscaling + github: rust_dev_preview/examples/auto-scaling excerpts: - description: snippet_tags: @@ -101,7 +101,7 @@ auto-scaling_CreateAutoScalingGroup: Rust: versions: - sdk_version: 1 - github: rust_dev_preview/examples/autoscaling + github: rust_dev_preview/examples/auto-scaling excerpts: - description: snippet_tags: @@ -162,15 +162,14 @@ auto-scaling_DeleteAutoScalingGroup: - sdk_version: 3 github: python/example_code/auto-scaling excerpts: - - description: Update the minimum size of an Auto Scaling group to zero, terminate all - instances in the group, and delete the group. + - description: Update the minimum size of an Auto Scaling group to zero, terminate all instances in the group, and delete the group. snippet_tags: - python.cross_service.resilient_service.AutoScaler.decl - python.cross_service.resilient_service.auto-scaling.DeleteAutoScalingGroup Rust: versions: - sdk_version: 1 - github: rust_dev_preview/examples/autoscaling + github: rust_dev_preview/examples/auto-scaling excerpts: - description: snippet_tags: @@ -237,7 +236,7 @@ auto-scaling_DescribeAutoScalingGroups: Rust: versions: - sdk_version: 1 - github: rust_dev_preview/examples/autoscaling + github: rust_dev_preview/examples/auto-scaling excerpts: - description: snippet_tags: @@ -304,7 +303,7 @@ auto-scaling_UpdateAutoScalingGroup: Rust: versions: - sdk_version: 1 - github: rust_dev_preview/examples/autoscaling + github: rust_dev_preview/examples/auto-scaling excerpts: - description: snippet_tags: @@ -382,6 +381,15 @@ auto-scaling_TerminateInstanceInAutoScalingGroup: - cpp.example_code.autoscaling.autoscaling_client - cpp.example_code.autoscaling.terminate_instance_autoscaling_group1 - cpp.example_code.autoscaling.terminate_instance_autoscaling_group2 + Rust: + versions: + - sdk_version: 1 + github: rust_dev_preview/examples/auto-scaling + excerpts: + - description: + snippet_tags: + - rust.auto-scaling.scenario.terminate_some_instance + - rust.auto-scaling.scenario.get_group services: auto-scaling: {TerminateInstanceInAutoScalingGroup} auto-scaling_SetDesiredCapacity: @@ -444,6 +452,14 @@ auto-scaling_SetDesiredCapacity: - cpp.example_code.autoscaling.autoscaling_client - cpp.example_code.autoscaling.set_desired_capacity1 - cpp.example_code.autoscaling.set_desired_capacity2 + Rust: + versions: + - sdk_version: 1 + github: rust_dev_preview/examples/auto-scaling + excerpts: + - description: + snippet_tags: + - rust.auto-scaling.scenario.scale_desired_capacity services: auto-scaling: {SetDesiredCapacity} auto-scaling_DescribeAutoScalingInstances: @@ -506,6 +522,14 @@ auto-scaling_DescribeAutoScalingInstances: - cpp.example_code.autoscaling.autoscaling_client - cpp.example_code.autoscaling.describe_autoscaling_instances1 - cpp.example_code.autoscaling.describe_autoscaling_instances2 + Rust: + versions: + - sdk_version: 1 + github: rust_dev_preview/examples/auto-scaling + excerpts: + - description: + snippet_tags: + - rust.auto-scaling.scenario.list_instances services: auto-scaling: {DescribeAutoScalingInstances} auto-scaling_DescribeScalingActivities: @@ -568,6 +592,14 @@ auto-scaling_DescribeScalingActivities: - cpp.example_code.autoscaling.autoscaling_client - cpp.example_code.autoscaling.describe_scaling_activities1 - cpp.example_code.autoscaling.describe_scaling_activities2 + Rust: + versions: + - sdk_version: 1 + github: rust_dev_preview/examples/auto-scaling + excerpts: + - description: + snippet_tags: + - rust.auto-scaling.scenario.describe_scenario services: auto-scaling: {DescribeScalingActivities} auto-scaling_EnableMetricsCollection: @@ -630,6 +662,14 @@ auto-scaling_EnableMetricsCollection: - cpp.example_code.autoscaling.autoscaling_client - cpp.example_code.autoscaling.enable_metrics_collection1 - cpp.example_code.autoscaling.enable_metrics_collection2 + Rust: + versions: + - sdk_version: 1 + github: rust_dev_preview/examples/auto-scaling + excerpts: + - description: + snippet_tags: + - rust.auto-scaling.scenario.enable_metrics_collection services: auto-scaling: {EnableMetricsCollection} auto-scaling_DisableMetricsCollection: @@ -692,6 +732,14 @@ auto-scaling_DisableMetricsCollection: - cpp.example_code.autoscaling.autoscaling_client - cpp.example_code.autoscaling.disable_metrics_collection1 - cpp.example_code.autoscaling.disable_metrics_collection2 + Rust: + versions: + - sdk_version: 1 + github: rust_dev_preview/examples/auto-scaling + excerpts: + - description: + snippet_tags: + - rust.auto-scaling.scenario.disable_metrics_collection services: auto-scaling: {DisableMetricsCollection} auto-scaling_AttachLoadBalancerTargetGroups: @@ -792,6 +840,17 @@ auto-scaling_Scenario_GroupsAndInstances: - description: snippet_tags: - cpp.example_code.autoscaling.groups_and_instances_scenario + Rust: + versions: + - sdk_version: 1 + github: rust_dev_preview/examples/auto-scaling + excerpts: + - description: + snippet_files: + - rust_dev_preview/examples/auto-scaling/Cargo.toml + - rust_dev_preview/examples/auto-scaling/src/bin/scenario.rs + - rust_dev_preview/examples/auto-scaling/src/lib.rs + - rust_dev_preview/examples/auto-scaling/src/scenario.rs services: auto-scaling: { diff --git a/rust_dev_preview/.gitignore b/rust_dev_preview/.gitignore index 4470988469a..2c96eb1b651 100644 --- a/rust_dev_preview/.gitignore +++ b/rust_dev_preview/.gitignore @@ -1,2 +1,2 @@ target/ -Cargo.lock \ No newline at end of file +Cargo.lock diff --git a/rust_dev_preview/examples/Cargo.toml b/rust_dev_preview/examples/Cargo.toml index 113cde5bb4f..ee214b256cd 100644 --- a/rust_dev_preview/examples/Cargo.toml +++ b/rust_dev_preview/examples/Cargo.toml @@ -6,7 +6,7 @@ members = [ "apigateway", "apigatewaymanagement", "applicationautoscaling", - "autoscaling", + "auto-scaling", "autoscalingplans", "batch", "cloudformation", diff --git a/rust_dev_preview/examples/autoscaling/Cargo.toml b/rust_dev_preview/examples/auto-scaling/Cargo.toml similarity index 63% rename from rust_dev_preview/examples/autoscaling/Cargo.toml rename to rust_dev_preview/examples/auto-scaling/Cargo.toml index 55a0b99b24b..60514e18223 100644 --- a/rust_dev_preview/examples/autoscaling/Cargo.toml +++ b/rust_dev_preview/examples/auto-scaling/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "autoscaling-code-examples" version = "0.1.0" -authors = ["Doug Schwartz "] +authors = ["Doug Schwartz ", "David Souther "] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,6 +9,11 @@ edition = "2021" [dependencies] aws-config = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "next" } aws-sdk-autoscaling = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "next" } +aws-sdk-ec2 = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "next" } +aws-types = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "next" } tokio = { version = "1.20.1", features = ["full"] } clap = { version = "~4.2", features = ["derive"] } tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } +anyhow = "1.0.75" +tracing = "0.1.37" +tokio-stream = "0.1.14" diff --git a/rust_dev_preview/examples/autoscaling/README.md b/rust_dev_preview/examples/auto-scaling/README.md similarity index 50% rename from rust_dev_preview/examples/autoscaling/README.md rename to rust_dev_preview/examples/auto-scaling/README.md index 8922a743cdc..62c2cecd015 100644 --- a/rust_dev_preview/examples/autoscaling/README.md +++ b/rust_dev_preview/examples/auto-scaling/README.md @@ -1,4 +1,4 @@ - + # Auto Scaling code examples for the SDK for Rust ## Overview @@ -12,7 +12,7 @@ Shows how to use the AWS SDK for Rust to work with Amazon EC2 Auto Scaling. ## ⚠ Important -* Running this code might result in charges to your AWS account. +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/?aws-products-pricing.sort-by=item.additionalFields.productNameLowercase&aws-products-pricing.sort-order=asc&awsf.Free%20Tier%20Type=*all&awsf.tech-category=*all) and [Free Tier](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all). * Running the tests might result in charges to your AWS account. * We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). * This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). @@ -24,21 +24,39 @@ Shows how to use the AWS SDK for Rust to work with Amazon EC2 Auto Scaling. ### Prerequisites -For prerequisites, see the [README](../README.md#Prerequisites) in the `rust_dev_preview` folder. +For prerequisites, see the [README](../../README.md#Prerequisites) in the `rust_dev_preview` folder. + +### Get started + +* [Hello Auto Scaling](src/bin/list-autoscaling-groups.rs#L24) (`DescribeAutoScalingGroups`) + ### Single actions Code excerpts that show you how to call individual service functions. * [Create a group](src/bin/create-autoscaling-group.rs#L32) (`CreateAutoScalingGroup`) * [Delete a group](src/bin/delete-autoscaling-group.rs#L32) (`DeleteAutoScalingGroup`) +* [Disable metrics collection for a group](src/scenario.rs#L608) (`DisableMetricsCollection`) +* [Enable metrics collection for a group](src/scenario.rs#L288) (`EnableMetricsCollection`) * [Get information about groups](src/bin/list-autoscaling-groups.rs#L24) (`DescribeAutoScalingGroups`) +* [Get information about instances](src/scenario.rs#L529) (`DescribeAutoScalingInstances`) +* [Get information about scaling activities](src/scenario.rs#L393) (`DescribeScalingActivities`) +* [Set the desired capacity of a group](src/scenario.rs#L586) (`SetDesiredCapacity`) +* [Terminate an instance in a group](src/scenario.rs#L645) (`TerminateInstanceInAutoScalingGroup`) * [Update a group](src/bin/update-autoscaling-group.rs#L32) (`UpdateAutoScalingGroup`) +### Scenarios + +Code examples that show you how to accomplish a specific task by calling multiple +functions within the same service. + +* [Manage groups and instances](README.md) + ## Run the examples ### Instructions @@ -47,14 +65,36 @@ Code excerpts that show you how to call individual service functions. +#### Hello Auto Scaling + +This example shows you how to get started using Auto Scaling. + + + +#### Manage groups and instances + +This example shows you how to do the following: + +* Create an Amazon EC2 Auto Scaling group with a launch template and Availability Zones, and get information about running instances. +* Enable Amazon CloudWatch metrics collection. +* Update the group's desired capacity and wait for an instance to start. +* Terminate an instance in the group. +* List scaling activities that occur in response to user requests and capacity changes. +* Get statistics for CloudWatch metrics, then clean up resources. + + + + + + ### Tests ⚠ Running tests might result in charges to your AWS account. -To find instructions for running these tests, see the [README](../README.md#Tests) +To find instructions for running these tests, see the [README](../../README.md#Tests) in the `rust_dev_preview` folder. diff --git a/rust_dev_preview/examples/autoscaling/src/bin/create-autoscaling-group.rs b/rust_dev_preview/examples/auto-scaling/src/bin/create-autoscaling-group.rs similarity index 100% rename from rust_dev_preview/examples/autoscaling/src/bin/create-autoscaling-group.rs rename to rust_dev_preview/examples/auto-scaling/src/bin/create-autoscaling-group.rs diff --git a/rust_dev_preview/examples/autoscaling/src/bin/delete-autoscaling-group.rs b/rust_dev_preview/examples/auto-scaling/src/bin/delete-autoscaling-group.rs similarity index 100% rename from rust_dev_preview/examples/autoscaling/src/bin/delete-autoscaling-group.rs rename to rust_dev_preview/examples/auto-scaling/src/bin/delete-autoscaling-group.rs diff --git a/rust_dev_preview/examples/autoscaling/src/bin/list-autoscaling-groups.rs b/rust_dev_preview/examples/auto-scaling/src/bin/list-autoscaling-groups.rs similarity index 100% rename from rust_dev_preview/examples/autoscaling/src/bin/list-autoscaling-groups.rs rename to rust_dev_preview/examples/auto-scaling/src/bin/list-autoscaling-groups.rs diff --git a/rust_dev_preview/examples/auto-scaling/src/bin/scenario.rs b/rust_dev_preview/examples/auto-scaling/src/bin/scenario.rs new file mode 100644 index 00000000000..a0ea12383e0 --- /dev/null +++ b/rust_dev_preview/examples/auto-scaling/src/bin/scenario.rs @@ -0,0 +1,191 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use std::{collections::BTreeSet, fmt::Display}; + +use anyhow::anyhow; +use autoscaling_code_examples::scenario::{AutoScalingScenario, ScenarioError}; +use tracing::{info, warn}; + +async fn show_scenario_description(scenario: &AutoScalingScenario, event: &str) { + let description = scenario.describe_scenario().await; + info!("DescribeAutoScalingInstances: {event}\n{description}"); +} + +#[derive(Default, Debug)] +struct Warnings(Vec); + +impl Warnings { + pub fn push(&mut self, warning: &str, error: ScenarioError) { + let formatted = format!("{warning}: {error}"); + warn!("{formatted}"); + self.0.push(formatted); + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl Display for Warnings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Warnings:")?; + for warning in &self.0 { + writeln!(f, "{: >4}- {warning}", "")?; + } + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + tracing_subscriber::fmt::init(); + + let shared_config = aws_config::from_env().load().await; + + let mut warnings = Warnings::default(); + + // 1. Create an EC2 launch template that you'll use to create an auto scaling group. Bonus: use SDK with EC2.CreateLaunchTemplate to create the launch template. + // 2. CreateAutoScalingGroup: pass it the launch template you created in step 0. Give it min/max of 1 instance. + // 4. EnableMetricsCollection: enable all metrics or a subset. + let scenario = match AutoScalingScenario::prepare_scenario(&shared_config).await { + Ok(scenario) => scenario, + Err(errs) => { + let err_str = errs + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join(", "); + return Err(anyhow!("Failed to initialize scenario: {err_str}")); + } + }; + + info!("Prepared autoscaling scenario:\n{scenario}"); + + let stable = scenario.wait_for_stable(1).await; + if let Err(err) = stable { + warnings.push( + "There was a problem while waiting for group to be stable", + err, + ); + } + + // 3. DescribeAutoScalingInstances: show that one instance has launched. + show_scenario_description( + &scenario, + "show that the group was created and one instance has launched", + ) + .await; + + // 5. UpdateAutoScalingGroup: update max size to 3. + let scale_max_size = scenario.scale_max_size(3).await; + if let Err(err) = scale_max_size { + warnings.push("There was a problem scaling max size", err); + } + + // 6. DescribeAutoScalingGroups: the current state of the group + show_scenario_description( + &scenario, + "show the current state of the group after setting max size", + ) + .await; + + // 7. SetDesiredCapacity: set desired capacity to 2. + let scale_desired_capacity = scenario.scale_desired_capacity(2).await; + if let Err(err) = scale_desired_capacity { + warnings.push("There was a problem setting desired capacity", err); + } + + // Wait for a second instance to launch. + let stable = scenario.wait_for_stable(2).await; + if let Err(err) = stable { + warnings.push( + "There was a problem while waiting for group to be stable", + err, + ); + } + + // 8. DescribeAutoScalingInstances: show that two instances are launched. + show_scenario_description( + &scenario, + "show that two instances are launched after setting desired capacity", + ) + .await; + + let ids_before = scenario + .list_instances() + .await + .map(|v| v.into_iter().collect::>()) + .unwrap_or_default(); + + // 9. TerminateInstanceInAutoScalingGroup: terminate one of the instances in the group. + let terminate_some_instance = scenario.terminate_some_instance().await; + if let Err(err) = terminate_some_instance { + warnings.push("There was a problem replacing an instance", err); + } + + let wait_after_terminate = scenario.wait_for_stable(1).await; + if let Err(err) = wait_after_terminate { + warnings.push( + "There was a problem waiting after terminating an instance", + err, + ); + } + + let wait_scale_up_after_terminate = scenario.wait_for_stable(2).await; + if let Err(err) = wait_scale_up_after_terminate { + warnings.push( + "There was a problem waiting for scale up after terminating an instance", + err, + ); + } + + let ids_after = scenario + .list_instances() + .await + .map(|v| v.into_iter().collect::>()) + .unwrap_or_default(); + + let difference = ids_after.intersection(&ids_before).count(); + if !(difference == 1 && ids_before.len() == 2 && ids_after.len() == 2) { + warnings.push( + "Before and after set not different", + ScenarioError::with(format!("{difference}")), + ); + } + + // 10. DescribeScalingActivities: list the scaling activities that have occurred for the group so far. + show_scenario_description( + &scenario, + "list the scaling activities that have occurred for the group so far", + ) + .await; + + // 11. DisableMetricsCollection + let scale_group = scenario.scale_group_to_zero().await; + if let Err(err) = scale_group { + warnings.push("There was a problem scaling the group to 0", err); + } + show_scenario_description(&scenario, "Scenario scaled to 0").await; + + // 12. DeleteAutoScalingGroup (to delete the group you must stop all instances): + // 13. Delete LaunchTemplate. + let clean_scenario = scenario.clean_scenario().await; + if let Err(errs) = clean_scenario { + for err in errs { + warnings.push("There was a problem cleaning the scenario", err); + } + } else { + info!("The scenario has been cleaned up!"); + } + + if warnings.is_empty() { + Ok(()) + } else { + Err(anyhow!( + "There were warnings during scenario execution:\n{warnings}" + )) + } +} diff --git a/rust_dev_preview/examples/autoscaling/src/bin/update-autoscaling-group.rs b/rust_dev_preview/examples/auto-scaling/src/bin/update-autoscaling-group.rs similarity index 100% rename from rust_dev_preview/examples/autoscaling/src/bin/update-autoscaling-group.rs rename to rust_dev_preview/examples/auto-scaling/src/bin/update-autoscaling-group.rs diff --git a/rust_dev_preview/examples/auto-scaling/src/lib.rs b/rust_dev_preview/examples/auto-scaling/src/lib.rs new file mode 100644 index 00000000000..b632e6235b3 --- /dev/null +++ b/rust_dev_preview/examples/auto-scaling/src/lib.rs @@ -0,0 +1 @@ +pub mod scenario; diff --git a/rust_dev_preview/examples/auto-scaling/src/scenario.rs b/rust_dev_preview/examples/auto-scaling/src/scenario.rs new file mode 100644 index 00000000000..87b1135be3c --- /dev/null +++ b/rust_dev_preview/examples/auto-scaling/src/scenario.rs @@ -0,0 +1,685 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +use std::{ + error::Error, + fmt::{Debug, Display}, + time::{Duration, SystemTime}, +}; + +use anyhow::anyhow; +use aws_sdk_autoscaling::{ + error::{DisplayErrorContext, ProvideErrorMetadata}, + types::{Activity, AutoScalingGroup, LaunchTemplateSpecification}, +}; +use aws_sdk_ec2::types::RequestLaunchTemplateData; +use tracing::trace; + +const LAUNCH_TEMPLATE_NAME: &str = "SDK_Code_Examples_EC2_Autoscaling_template_from_Rust_SDK"; +const AUTOSCALING_GROUP_NAME: &str = "SDK_Code_Examples_EC2_Autoscaling_Group_from_Rust_SDK"; +const MAX_WAIT: Duration = Duration::from_secs(5 * 60); // Wait at most 25 seconds. +const WAIT_TIME: Duration = Duration::from_millis(500); // Wait half a second at a time. + +struct Waiter { + start: SystemTime, + max: Duration, +} + +impl Waiter { + fn new() -> Self { + Waiter { + start: SystemTime::now(), + max: MAX_WAIT, + } + } + + async fn sleep(&self) -> Result<(), ScenarioError> { + if SystemTime::now() + .duration_since(self.start) + .unwrap_or(Duration::MAX) + > self.max + { + Err(ScenarioError::with( + "Exceeded maximum wait duration for stable group", + )) + } else { + tokio::time::sleep(WAIT_TIME).await; + Ok(()) + } + } +} + +pub struct AutoScalingScenario { + ec2: aws_sdk_ec2::Client, + autoscaling: aws_sdk_autoscaling::Client, + launch_template_arn: String, + auto_scaling_group_name: String, +} + +impl Display for AutoScalingScenario { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "\tLaunch Template ID: {}\n", + self.launch_template_arn + ))?; + f.write_fmt(format_args!( + "\tScaling Group Name: {}\n", + self.auto_scaling_group_name + ))?; + + Ok(()) + } +} + +pub struct AutoScalingScenarioDescription { + group: Result, ScenarioError>, + instances: Result, anyhow::Error>, + activities: Result, anyhow::Error>, +} + +impl Display for AutoScalingScenarioDescription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "\t Group status:")?; + match &self.group { + Ok(groups) => { + for status in groups { + writeln!(f, "\t\t- {status}")?; + } + } + Err(e) => writeln!(f, "\t\t! - {e}")?, + } + writeln!(f, "\t Instances:")?; + match &self.instances { + Ok(instances) => { + for instance in instances { + writeln!(f, "\t\t- {instance}")?; + } + } + Err(e) => writeln!(f, "\t\t! {e}")?, + } + + writeln!(f, "\t Activities:")?; + match &self.activities { + Ok(activities) => { + for activity in activities { + writeln!( + f, + "\t\t- {} Progress: {}% Status: {:?} End: {:?}", + activity.cause(), + activity.progress(), + activity.status_code(), + // activity.status_message().unwrap_or_default() + activity.end_time(), + )?; + } + } + Err(e) => writeln!(f, "\t\t! {e}")?, + } + + Ok(()) + } +} + +#[derive(Debug)] +struct MetadataError { + message: Option, + code: Option, +} + +impl MetadataError { + fn from(err: &dyn ProvideErrorMetadata) -> Self { + MetadataError { + message: err.message().map(|s| s.to_string()), + code: err.code().map(|s| s.to_string()), + } + } +} + +impl Display for MetadataError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let display = match (&self.message, &self.code) { + (None, None) => "Unknown".to_string(), + (None, Some(code)) => format!("({code})"), + (Some(message), None) => message.to_string(), + (Some(message), Some(code)) => format!("{message} ({code})"), + }; + write!(f, "{display}") + } +} + +#[derive(Debug)] +pub struct ScenarioError { + message: String, + context: Option, +} + +impl ScenarioError { + pub fn with(message: impl Into) -> Self { + ScenarioError { + message: message.into(), + context: None, + } + } + + pub fn new(message: impl Into, err: &dyn ProvideErrorMetadata) -> Self { + ScenarioError { + message: message.into(), + context: Some(MetadataError::from(err)), + } + } +} + +impl Error for ScenarioError { + // While `Error` can capture `source` information about the underlying error, for this example + // the ScenarioError captures the underlying information in MetadataError and treats it as a + // single Error from this Crate. In other contexts, it may be appropriate to model the error + // as including the SdkError as its source. +} +impl Display for ScenarioError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.context { + Some(c) => write!(f, "{}: {}", self.message, c), + None => write!(f, "{}", self.message), + } + } +} + +impl AutoScalingScenario { + pub async fn prepare_scenario( + sdk_config: &aws_types::sdk_config::SdkConfig, + ) -> Result> { + let ec2 = aws_sdk_ec2::Client::new(sdk_config); + let autoscaling = aws_sdk_autoscaling::Client::new(sdk_config); + + let auto_scaling_group_name = String::from(AUTOSCALING_GROUP_NAME); + + // Before creating any resources, prepare the list of AZs + let availablity_zones = ec2.describe_availability_zones().send().await; + if let Err(err) = availablity_zones { + return Err(vec![ScenarioError::new("Failed to find AZs", &err)]); + } + + let availability_zones: Vec = availablity_zones + .unwrap() + .availability_zones + .unwrap_or_default() + .iter() + .take(3) + .map(|z| z.zone_name.clone().unwrap()) + .collect(); + + // 1. Create an EC2 launch template that you'll use to create an auto scaling group. Bonus: use SDK with EC2.CreateLaunchTemplate to create the launch template. + // * Recommended: InstanceType='t1.micro', ImageId='ami-0ca285d4c2cda3300' + let create_launch_template = ec2 + .create_launch_template() + .launch_template_name(LAUNCH_TEMPLATE_NAME) + .launch_template_data( + RequestLaunchTemplateData::builder() + .instance_type(aws_sdk_ec2::types::InstanceType::T1Micro) + .image_id("ami-0ca285d4c2cda3300") + .build(), + ) + .send() + .await + .map_err(|err| vec![ScenarioError::new("Failed to create launch template", &err)])?; + + let launch_template_arn = match create_launch_template.launch_template { + Some(launch_template) => launch_template.launch_template_id.unwrap_or_default(), + None => { + // Try to delete the launch template + let _ = ec2 + .delete_launch_template() + .launch_template_name(LAUNCH_TEMPLATE_NAME) + .send() + .await; + return Err(vec![ScenarioError::with("Failed to load launch template")]); + } + }; + + // 2. CreateAutoScalingGroup: pass it the launch template you created in step 0. Give it min/max of 1 instance. + // You can use EC2.describe_availability_zones() to get a list of AZs (you have to specify an AZ when you create the group). + // Wait for instance to launch. Use a waiter if you have one, otherwise DescribeAutoScalingInstances until LifecycleState='InService' + if let Err(err) = autoscaling + .create_auto_scaling_group() + .auto_scaling_group_name(auto_scaling_group_name.as_str()) + .launch_template( + LaunchTemplateSpecification::builder() + .launch_template_id(launch_template_arn.clone()) + .version("$Latest") + .build(), + ) + .max_size(1) + .min_size(1) + .set_availability_zones(Some(availability_zones)) + .send() + .await + { + let mut errs = vec![ScenarioError::new( + "Failed to create autoscaling group", + &err, + )]; + + if let Err(err) = autoscaling + .delete_auto_scaling_group() + .auto_scaling_group_name(auto_scaling_group_name.as_str()) + .send() + .await + { + errs.push(ScenarioError::new( + "Failed to clean up autoscaling group", + &err, + )); + } + + if let Err(err) = ec2 + .delete_launch_template() + .launch_template_id(launch_template_arn.clone()) + .send() + .await + { + errs.push(ScenarioError::new( + "Failed to clean up launch template", + &err, + )); + } + return Err(errs); + } + + let scenario = AutoScalingScenario { + ec2, + autoscaling: autoscaling.clone(), // Clients are cheap so cloning here to prevent a move is ok. + auto_scaling_group_name: auto_scaling_group_name.clone(), + launch_template_arn, + }; + + // snippet-start:[rust.auto-scaling.scenario.enable_metrics_collection] + let enable_metrics_collection = autoscaling + .enable_metrics_collection() + .auto_scaling_group_name(auto_scaling_group_name.as_str()) + .granularity("1Minute") + .set_metrics(Some(vec![ + String::from("GroupMinSize"), + String::from("GroupMaxSize"), + String::from("GroupDesiredCapacity"), + String::from("GroupInServiceInstances"), + String::from("GroupTotalInstances"), + ])) + .send() + .await; + // snippet-end:[rust.auto-scaling.scenario.enable_metrics_collection] + + match enable_metrics_collection { + Ok(_) => Ok(scenario), + Err(err) => { + scenario.clean_scenario().await?; + Err(vec![ScenarioError::new( + "Failed to enable metrics collections for group", + &err, + )]) + } + } + } + + pub async fn clean_scenario(self) -> Result<(), Vec> { + let _ = self.wait_for_no_scaling().await; + let delete_group = self + .autoscaling + .delete_auto_scaling_group() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .send() + .await; + + // 14. Delete LaunchTemplate. + let delete_launch_template = self + .ec2 + .delete_launch_template() + .launch_template_id(self.launch_template_arn.clone()) + .send() + .await; + + let early_exit = match (delete_group, delete_launch_template) { + (Ok(_), Ok(_)) => Ok(()), + (Ok(_), Err(e)) => Err(vec![ScenarioError::new( + "There was an error cleaning the launch template", + &e, + )]), + (Err(e), Ok(_)) => Err(vec![ScenarioError::new( + "There was an error cleaning the scale group", + &e, + )]), + (Err(e1), Err(e2)) => Err(vec![ + ScenarioError::new("Multiple error cleaning the scenario Scale Group", &e1), + ScenarioError::new("Multiple error cleaning the scenario Launch Template", &e2), + ]), + }; + + if early_exit.is_err() { + early_exit + } else { + // Wait for delete_group to finish + let waiter = Waiter::new(); + let mut errors = Vec::::new(); + while errors.len() < 3 { + if let Err(e) = waiter.sleep().await { + errors.push(e); + continue; + } + let describe_group = self + .autoscaling + .describe_auto_scaling_groups() + .auto_scaling_group_names(self.auto_scaling_group_name.clone()) + .send() + .await; + match describe_group { + Ok(group) => match group.auto_scaling_groups().first() { + Some(group) => { + if group.status() != Some("Delete in progress") { + errors.push(ScenarioError::with(format!( + "Group in an unknown state while deleting: {}", + group.status().unwrap_or("unknown error") + ))); + return Err(errors); + } + } + None => return Ok(()), + }, + Err(err) => { + errors.push(ScenarioError::new("Failed to describe autoscaling group during cleanup 3 times, last error", &err)); + } + } + if errors.len() > 3 { + return Err(errors); + } + } + Err(vec![ScenarioError::with( + "Exited cleanup wait loop without retuning success or failing after three rounds", + )]) + } + } + + // snippet-start:[rust.auto-scaling.scenario.describe_scenario] + pub async fn describe_scenario(&self) -> AutoScalingScenarioDescription { + let group = self + .autoscaling + .describe_auto_scaling_groups() + .auto_scaling_group_names(self.auto_scaling_group_name.clone()) + .send() + .await + .map(|s| { + s.auto_scaling_groups() + .iter() + .map(|s| { + format!( + "{}: {}", + s.auto_scaling_group_name(), + s.status().unwrap_or_default() + ) + }) + .collect::>() + }) + .map_err(|e| { + ScenarioError::new("Failed to describe auto scaling groups for scenario", &e) + }); + + let instances = self + .list_instances() + .await + .map_err(|e| anyhow!("There was an error listing instances: {e}",)); + + // 10. DescribeScalingActivities: list the scaling activities that have occurred for the group so far. + // Bonus: use CloudWatch API to get and show some metrics collected for the group. + // CW.ListMetrics with Namespace='AWS/AutoScaling' and Dimensions=[{'Name': 'AutoScalingGroupName', 'Value': }] + // CW.GetMetricStatistics with Statistics='Sum'. Start and End times must be in UTC! + let activities = self + .autoscaling + .describe_scaling_activities() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .into_paginator() + .items() + .send() + .collect::, _>>() + .await + .map_err(|e| { + anyhow!( + "There was an error retrieving scaling activities: {}", + DisplayErrorContext(&e) + ) + }); + + AutoScalingScenarioDescription { + group, + instances, + activities, + } + } + // snippet-end:[rust.auto-scaling.scenario.describe_scenario] + + // snippet-start:[rust.auto-scaling.scenario.get_group] + async fn get_group(&self) -> Result { + let describe_auto_scaling_groups = self + .autoscaling + .describe_auto_scaling_groups() + .auto_scaling_group_names(self.auto_scaling_group_name.clone()) + .send() + .await; + + if let Err(err) = describe_auto_scaling_groups { + return Err(ScenarioError::new( + format!( + "Failed to get status of autoscaling group {}", + self.auto_scaling_group_name.clone() + ) + .as_str(), + &err, + )); + } + + let describe_auto_scaling_groups_output = describe_auto_scaling_groups.unwrap(); + let auto_scaling_groups = describe_auto_scaling_groups_output.auto_scaling_groups(); + let auto_scaling_group = auto_scaling_groups.first(); + + if auto_scaling_group.is_none() { + return Err(ScenarioError::with(format!( + "Could not find autoscaling group {}", + self.auto_scaling_group_name.clone() + ))); + } + + Ok(auto_scaling_group.unwrap().clone()) + } + // snippet-end:[rust.auto-scaling.scenario.get_group] + + pub async fn wait_for_no_scaling(&self) -> Result<(), ScenarioError> { + let waiter = Waiter::new(); + let mut scaling = true; + while scaling { + waiter.sleep().await?; + let describe_activities = self + .autoscaling + .describe_scaling_activities() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .send() + .await + .map_err(|e| { + ScenarioError::new("Failed to get autoscaling activities for group", &e) + })?; + let activities = describe_activities.activities(); + trace!( + "Waiting for no scaling found {} activities", + activities.len() + ); + scaling = activities.iter().any(|a| a.progress < 100); + } + Ok(()) + } + + pub async fn wait_for_stable(&self, size: usize) -> Result<(), ScenarioError> { + self.wait_for_no_scaling().await?; + + let mut group = self.get_group().await?; + let mut count = count_group_instances(&group); + + let waiter = Waiter::new(); + while count != size { + trace!("Waiting for stable {size} (current: {count})"); + waiter.sleep().await?; + group = self.get_group().await?; + count = count_group_instances(&group); + } + + Ok(()) + } + + // snippet-start:[rust.auto-scaling.scenario.list_instances] + pub async fn list_instances(&self) -> Result, ScenarioError> { + // The direct way to list instances is by using DescribeAutoScalingGroup's instances property. However, this returns a Vec, as opposed to a Vec. + // Ok(self.get_group().await?.instances.unwrap_or_default().map(|i| i.instance_id.clone().unwrap_or_default()).filter(|id| !id.is_empty()).collect()) + + // Alternatively, and for the sake of example, DescribeAutoScalingInstances returns a list that can be filtered by the client. + self.autoscaling + .describe_auto_scaling_instances() + .into_paginator() + .items() + .send() + .try_collect() + .await + .map(|items| { + items + .into_iter() + .filter(|i| i.auto_scaling_group_name == self.auto_scaling_group_name) + .map(|i| i.instance_id) + .filter(|id| !id.is_empty()) + .collect::>() + }) + .map_err(|err| ScenarioError::new("Failed to get list of auto scaling instances", &err)) + } + // snippet-end:[rust.auto-scaling.scenario.list_instances] + + pub async fn scale_min_size(&self, size: i32) -> Result<(), ScenarioError> { + let update_group = self + .autoscaling + .update_auto_scaling_group() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .min_size(size) + .send() + .await; + if let Err(err) = update_group { + return Err(ScenarioError::new( + format!("Failer to update group to min size ({size}))").as_str(), + &err, + )); + } + Ok(()) + } + + pub async fn scale_max_size(&self, size: i32) -> Result<(), ScenarioError> { + // 5. UpdateAutoScalingGroup: update max size to 3. + let update_group = self + .autoscaling + .update_auto_scaling_group() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .max_size(size) + .send() + .await; + if let Err(err) = update_group { + return Err(ScenarioError::new( + format!("Failed to update group to max size ({size})").as_str(), + &err, + )); + } + Ok(()) + } + + // snippet-start:[rust.auto-scaling.scenario.scale_desired_capacity] + pub async fn scale_desired_capacity(&self, capacity: i32) -> Result<(), ScenarioError> { + // 7. SetDesiredCapacity: set desired capacity to 2. + // Wait for a second instance to launch. + let update_group = self + .autoscaling + .set_desired_capacity() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .desired_capacity(capacity) + .send() + .await; + if let Err(err) = update_group { + return Err(ScenarioError::new( + format!("Failed to update group to desired capacity ({capacity}))").as_str(), + &err, + )); + } + Ok(()) + } + // snippet-end:[rust.auto-scaling.scenario.scale_desired_capacity] + + pub async fn scale_group_to_zero(&self) -> Result<(), ScenarioError> { + // snippet-start:[rust.auto-scaling.scenario.disable_metrics_collection] + // If this fails it's fine, just means there are extra cloudwatch metrics events for the scale-down. + let _ = self + .autoscaling + .disable_metrics_collection() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .send() + .await; + // snippet-end:[rust.auto-scaling.scenario.disable_metrics_collection] + + // 12. DeleteAutoScalingGroup (to delete the group you must stop all instances): + // UpdateAutoScalingGroup with MinSize=0 + let update_group = self + .autoscaling + .update_auto_scaling_group() + .auto_scaling_group_name(self.auto_scaling_group_name.clone()) + .min_size(0) + .desired_capacity(0) + .send() + .await; + if let Err(err) = update_group { + return Err(ScenarioError::new( + "Failed to update group for scaling down&", + &err, + )); + } + + let stable = self.wait_for_stable(0).await; + if let Err(err) = stable { + return Err(ScenarioError::with(format!( + "Error while waiting for group to be stable on scale down: {err}" + ))); + } + + Ok(()) + } + + // snippet-start:[rust.auto-scaling.scenario.terminate_some_instance] + pub async fn terminate_some_instance(&self) -> Result<(), ScenarioError> { + // Retrieve a list of instances in the auto scaling group. + let auto_scaling_group = self.get_group().await?; + let instances = auto_scaling_group.instances(); + // Or use other logic to find an instance to terminate. + let instance = instances.first(); + if let Some(instance) = instance { + let termination = self + .ec2 + .terminate_instances() + .instance_ids(instance.instance_id()) + .send() + .await; + if let Err(err) = termination { + Err(ScenarioError::new( + "There was a problem terminating an instance", + &err, + )) + } else { + Ok(()) + } + } else { + Err(ScenarioError::with("There was no instance to terminate")) + } + } + // snippet-end:[rust.auto-scaling.scenario.terminate_some_instance] +} + +fn count_group_instances(group: &AutoScalingGroup) -> usize { + group.instances.as_ref().map(|i| i.len()).unwrap_or(0) +} diff --git a/rust_dev_preview/examples/ec2/Cargo.toml b/rust_dev_preview/examples/ec2/Cargo.toml index f1e3575f66b..400eb63a003 100644 --- a/rust_dev_preview/examples/ec2/Cargo.toml +++ b/rust_dev_preview/examples/ec2/Cargo.toml @@ -9,10 +9,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [dependencies] aws-config = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "next" } aws-sdk-ec2 = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "next" } aws-types = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "next" } tokio = { version = "1.20.1", features = ["full"] } clap = { version = "~4.2", features = ["derive"] } -tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }