├── .gitignore ├── CODE_OF_CONDUCT. md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE.txt ├── README.md ├── environment ├── README.md ├── configure.sh └── environment.yaml ├── features ├── custom-termination-policies │ ├── metric-based-termination │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.py │ │ ├── cdk.json │ │ ├── images │ │ │ ├── architecture.png │ │ │ └── lambda.png │ │ ├── main.drawio │ │ ├── metric_based_termination │ │ │ ├── __init__.py │ │ │ ├── assets │ │ │ │ ├── func_termination_policy │ │ │ │ │ └── index.py │ │ │ │ ├── stress_document.yml │ │ │ │ └── user_data.txt │ │ │ └── metric_based_termination_stack.py │ │ ├── requirements-dev.txt │ │ ├── requirements.txt │ │ └── source.bat │ └── quick-start-example │ │ ├── README.md │ │ └── template.yaml ├── faster-target-tracking │ ├── FasterScalingCFN.yml │ ├── LICENSE │ └── README.md ├── lifecycle-hooks │ ├── lambda-managed-linux │ │ ├── README.md │ │ ├── source │ │ │ └── LifecycleFunction │ │ │ │ ├── app.py │ │ │ │ └── requirements.txt │ │ └── template.yaml │ ├── lambda-managed-windows │ │ ├── README.md │ │ ├── source │ │ │ └── LifecycleFunction │ │ │ │ ├── app.py │ │ │ │ └── requirements.txt │ │ └── template.yaml │ ├── userdata-managed-linux │ │ ├── README.md │ │ └── template.yaml │ ├── userdata-managed-windows-multi-reboot │ │ ├── README.md │ │ └── template.yaml │ └── userdata-managed-windows │ │ ├── README.md │ │ └── template.yaml ├── predictive-scaling-blue-green-deployment │ ├── README.md │ ├── predictive-scaling-policy-cpu.json │ └── template.yaml ├── predictive-scaling │ ├── README.md │ └── template.yaml └── warm-pools │ ├── README.md │ ├── scaling-policy.json │ └── ssm-stress.json └── tools └── launch-configuration-inventory ├── .gitignore ├── README.md ├── inventory.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | **/dist 3 | **/global-s3-assets 4 | **/regional-s3-assets 5 | **/open-source 6 | **/.zip 7 | **/tmp 8 | **/out-tsc 9 | 10 | # dependencies 11 | **/node_modules 12 | 13 | # e2e 14 | **/e2e/*.js 15 | **/e2e/*.map 16 | 17 | # misc 18 | **/npm-debug.log 19 | **/testem.log 20 | **/package-lock.json 21 | **/.vscode/settings.json 22 | 23 | # System Files 24 | **/.DS_Store 25 | **/.vscode 26 | 27 | # SAM Cache 28 | .aws-sam 29 | 30 | # Local Testing 31 | deploy.sh 32 | 33 | # Packaged Template 34 | packaged.yaml 35 | 36 | # SAM TOML 37 | samconfig.toml -------------------------------------------------------------------------------- /CODE_OF_CONDUCT. md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 12 | 13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 14 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 15 | 16 | * A reproducible test case or series of steps 17 | * The version of our code being used 18 | * Any modifications you've made relevant to the bug 19 | * Anything unusual about your environment or deployment 20 | 21 | 22 | ## Contributing via Pull Requests 23 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 24 | 25 | 1. You are working against the latest source on the *master* branch. 26 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 27 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 28 | 29 | To send us a pull request, please: 30 | 31 | 1. Fork the repository. 32 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 33 | 3. Ensure local tests pass. 34 | 4. Commit to your fork using clear commit messages. 35 | 5. Send us a pull request, answering any default questions in the pull request interface. 36 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 37 | 38 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 39 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 40 | 41 | 42 | ## Finding contributions to work on 43 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 44 | 45 | 46 | ## Code of Conduct 47 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 48 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 49 | opensource-codeofconduct@amazon.com with any additional questions or comments. 50 | 51 | 52 | ## Security issue notifications 53 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 54 | 55 | 56 | ## Licensing 57 | 58 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 59 | 60 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | amazon-ec2-auto-scaling-group-examples 2 | 3 | Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except 5 | in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/ 6 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 7 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the 8 | specific language governing permissions and limitations under the License. 9 | 10 | ********************** 11 | THIRD PARTY COMPONENTS 12 | ********************** 13 | This software includes third party software subject to the following copyrights: 14 | 15 | AWS SDK under the Apache License Version 2.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon EC2 Auto Scaling Group Examples 2 | 3 | This repository contains code samples, learning activities, and best-practices for scaling and elasticity with Amazon EC2 Auto Scaling groups. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with these examples. 8 | 9 | ## Examples 10 | 11 | * [Code Samples by Auto Scaling Features](/features) 12 | * [Custom Termination Policies](/features/custom-termination-policies) 13 | * [Lifecycle Hooks](/features/lifecycle-hooks) 14 | * [Predictive Scaling](/features/predictive-scaling) 15 | * [Predictive Scaling Blue Green Deployment](/features/predictive-scaling-blue-green-deployment) 16 | * [Warm Pools](/features/warm-pools) 17 | * [Faster Target Tracking](/features/faster-target-tracking) 18 | * [Auto Scaling Utilities and Tools](/tools) 19 | * [Launch Configuration Inventory Script](/tools/launch-configuration-inventory) -------------------------------------------------------------------------------- /environment/README.md: -------------------------------------------------------------------------------- 1 | # Amazon EC2 Auto Scaling Group Examples Environment 2 | 3 | ## Overview 4 | 5 | Deploy this environment if you want an AWS Cloud9 Environment pre-configured to run the examples contained in this repository. 6 | 7 | ## Deployment 8 | 9 | This stack will deploy an AWS Cloud9 Environment running on a t3.small instance in your default VPC. If you've removed or changed your default VPC then you may run into issues running this environment. You will be charged for running this environment based on AWS Cloud9 [pricing](https://aws.amazon.com/cloud9/pricing/). 10 | 11 | 1. Click the Launch Stack button below to deploy this environment. 12 | 2. Switch your region if necessary. 13 | 3. Follow the instructions to deploy the CloudFormation template. This template does not require any parameters. 14 | 4. Complete the post deployment tasks below after the stack has been deployed. 15 | 16 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=AutoScalingGroupExampleEnvironment&templateURL=https://amazon-ec2-auto-scaling-group-examples.s3-us-west-2.amazonaws.com/environment.yaml) 17 | 18 | ## Post Deployment 19 | 20 | 1. Open Cloud9 in the region you deployed the CloudFormation template. 21 | 2. Select "Your Environments" from the menu on the left side of the page. 22 | 3. Locate the environment with a name starting with "AutoScalingGroupExampleEnvironment" and click Open IDE. 23 | 4. When the environment launches for the first time, this repository will be cloned into ~/environment. 24 | 5. Change directories to ~/environment/amazon-ec2-auto-scaling-group-examples/environment. 25 | 26 | ```bash 27 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/environment 28 | ``` 29 | 6. Run the configuration script to configure the environment. This script will take several minutes to complete. 30 | 31 | ```bash 32 | sh configure.sh 33 | ``` 34 | 35 | 7. After the script has completed, OPEN A NEW TERMINAL SESSION from the Menu Bar by navigating to Window, New Terminal. 36 | 8. Close all other open terminal sessions. 37 | 9. You are now ready to use the examples in this repository. Each example has a README file that contains further instructions. 38 | 39 | ## Clean Up 40 | 41 | 1. Navigate to CloudFormation in the AWS console and delete the example stack. If you have a configured AWS CLI environment, and used the default stack name when launching the stack, you can delete it with the following command. 42 | 43 | ```bash 44 | aws cloudformation delete-stack --stack-name AutoScalingGroupExampleEnvironment 45 | ``` 46 | -------------------------------------------------------------------------------- /environment/configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) 6 | CURRENT_REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/\(.*\)[a-z]/\1/') 7 | RED='\033[0;31m' 8 | YELLOW='\033[1;33m' 9 | NC='\033[0m' 10 | 11 | export INFOPATH="/home/linuxbrew/.linuxbrew/share/info" 12 | 13 | function _logger() { 14 | echo -e "$(date) ${YELLOW}[*] $@ ${NC}" 15 | } 16 | 17 | function resize_volume() { 18 | SIZE=${1:-20} 19 | INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id) 20 | VOLUMEID=$(aws ec2 describe-instances \ 21 | --instance-id $INSTANCEID \ 22 | --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ 23 | --output text) 24 | 25 | aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE 26 | 27 | while [ \ 28 | "$(aws ec2 describe-volumes-modifications \ 29 | --volume-id $VOLUMEID \ 30 | --filters Name=modification-state,Values="optimizing","completed" \ 31 | --query "length(VolumesModifications)"\ 32 | --output text)" != "1" ]; do 33 | sleep 1 34 | done 35 | 36 | if [ $(readlink -f /dev/xvda) = "/dev/xvda" ] 37 | then 38 | sudo growpart /dev/xvda 1 39 | STR=$(cat /etc/os-release) 40 | SUB="VERSION_ID=\"2\"" 41 | if [[ "$STR" == *"$SUB"* ]] 42 | then 43 | sudo xfs_growfs -d / 44 | else 45 | sudo resize2fs /dev/xvda1 46 | fi 47 | else 48 | sudo growpart /dev/nvme0n1 1 49 | STR=$(cat /etc/os-release) 50 | SUB="VERSION_ID=\"2\"" 51 | if [[ "$STR" == *"$SUB"* ]] 52 | then 53 | sudo xfs_growfs -d / 54 | else 55 | sudo resize2fs /dev/nvme0n1p1 56 | fi 57 | fi 58 | } 59 | 60 | function upgrade_sam_cli() { 61 | _logger "[+] Backing up current SAM CLI" 62 | cp $(which sam) ~/.sam_old_backup 63 | 64 | _logger "[+] Installing latest SAM CLI" 65 | brew tap aws/tap 66 | brew install aws-sam-cli 67 | 68 | _logger "[+] Updating Cloud9 SAM binary" 69 | # Allows for local invoke within IDE (except debug run) 70 | ln -sf $(which sam) ~/.c9/bin/sam 71 | } 72 | 73 | function upgrade_existing_packages() { 74 | _logger "[+] Upgrading system packages" 75 | sudo yum update -y 76 | 77 | _logger "[+] Upgrading Python pip and setuptools" 78 | python3 -m pip install --upgrade pip setuptools --user 79 | 80 | _logger "[+] Installing latest AWS CLI" 81 | python3 -m pip install --upgrade --user awscli 82 | } 83 | 84 | function install_linuxbrew() { 85 | _logger "[+] Creating touch symlink" 86 | sudo ln -sf /bin/touch /usr/bin/touch 87 | _logger "[+] Installing homebrew..." 88 | echo | sh -c "$(curl -fsSL https://raw.githubusercontent.com/Linuxbrew/install/master/install.sh)" 89 | _logger "[+] Adding homebrew in PATH" 90 | test -d ~/.linuxbrew && eval $(~/.linuxbrew/bin/brew shellenv) 91 | test -d /home/linuxbrew/.linuxbrew && eval $(/home/linuxbrew/.linuxbrew/bin/brew shellenv) 92 | test -r ~/.bash_profile && echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.bash_profile 93 | echo "eval \$($(brew --prefix)/bin/brew shellenv)" >>~/.profile 94 | } 95 | 96 | function install_utilities() { 97 | sudo yum install -y jq 98 | brew install dateutils 99 | } 100 | 101 | function main() { 102 | resize_volume 103 | upgrade_existing_packages 104 | install_linuxbrew 105 | upgrade_sam_cli 106 | install_utilities 107 | 108 | echo 'export PATH="/home/linuxbrew/.linuxbrew/opt/python@3.8/bin:$PATH"' >> /home/ec2-user/.bash_profile 109 | 110 | echo -e "${RED} [!!!!!!!!!] OPEN A NEW TERMINAL AND CLOSE THIS ONE ${NC}" 111 | exec ${SHELL} 112 | } 113 | 114 | main -------------------------------------------------------------------------------- /environment/environment.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | 18 | Description: > 19 | amazon-ec2-auto-scaling-group-example-environment 20 | 21 | Deploys a Cloud9 IDE Environment for use with Amazon EC2 Auto Scaling Group examples. 22 | 23 | Resources: 24 | 25 | AutoScalingGroupExampleEnvironment: 26 | Type: AWS::Cloud9::EnvironmentEC2 27 | Properties: 28 | AutomaticStopTimeMinutes: 60 29 | Description: "Amazon EC2 Auto Scaling Group Example Environment" 30 | InstanceType: t3.small 31 | Repositories: 32 | - 33 | PathComponent: /amazon-ec2-auto-scaling-group-examples 34 | RepositoryUrl: https://github.com/aws-samples/amazon-ec2-auto-scaling-group-examples.git -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | 12 | .idea* 13 | -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/README.md: -------------------------------------------------------------------------------- 1 | # Custom Termination Policy Example: Using CloudWatch metric data to select instances for termination 2 | 3 | This example template demonstrates how to create a [custom termination policy](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lambda-custom-termination-policy.html) for an EC2 Auto Scaling group that uses CloudWatch metric data to select idle instances for termination. The stack will deploy the following resources: 4 | 5 | - A VPC that spans the whole region 6 | - A Lambda function that implements the logic of the custom termination policy and defines environment variables for specifying threshold values for the CloudWatch metrics 7 | - An Auto Scaling Group that uses attribute-based instance type selection (ABS) with a target tracking scaling policy, where the target metric is the average CPU consumption 8 | - A Launch Template that defines the AMI to use when procuring capacity in the Auto Scaling Group 9 | - A Systems Manager Document that executes [stress-ng](https://wiki.ubuntu.com/Kernel/Reference/stress-ng) within a shell script 10 | - A State Manager association that applies the SSM Document to all the instances in the ASG 11 | 12 | The diagram below shows the architecture that will be deployed: 13 | 14 | ## Architecture diagram 15 | 16 | ![Architecture diagram](images/architecture.png) 17 | 18 | The Lambda function is invoked by Amazon EC2 Auto Scaling in response to certain events. It processes the information in the input data sent by Amazon EC2 Auto Scaling and returns a list of instances that are ready to terminate. This example returns all instances whose average CPU utilization is below 50% during 5 minutes. To do this, the function performs an API call to retrieve metric data from Amazon CloudWatch. The state diagram below depicts the execution flow of the Lambda function: 19 | 20 | ![Execution flow](images/lambda.png) 21 | 22 | ## Expected outcome 23 | 24 | Once the stack is deployed, the following events will take place: 25 | 26 | 1. As many instances as specified in the parameter **DesiredCapacity** are launched to meet the initial demand for the ASG 27 | 2. The State Manager Association is automatically applied, increasing the CPU usage of the instances in the ASG 28 | 3. The dynamic scaling policy is triggered, since the target 50% of CPU usage is no longer met 29 | 4. The desired capacity of the ASG is increased to meet the new demand, and new instances are launched to procure more capacity 30 | 5. Some minutes after the State Manager Association is applied, the scale-in event is triggered invoking the Lambda function 31 | 6. The Lambda function selects instances for termination and the desired capacity of the ASG is updated accordingly 32 | 33 | Some tips: 34 | 35 | - You can verify the execution of the Lambda Function by: 36 | - Navigating to the [Lambda console](https://console.aws.amazon.com/lambda) 37 | - Selecting the function **customTerminationPolicy** 38 | - Selecting the **Monitor** tab and scrolling down to **Recent invocations** 39 | - You can apply the State Manager Association whenever you want by: 40 | - Navigating to the [State Manager console](https://console.aws.amazon.com/systems-manager/state-manager) 41 | - Ticking the Association whose Document name is **Stress** and selecting **Apply association now** 42 | 43 | ## Deployment instructions 44 | 45 | The following steps assume that you have Python and [venv](https://docs.python.org/3/library/venv.html) installed in your local machine. 46 | 47 | ### 1. Cloning the repository 48 | 49 | Navigate to the directory in your machine where you want the repository to be cloned and execute the following command: 50 | 51 | ```bash 52 | git clone https://github.com/aws-samples/amazon-ec2-auto-scaling-group-examples.git 53 | ``` 54 | 55 | ### 2. Creating a virtual environment and installing project dependencies 56 | 57 | After cloning this repository, navigate to the `features/custom-termination-policies/metric-based-termination` directory, and execute the following commands: 58 | 59 | #### 2.1 Creating the virtual environment 60 | 61 | ```python 62 | python3 -m venv .venv 63 | ``` 64 | 65 | #### 2.2 Installing project dependencies in the virtual environment 66 | 67 | ```python 68 | source .venv/bin/activate 69 | python -m pip install -r requirements.txt 70 | ``` 71 | 72 | ### 3. Bootstrapping your AWS account 73 | 74 | Deploying AWS CDK apps into an AWS environment may require that you provision resources the AWS CDK needs to perform the deployment. These resources include an Amazon S3 bucket for storing files and IAM roles that grant permissions needed to perform deployments. Execute the following command to bootstrap your environment: 75 | 76 | ```bash 77 | cdk bootstrap 78 | ``` 79 | 80 | You can read more about this process [here](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html). 81 | 82 | ### 4. Deploying using CDK 83 | 84 | When deploying you can optionally specify the value for these parameters: 85 | 86 | - **ASGName**: name of the Auto Scaling group. The default value is `Example ASG` 87 | - **MinCapacity**: minimum capacity that the Auto Scaling group has to procure. The default value is `1` 88 | - **MaxCapacity**: maximum capacity that the Auto Scaling group has to procure. The default value is `6` 89 | - **DesiredCapacity**: initial capacity that the Auto Scaling group has to procure. The default is `2` 90 | 91 | ```bash 92 | cdk deploy --parameters ASGName= --parameters MinCapacity= --parameters MaxCapacity= --parameters DesiredCapacity= 93 | ``` 94 | 95 | If you don't want to provide a value for any of those parameters you can simply execute the following command: 96 | 97 | ```bash 98 | cdk deploy 99 | ``` 100 | 101 | The deployment process will take roughly **5 minutes** to complete. 102 | 103 | ### 5. Cleaning up 104 | 105 | To delete all the resources created by CDK: 106 | 107 | 1. Navigate to the **CloudFormation** section in the AWS console. 108 | 2. Select the stack named **MetricBasedTerminationStack** and click on **Delete**. -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import aws_cdk as cdk 4 | 5 | from metric_based_termination.metric_based_termination_stack import MetricBasedTerminationStack 6 | 7 | 8 | app = cdk.App() 9 | 10 | MetricBasedTerminationStack(app, "MetricBasedTerminationStack") 11 | 12 | app.synth() 13 | -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/core:checkSecretUsage": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 30 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 31 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 32 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 33 | "@aws-cdk/core:target-partitions": [ 34 | "aws", 35 | "aws-cn" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-auto-scaling-group-examples/cffadfc4e51b37c7581eb73d5316961a57bb20aa/features/custom-termination-policies/metric-based-termination/images/architecture.png -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/images/lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-auto-scaling-group-examples/cffadfc4e51b37c7581eb73d5316961a57bb20aa/features/custom-termination-policies/metric-based-termination/images/lambda.png -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/main.drawio: -------------------------------------------------------------------------------- 1 | 7Vtbc5s4FP41fkwGcbUf40vadJJuZr1t2n3xyCBjNoA8ICf2/vqVQAKE5FtiN0nXbWaCDuIIdM73nQukYw2S1acMLuZ3OEBxxzSCVccadkwTmC6gv5hkXUq8rlUKwiwKSpFRC8bRv4hfKaTLKEA5l5UignFMooUs9HGaIp9IMphl+FmeNsNxIAkWMESKYOzDWJU+RAGZc6nr2PWJzygK52Jp4PbKMwkUs/mj5HMY4OeGyBp1rEGGMSmPktUAxWz35I253nC2urMMpWSfC77f36xub7zPXy4ev3y7WNv/PBHzgmt5gvGSP/HVw5gKBjFeBvy+yVrsxgJHKSl21OnTH7rewOg49MyAjS5NpyVojz1ZANQR0yEL2mNPFoC2etBaH7RvsCFQRpJ6o7W+0bhB+mP18ZLEUYoGle8ZVBhmMIioSQY4xhmVpTilu9efkySmI0APn+cRQeMF9NmuPlPcUNkMp4R7PzDFmG8800r9m0C6VsZ1FJZA2egJlQYp58QxXOTRtLoqQ/4yy6Mn9CfKS+VMSh1xwY6TVchAewmfc/syzPByUdz+DV1Le3ZCDyc+c4wJjAlTRDL8iMSDdkyL/r9mztefRXHc2oAnlJGIAusqjkKmn2C2HOSjGM0KjXRXojS8LUZDy+A7oVsigPkcBfyRVCwIx6arolVDxLHxCeEEkWxNp/CzAqZrefhco74rZPMG4B2DCyFnmrDSXGORHnA4HgBNU4Hm9/vBGZJnSDYh+bTwtUC0u11gnxSIV1d9r9/dDcStYWdvdFoyOh0VnS7QoNNyToVOSw2cf9MxUACqsYBiLmB7o/5Vcy/BRkO13bBllkrVnpYwj28JYGssYbqnsoStt4T521sCdN+bKRzVFEuCqYRl1JROijSfkdo5iv2/otgAMaW70kvqK5O89JRJHfzGBUb57SmAHXZdF1gviXS+uKcWbiuNEgXwgPgXUzGkpjodrF0Z1cBQYV2RcBPWFR0cHdYisjZwfZPmBKY+Lc3bUM4fEfHn3CO0jrwpo9/g4E3DMvPYFDCe4gp8spSMCPvfwimK73EekajwvSkmBCf7O0gDYrvgBPNF+aCzaMXuQ8ET2ISTqNrRvV3LOdS1gCP7lucqrmV3Vc8SsqM7Vu/sV7+FX1nvy62AJjlv5SFu0cCYZvQoZEeI7ePZ547kc81Ibp6yFmllwMB9a89TixHFqVAQIrHNOCNzHOIUxqNa2rQRSoMr1sauvQLF02Io7F14D8yImDaNsf8ohNdRLDQpmZNDnc9lPQKaaaVB5WhoFZEfYnV6/JPJLx0+Gq4a04brzkFVv9gLvMx8tGUe5x/6ACHa2kXgOGc7utVdmhWRSKcyFENCM9jGhXqH4OrumfM3XK8VSk27lX2VT8mvarblW4rsXYrKbVAUFf5ZPeMrXFbzCiCB/1Jy4W8BHiDjwK3UqC3ftCWcrozTlnJqOSdNKwoszQptoU7mqUKgThM1mSrUyXQFaPtqoLkatK4uyr/zdh51O18VuOm5a8u2u2bj3DCi9S+PvymjcDWy9wfAclwd7c6Kf+8xvGtDeYZKIivLdlr457oCvngr9MyIYnJAvD+4C+z2ehJXumpp7GnCvXeycK++o7mFyTSAaoI5WObMkDSqoSyJUljYl+4RjiN/fWbXj0QHXndk2IfRwdBwBkBN9H9XOohLEPw6Iqgq0DdjArX1/eES/5RuxI/moJH6s2Gd+xejdXN0j7KI7mTRRj78PeDOikBk+rtLAvCGJUGrGjV7Ly0JdinaUBJQT4DrxjQeODbesG3ZcvncNbbelyPg88L5HLQ1zsobPm454ypALD/nMg1K/FpMFiQro08h0jbfJlEQFJDN2AsQWL8ZkYP18AAobOq6VR+38VU6zc/HdORoXNqua0o7L4D2Sg+/6DqSWq/ll3g2y9FpqlTvzK/vgl/NM7/uza9t/hOtnX350nR/BWF2NxJmlL5nvhSMcAy+7MmB0DwOWwJxjWBhyzsBX44f7Dtv+vV+El4FX398ux2BPxLNx4OqKWsG9GOY55Ev23QTe9WE9VPiqwPYS/SY677yz07dcdb3mNlAz4ItLjomLYpPzvejxa0sKNq88sejR3E0tyfzGXgpMXp2S9HpetFar1Xf291RjdQxTRcmrMBNp/miqJUJfDtq2ptxZE/cjNMtzGRaPZlDjpTIta44HRGpbxfG65yghIZN4w6mMKQ2OXe/zu8WPuh2nt8tnLyZmJd8MUk4W2yh4f0C/95dxarg+AVdRS157vM1wWFZnEi8Dni5r0u8NmWDYEc2eMSEbVu02ZmvOW+ZrtmenGVZL03XnNbfLFl7pmuH1rGmIbd9bHDculRrSrWhPiaUIXLNp4EfMvOzt1LRhXFpWLZMRyI9fqX7iRZO1ZR4fSpIh/XfsZbT6z8Htkb/AQ==5VnbctowEP0aHtvB+AI8poSkeehMJ2mnyaNiL7YmQnJlOUC/vlpbvhMICXiGgQfwHl282j17LJmBPVuubyWJox8iADYYDYP1wL4ejEZTx9PfCGxywJ2McyCUNMghqwIe6D8w4NCgKQ0gaXRUQjBF4yboC87BVw2MSClWzW4LwZp3jUkIHeDBJ6yL/qGBigzquU7V8B1oGBW3trxp3rIkRW+zlCQigVjVIHs+sGdSCJVfLdczYBi8IjC/r54CL/h1/5iubsmSu/wvefmST3ZzyJByDRK4Ou7Uo3zqV8JSE7Bb4CCJAkwUXeLPinJcNyYOE6rDAhLjA0pSH6lBFDERUpsi7lKkPAC8tTWwv60iquAhJj62rjTRNBapJTPNRPqGOc5QmwvB1UwwIbOp7GH2QZwyVsMX+uMjnigpXqDRYkaY1YFUsG7xYU8srTLBujJA4GI3epyZpaDEpmmu6gwzWFQjl2swYkgdlhNXedMXJnUHpNHupLGTEAh0XRhTSBWJUHDC5hVaTwhGjOoiumI05Bp7FkqJpW4AHlxhVeIkMfAcMbmb7ExFRYhhNkov+7FuPKHx1S3M63W98XpjrHxZuJZGOhORSh/28/x9addMJzKEXf2c7fSQwIiir03ntmXbDP0pqHanopXd5JU1ahEm98uManGmdOPjNHI6NJqvlSQ+ukh5ogj3URDuggSlWGpG6FBFkAV2wwQJMBWQregZwzGf4XRXaSYcKMqUh5emFNZeqSixXqTCPV+pqNThqSEOx5UK55C8v0MqvJNIhTX8mFTolJBNrVuMHZK372O3HnXFfW/e8mvk7uqvL3IPjqpbXofT97g5gVfo7lO0kJWq1d7gpEkmULpVJJnQtTTvspSrlXhni3JZfSrX+HyV67BNDqypejRO4nVtlLaqQWh8Vu28I6vd5CRqN2k9RN8pdt0N1rhF6nFrojxen1bNtsP6vLnbr939T6Oak0493QWaBHSxyUYyqO36kpo6XvTBz9uviiXWiypOz04VP6BufR8XJ0dWRWu4nVWflEV72tJFp9/zYvH4b268UsmzcyADX0Gw5eh4UYrhtM/02xSj1xOgZZ2dZPRc/gWvj1f/p3lf5LaeRh3OnLr+uy+MukxijMZJizFJRGJsRzoofNncqdiykvuvWLelqpbbrVh7S8HahxesNqv/D/KsVP/C2PP/ -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/metric_based_termination/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-auto-scaling-group-examples/cffadfc4e51b37c7581eb73d5316961a57bb20aa/features/custom-termination-policies/metric-based-termination/metric_based_termination/__init__.py -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/metric_based_termination/assets/func_termination_policy/index.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import datetime 3 | import os 4 | 5 | # Available metrics: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/viewing_metrics_with_cloudwatch.html 6 | 7 | METRIC_NAME = os.getenv('METRIC_NAME') # Metric of which to retrieve data 8 | METRIC_THRESHOLD = float(os.getenv('METRIC_THRESHOLD')) # Value below which an instance is considered idle 9 | METRIC_STAT = os.getenv('METRIC_STAT') # Statistic to use when retrieving CloudWatch metric data 10 | METRIC_TIME_WINDOW_IN_MINUTES = int( # Time window for retrieving CloudWatch metric data 11 | os.getenv('METRIC_TIME_WINDOW_IN_MINUTES') 12 | ) 13 | 14 | METRICS = [ 15 | METRIC_NAME 16 | ] 17 | 18 | 19 | def generate_time_window(): 20 | """Generates a start time, end time and period for retrieving CloudWatch metrics 21 | """ 22 | 23 | end_time = datetime.datetime.now() 24 | start_time = end_time - datetime.timedelta(minutes=METRIC_TIME_WINDOW_IN_MINUTES) 25 | 26 | # Calculate the number of seconds in the time widow and use it as period to retrieve only one sample 27 | period = int((end_time - start_time).total_seconds()) 28 | 29 | return start_time, end_time, period 30 | 31 | 32 | def get_metric_data(instances, start_time, end_time, period): 33 | """Retrieves CloudWatch metric data 34 | 35 | Keyword arguments: 36 | instances -- list with instance information 37 | start_time -- time stamp that determines the first data point to return 38 | end_time -- time stamp that determines the last data point to return 39 | period -- the granularity of the returned data points 40 | """ 41 | 42 | client = boto3.client('cloudwatch') 43 | paginator = client.get_paginator('get_metric_data') 44 | 45 | # Hold the metrics for each instance in the form of: 46 | # + instanceId1 47 | # + metricName1: 0 48 | # + metricName2: 0 49 | # ... 50 | metric_data = {instance['InstanceId']: {metric_name: 0 for metric_name in METRICS} for instance in instances} 51 | 52 | # List that contains one entry per instance and metric used to retrieve CloudWatch metric data 53 | metric_data_queries = [ 54 | { 55 | 'Id': '{}{}'.format(instance['InstanceId'].replace('-', '_'), metric_name), 56 | 'MetricStat': { 57 | 'Metric': { 58 | 'Namespace': 'AWS/EC2', 59 | 'MetricName': metric_name, 60 | 'Dimensions': [ 61 | { 62 | 'Name': 'InstanceId', 63 | 'Value': instance['InstanceId'] 64 | } 65 | ] 66 | }, 67 | 'Stat': METRIC_STAT, 68 | 'Period': period 69 | }, 70 | 'ReturnData': True, 71 | 'Label': '{}_{}'.format(instance['InstanceId'], metric_name) 72 | } for instance in instances for metric_name in METRICS 73 | ] 74 | 75 | response = paginator.paginate( 76 | MetricDataQueries=metric_data_queries, 77 | StartTime=start_time, 78 | EndTime=end_time 79 | ) 80 | 81 | # Process the retrieved metrics and add them to the metric_data dictionary 82 | for page in response: 83 | for result in page['MetricDataResults']: 84 | instance_id = result['Label'].split('_')[0] 85 | metric_name = result['Label'].split('_')[1] 86 | 87 | if result['Values']: 88 | metric_data[instance_id][metric_name] = result['Values'][0] 89 | 90 | # Update the list of instances to include the retrieved metrics 91 | for instance in instances: 92 | if instance['InstanceId'] == instance_id: 93 | instance.update({'Metrics': metric_data[instance_id]}) 94 | 95 | 96 | def should_terminate_instance(instance, capacities): 97 | """Returns whether an instance should be selected for termination 98 | considering an even balancing across AZs 99 | 100 | Keyword arguments: 101 | instance -- dictionary with instance data 102 | capacities -- dictionary with the suggested number of instances to terminate per availability zone 103 | """ 104 | 105 | return instance['Metrics'][METRIC_NAME] < METRIC_THRESHOLD and capacities[instance['AvailabilityZone']] > 0 106 | 107 | 108 | def instances_sorting_func(instance): 109 | """Implements the instances sorting logic using CloudWatch metric data 110 | 111 | Keyword arguments: 112 | instance -- dictionary with instance data 113 | """ 114 | 115 | return instance['Metrics'][METRIC_NAME] 116 | 117 | 118 | def lambda_handler(event, context): 119 | # Generate a time window for retrieving CloudWatch metric data 120 | start_time, end_time, period = generate_time_window() 121 | 122 | print('Received instances: ', event['Instances']) 123 | 124 | # Build a dictionary with the form {AvailabilityZone: capacity} 125 | capacities = {capacity['AvailabilityZone']: capacity['Capacity'] for capacity in event['CapacityToTerminate']} 126 | instances = event['Instances'] 127 | instances_to_terminate = [] 128 | 129 | # Get CloudWatch metric data for every instance in the generated time window 130 | # This method will add a `Metrics` property to every dictionary in the instances variable 131 | get_metric_data(instances, start_time, end_time, period) 132 | 133 | # Sort the instances in ascending order by their metric values (idle instances first) 134 | instances.sort(key=instances_sorting_func) 135 | 136 | # Select instances for termination using capacity information and the retrieved CloudWatch metric data 137 | for instance in instances: 138 | if should_terminate_instance(instance, capacities): 139 | instances_to_terminate.append(instance['InstanceId']) 140 | capacities[instance['AvailabilityZone']] -= 1 141 | 142 | print('Selected instances: {}'.format(', '.join(instances_to_terminate))) 143 | 144 | return { 145 | 'InstanceIDs': instances_to_terminate 146 | } 147 | -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/metric_based_termination/assets/stress_document.yml: -------------------------------------------------------------------------------- 1 | schemaVersion: '2.2' 2 | description: Stresses an EC2 instance 3 | mainSteps: 4 | - name: Stress 5 | action: aws:runShellScript 6 | inputs: 7 | runCommand: 8 | - "#!/bin/bash" 9 | - sudo yum install -y stress-ng 10 | - rand="$(( $RANDOM % 15 + 3 ))m" 11 | - stress-ng --matrix 0 -t $rand --times -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/metric_based_termination/assets/user_data.txt: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | Content-Type: multipart/mixed; boundary="==MYBOUNDARY==" 3 | 4 | --==MYBOUNDARY== 5 | Content-Type: text/x-shellscript; charset="us-ascii" 6 | 7 | #!/bin/bash 8 | sudo yum install -y epel-release 9 | sudo yum install -y stress 10 | sudo yum install -y stress-ng 11 | 12 | --==MYBOUNDARY==-- -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/metric_based_termination/metric_based_termination_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | CfnParameter, 3 | Duration, 4 | Stack, 5 | aws_ec2 as ec2, 6 | aws_ssm as ssm, 7 | aws_lambda as _lambda, 8 | aws_iam as iam, 9 | aws_autoscaling as asg, 10 | Fn 11 | ) 12 | from constructs import Construct 13 | 14 | import yaml 15 | 16 | 17 | class MetricBasedTerminationStack(Stack): 18 | def _create_vpc(self): 19 | return ec2.Vpc(self, "Vpc", 20 | subnet_configuration=[ 21 | ec2.SubnetConfiguration( 22 | cidr_mask=24, 23 | name="StressSubnetConfiguration", 24 | subnet_type=ec2.SubnetType.PUBLIC 25 | ) 26 | ]) 27 | 28 | def _create_launch_template(self, vpc): 29 | # IAM role to associate with the instance profile that is used by instances 30 | instance_role = iam.Role(self, 'StressInstanceRole', 31 | assumed_by=iam.ServicePrincipal('ec2.amazonaws.com'), 32 | role_name='StressInstanceRole', 33 | managed_policies=[ 34 | iam.ManagedPolicy.from_managed_policy_arn( 35 | self, 36 | 'AmazonSSMManagedInstanceCore', 37 | 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore' 38 | ) 39 | ]) 40 | 41 | # Security group where the instances will be placed 42 | security_group = ec2.SecurityGroup(self, 'SecurityGroup', 43 | security_group_name="StressSecurityGroup", 44 | allow_all_outbound=True, 45 | vpc=vpc) 46 | 47 | # Read the UserData from the txt file 48 | with open('metric_based_termination/assets/user_data.txt') as fd: 49 | user_data = fd.read() 50 | 51 | return ec2.LaunchTemplate(self, 'LaunchTemplate', 52 | launch_template_name="StressLaunchTemplate", 53 | security_group=security_group, 54 | user_data=ec2.UserData.custom(user_data), 55 | role=instance_role, 56 | machine_image=ec2.MachineImage.latest_amazon_linux()) 57 | 58 | def _create_ssm_document(self): 59 | # Read the document data from the yaml file 60 | with open('metric_based_termination/assets/stress_document.yml') as fd: 61 | content = fd.read() 62 | 63 | return ssm.CfnDocument(self, "Document", 64 | name='StressDocument', 65 | document_type='Command', 66 | content=yaml.safe_load(content)) 67 | 68 | def _create_ssm_association(self, document, launch_template): 69 | ssm.CfnAssociation(self, 'DocumentAssociation', 70 | name=document.name, 71 | association_name='StressAssociation', 72 | targets=[ 73 | ssm.CfnAssociation.TargetProperty(key='tag:aws:ec2launchtemplate:id', 74 | values=[ 75 | launch_template.launch_template_id 76 | ]) 77 | ]) 78 | 79 | def _create_termination_function(self): 80 | # Create the Lambda function that implements the custom termination policy 81 | function = _lambda.Function(self, 'Function', 82 | runtime=_lambda.Runtime.PYTHON_3_9, 83 | function_name='customTerminationPolicy', 84 | code=_lambda.Code.from_asset('metric_based_termination/assets/func_termination_policy'), 85 | timeout=Duration.minutes(5), 86 | handler='index.lambda_handler', 87 | environment={'METRIC_NAME': 'CPUUtilization', 88 | 'METRIC_THRESHOLD': '3', 89 | 'METRIC_STAT': 'Minimum', 90 | 'METRIC_TIME_WINDOW_IN_MINUTES': '5'} 91 | ) 92 | 93 | # Grant the function permission to retrieve CloudWatch metrics 94 | function.add_to_role_policy(iam.PolicyStatement( 95 | effect=iam.Effect.ALLOW, 96 | resources=['*'], 97 | actions=['cloudwatch:GetMetricData', 98 | 'logs:CreateLogGroup', 99 | 'logs:CreateLogStream', 100 | 'logs:PutLogEvents'] 101 | )) 102 | 103 | # Grant the function permission to update the ASG 104 | function.add_to_role_policy(iam.PolicyStatement( 105 | effect=iam.Effect.ALLOW, 106 | actions=['autoscaling:UpdateAutoScalingGroup'], 107 | resources=['*'] 108 | )) 109 | 110 | # Grant the EC2 ASG service-linked role permission to execute the function 111 | principal = iam.ServicePrincipal(Fn.sub("arn:aws:iam::${AWS::AccountId}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling")) 112 | function.grant_invoke(principal) 113 | 114 | return function 115 | 116 | def _create_asg(self, launch_template, termination_function, vpc): 117 | # Define ASG configuration parameters 118 | asg_name = CfnParameter(self, "ASGName", type='String', description="ASG Name", default='Example ASG') 119 | min_capacity = CfnParameter(self, "MinCapacity", type='Number', description="Minimum capacity", default='1') 120 | max_capacity = CfnParameter(self, "MaxCapacity", type='Number', description="Maximum capacity", default='6') 121 | desired_cap = CfnParameter(self, "DesiredCapacity", type='Number', description="Desired capacity", default='2') 122 | 123 | # Override the configuration of the Launch Template using Attribute Based Selection (ABS) 124 | launch_template_property = asg.CfnAutoScalingGroup.LaunchTemplateProperty( 125 | launch_template_specification=asg.CfnAutoScalingGroup.LaunchTemplateSpecificationProperty( 126 | launch_template_id=launch_template.launch_template_id, 127 | version=launch_template.version_number 128 | ), 129 | overrides=[asg.CfnAutoScalingGroup.LaunchTemplateOverridesProperty( 130 | instance_requirements=asg.CfnAutoScalingGroup.InstanceRequirementsProperty( 131 | memory_mib=asg.CfnAutoScalingGroup.MemoryMiBRequestProperty( 132 | max=8192, 133 | min=0 134 | ), 135 | v_cpu_count=asg.CfnAutoScalingGroup.VCpuCountRequestProperty( 136 | max=6, 137 | min=1 138 | ) 139 | ) 140 | )] 141 | ) 142 | 143 | # Create the ASG 144 | l1_asg = asg.CfnAutoScalingGroup( 145 | self, 'ASG', 146 | auto_scaling_group_name=asg_name.value_as_string, 147 | max_size=max_capacity.value_as_string, 148 | min_size=min_capacity.value_as_string, 149 | desired_capacity=desired_cap.value_as_string, 150 | capacity_rebalance=True, 151 | termination_policies=[termination_function.function_arn], 152 | vpc_zone_identifier=vpc.select_subnets().subnet_ids, 153 | mixed_instances_policy=asg.CfnAutoScalingGroup.MixedInstancesPolicyProperty( 154 | launch_template=launch_template_property, 155 | instances_distribution=asg.CfnAutoScalingGroup.InstancesDistributionProperty( 156 | on_demand_allocation_strategy="lowest-price", 157 | spot_allocation_strategy="capacity-optimized" 158 | ) 159 | )) 160 | 161 | # Set a CPU based target tracking scaling policy for the ASG 162 | scaling_policy = asg.CfnScalingPolicy( 163 | self, 'ScalingPolicy', 164 | auto_scaling_group_name=l1_asg.auto_scaling_group_name, 165 | policy_type='TargetTrackingScaling', 166 | target_tracking_configuration=asg.CfnScalingPolicy.TargetTrackingConfigurationProperty( 167 | target_value=50, 168 | disable_scale_in=False, 169 | predefined_metric_specification=asg.CfnScalingPolicy.PredefinedMetricSpecificationProperty( 170 | predefined_metric_type='ASGAverageCPUUtilization' 171 | ) 172 | )) 173 | scaling_policy.node.add_dependency(l1_asg) 174 | 175 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 176 | super().__init__(scope, construct_id, **kwargs) 177 | 178 | # Create a VPC where new instances will be deployed and a Launch Template that uses it 179 | vpc = self._create_vpc() 180 | launch_template = self._create_launch_template(vpc) 181 | 182 | # Create the SSM document and associate it with the instances launched using the Launch Template 183 | document = self._create_ssm_document() 184 | self._create_ssm_association(document, launch_template) 185 | 186 | # Create the custom termination policy using a Lambda function and an ASG that uses it 187 | termination_function = self._create_termination_function() 188 | self._create_asg(launch_template, termination_function, vpc) 189 | -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.37.1 2 | constructs>=10.0.0,<11.0.0 3 | PyYAML~=6.0 4 | -------------------------------------------------------------------------------- /features/custom-termination-policies/metric-based-termination/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /features/custom-termination-policies/quick-start-example/README.md: -------------------------------------------------------------------------------- 1 | # Custom Termination Policy Example: Quick Start Example 2 | 3 | This example template demonstrates how to create a [custom termination policy](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lambda-custom-termination-policy.html) for an EC2 Auto Scaling group. This example stack will create an Auto Scaling group in a new VPC configured with a custom termination policy using an AWS Lambda function that Amazon EC2 Auto Scaling invokes in response to certain events. The Lambda function processes the information in the input data sent by Amazon EC2 Auto Scaling and returns a list of instances that are ready to terminate. This example returns all instances as candidates for termination, and can be customized as needed. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 8 | 9 | ### Prerequisites 10 | 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 12 | 13 | ## Deployment Steps 14 | 15 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 16 | 17 | 1. Change directories to this example. 18 | 19 | ```bash 20 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/custom-termination-policies/quick-start-example 21 | ``` 22 | 23 | 2. Deploy the CloudFormation Stack. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 24 | 25 | ```bash 26 | aws cloudformation deploy \ 27 | --template-file template.yaml \ 28 | --stack-name custom-termination-policy-example \ 29 | --capabilities CAPABILITY_IAM \ 30 | --parameter-overrides \ 31 | InstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 32 | ``` 33 | 34 | ## Clean Up 35 | 36 | Delete the CloudFormation Stack 37 | 38 | ```bash 39 | aws cloudformation delete-stack --stack-name custom-termination-policy-example 40 | ``` -------------------------------------------------------------------------------- /features/custom-termination-policies/quick-start-example/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | 18 | Description: > 19 | amazon-ec2-autoscaling-custom-termination-policy-quick-start-example 20 | 21 | Sample CloudFormation template that deploys an Auto Scaling Group with a custom termination policy that can be 22 | customized based on your needs. 23 | 24 | Parameters: 25 | AmiId: 26 | Description: AMI Id 27 | Type: 'AWS::SSM::Parameter::Value' 28 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 29 | AutoScalingGroupName: 30 | Description: Auto Scaling Group Name 31 | Type: String 32 | Default: Example Auto Scaling Group 33 | AutoScalingGroupMinSize: 34 | Description: Minimum Size 35 | Type: Number 36 | Default: 0 37 | AutoScalingGroupMaxSize: 38 | Description: Maximum Size 39 | Type: Number 40 | Default: 10 41 | AutoScalingGroupDesiredCapacity: 42 | Description: Desired Capacity 43 | Type: Number 44 | Default: 6 45 | InstanceKeyPair: 46 | Description: Amazon EC2 Key Pair 47 | Type: "AWS::EC2::KeyPair::KeyName" 48 | VpcCIDR: 49 | Description: Please enter the IP range (CIDR notation) for this VPC 50 | Type: String 51 | Default: 10.192.0.0/16 52 | AvailabilityZone1CIDR: 53 | Description: Please enter the IP range (CIDR notation) for the app subnet in the first Availability Zone 54 | Type: String 55 | Default: 10.192.10.0/24 56 | AvailabilityZone2CIDR: 57 | Description: Please enter the IP range (CIDR notation) for the app subnet in the second Availability Zone 58 | Type: String 59 | Default: 10.192.11.0/24 60 | AvailabilityZone3CIDR: 61 | Description: Please enter the IP range (CIDR notation) for the app subnet in the third Availability Zone 62 | Type: String 63 | Default: 10.192.12.0/24 64 | 65 | Metadata: 66 | AWS::CloudFormation::Interface: 67 | ParameterGroups: 68 | - 69 | Label: 70 | default: "VPC Configuration" 71 | Parameters: 72 | - VpcCIDR 73 | - AvailabilityZone1CIDR 74 | - AvailabilityZone2CIDR 75 | - AvailabilityZone3CIDR 76 | - 77 | Label: 78 | default: "Instance Configuration" 79 | Parameters: 80 | - AmiId 81 | - InstanceKeyPair 82 | - 83 | Label: 84 | default: "Auto Scaling Group Configuration" 85 | Parameters: 86 | - AutoScalingGroupName 87 | - AutoScalingGroupVpcID 88 | - AutoScalingGroupSubnetIDs 89 | - AutoScalingGroupMinSize 90 | - AutoScalingGroupMaxSize 91 | - AutoScalingGroupDesiredCapacity 92 | 93 | Resources: 94 | 95 | # VPC/Networking Resources 96 | 97 | VPC: 98 | Type: AWS::EC2::VPC 99 | Properties: 100 | CidrBlock: !Ref VpcCIDR 101 | EnableDnsHostnames: true 102 | 103 | InternetGateway: 104 | Type: AWS::EC2::InternetGateway 105 | 106 | InternetGatewayAttachment: 107 | Type: AWS::EC2::VPCGatewayAttachment 108 | Properties: 109 | InternetGatewayId: !Ref InternetGateway 110 | VpcId: !Ref VPC 111 | 112 | AvailabilityZoneSubnet1: 113 | Type: AWS::EC2::Subnet 114 | Properties: 115 | VpcId: !Ref VPC 116 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 117 | CidrBlock: !Ref AvailabilityZone1CIDR 118 | MapPublicIpOnLaunch: true 119 | 120 | AvailabilityZoneSubnet2: 121 | Type: AWS::EC2::Subnet 122 | Properties: 123 | VpcId: !Ref VPC 124 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 125 | CidrBlock: !Ref AvailabilityZone2CIDR 126 | MapPublicIpOnLaunch: true 127 | 128 | AvailabilityZoneSubnet3: 129 | Type: AWS::EC2::Subnet 130 | Properties: 131 | VpcId: !Ref VPC 132 | AvailabilityZone: !Select [ 2, !GetAZs '' ] 133 | CidrBlock: !Ref AvailabilityZone3CIDR 134 | MapPublicIpOnLaunch: true 135 | 136 | InstanceSecurityGroup: 137 | Type: AWS::EC2::SecurityGroup 138 | Properties: 139 | VpcId: !Ref VPC 140 | GroupDescription: Instance Security Group 141 | 142 | RouteTable: 143 | Type: AWS::EC2::RouteTable 144 | Properties: 145 | VpcId: !Ref VPC 146 | 147 | DefaultPublicRoute: 148 | Type: AWS::EC2::Route 149 | DependsOn: InternetGatewayAttachment 150 | Properties: 151 | RouteTableId: !Ref RouteTable 152 | DestinationCidrBlock: 0.0.0.0/0 153 | GatewayId: !Ref InternetGateway 154 | 155 | AvailabilityZoneSubnet1RouteTableAssociation: 156 | Type: AWS::EC2::SubnetRouteTableAssociation 157 | Properties: 158 | RouteTableId: !Ref RouteTable 159 | SubnetId: !Ref AvailabilityZoneSubnet1 160 | 161 | AvailabilityZoneSubnet2RouteTableAssociation: 162 | Type: AWS::EC2::SubnetRouteTableAssociation 163 | Properties: 164 | RouteTableId: !Ref RouteTable 165 | SubnetId: !Ref AvailabilityZoneSubnet2 166 | 167 | AvailabilityZoneSubnet3RouteTableAssociation: 168 | Type: AWS::EC2::SubnetRouteTableAssociation 169 | Properties: 170 | RouteTableId: !Ref RouteTable 171 | SubnetId: !Ref AvailabilityZoneSubnet3 172 | 173 | # Instance/Auto Scaling Group Resources 174 | 175 | LaunchTemplate: 176 | Type: AWS::EC2::LaunchTemplate 177 | Properties: 178 | LaunchTemplateData: 179 | ImageId: !Ref AmiId 180 | KeyName: !Ref InstanceKeyPair 181 | SecurityGroupIds: 182 | - !Ref InstanceSecurityGroup 183 | IamInstanceProfile: 184 | Arn: !GetAtt 185 | - InstanceProfile 186 | - Arn 187 | TagSpecifications: 188 | - ResourceType: instance 189 | Tags: 190 | - Key: Name 191 | Value: CustomTerminationExampleInstance 192 | UserData: 193 | 'Fn::Base64': 194 | !Sub 195 | - |- 196 | Content-Type: multipart/mixed; boundary="//" 197 | MIME-Version: 1.0 198 | 199 | --// 200 | Content-Type: text/cloud-config; charset="us-ascii" 201 | MIME-Version: 1.0 202 | Content-Transfer-Encoding: 7bit 203 | Content-Disposition: attachment; filename="cloud-config.txt" 204 | 205 | #cloud-config 206 | cloud_final_modules: 207 | - [scripts-user, always] 208 | 209 | --// 210 | Content-Type: text/x-shellscript; charset="us-ascii" 211 | MIME-Version: 1.0 212 | Content-Transfer-Encoding: 7bit 213 | Content-Disposition: attachment; filename="userdata.txt" 214 | 215 | #!/bin/bash 216 | rpm -q stress &> /dev/null 217 | if [ $? -ne 0 ] 218 | then 219 | sudo amazon-linux-extras install epel -y 220 | sudo yum install stress -y 221 | fi 222 | --// 223 | - { 224 | autoScalingGroupName: !Ref AutoScalingGroupName 225 | } 226 | 227 | InstanceRole: 228 | Type: "AWS::IAM::Role" 229 | Properties: 230 | AssumeRolePolicyDocument: 231 | Version: "2012-10-17" 232 | Statement: 233 | - 234 | Effect: "Allow" 235 | Principal: 236 | Service: 237 | - "ec2.amazonaws.com" 238 | Action: 239 | - "sts:AssumeRole" 240 | Path: "/" 241 | ManagedPolicyArns: 242 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 243 | 244 | InstanceProfile: 245 | Type: "AWS::IAM::InstanceProfile" 246 | Properties: 247 | Path: "/" 248 | Roles: 249 | - 250 | Ref: "InstanceRole" 251 | 252 | AutoScalingGroup: 253 | Type: AWS::AutoScaling::AutoScalingGroup 254 | Properties: 255 | AutoScalingGroupName: !Ref AutoScalingGroupName 256 | DesiredCapacity: !Ref AutoScalingGroupDesiredCapacity 257 | MaxSize: !Ref AutoScalingGroupMaxSize 258 | MinSize: !Ref AutoScalingGroupMinSize 259 | MixedInstancesPolicy: 260 | InstancesDistribution: 261 | OnDemandAllocationStrategy: prioritized 262 | OnDemandBaseCapacity: 2 263 | OnDemandPercentageAboveBaseCapacity: 0 264 | SpotAllocationStrategy: capacity-optimized 265 | LaunchTemplate: 266 | LaunchTemplateSpecification: 267 | LaunchTemplateId: !Ref LaunchTemplate 268 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 269 | Overrides: 270 | - InstanceType: c3.large 271 | - InstanceType: c4.large 272 | - InstanceType: c5.large 273 | - InstanceType: m3.large 274 | - InstanceType: m4.large 275 | - InstanceType: m5.large 276 | - InstanceType: r3.large 277 | - InstanceType: r4.large 278 | - InstanceType: r5.large 279 | VPCZoneIdentifier: 280 | - !Ref AvailabilityZoneSubnet1 281 | - !Ref AvailabilityZoneSubnet2 282 | - !Ref AvailabilityZoneSubnet3 283 | 284 | CustomTerminationPolicyFunctionInvokePermission: 285 | Type: AWS::Lambda::Permission 286 | Properties: 287 | FunctionName: !GetAtt [ CustomTerminationPolicyFunction, Arn ] 288 | Action: lambda:InvokeFunction 289 | Principal: !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling" 290 | 291 | CustomTerminationPolicyFunctionRole: 292 | Type: "AWS::IAM::Role" 293 | Properties: 294 | AssumeRolePolicyDocument: 295 | Version: "2012-10-17" 296 | Statement: 297 | - Effect: Allow 298 | Principal: 299 | Service: lambda.amazonaws.com 300 | Action: "sts:AssumeRole" 301 | Policies: 302 | - PolicyName: CustomTerminationPolicyFunctionLogsPolicy 303 | PolicyDocument: 304 | Version: "2012-10-17" 305 | Statement: 306 | - Effect: Allow 307 | Action: 308 | - "logs:CreateLogGroup" 309 | - "logs:CreateLogStream" 310 | - "logs:PutLogEvents" 311 | Resource: "*" 312 | 313 | CustomTerminationPolicyFunction: 314 | Type: AWS::Lambda::Function 315 | DependsOn: AutoScalingGroup 316 | Properties: 317 | Handler: index.lambda_handler 318 | Role: !GetAtt [ CustomTerminationPolicyFunctionRole, Arn ] 319 | Runtime: python3.8 320 | Timeout: 600 321 | Code: 322 | ZipFile: | 323 | import boto3 324 | import os 325 | import json 326 | import logging 327 | import time 328 | 329 | from botocore.exceptions import ClientError 330 | 331 | logger = logging.getLogger() 332 | logger.setLevel(logging.INFO) 333 | 334 | cloudwatch = boto3.client('cloudwatch') 335 | 336 | def determine_instances_to_terminate(instances): 337 | instances_to_terminate = [] 338 | 339 | # TODO: Add Custom Logic to Select Instances 340 | for instance in instances: 341 | instances_to_terminate.append(instance['InstanceId']) 342 | 343 | return instances_to_terminate 344 | 345 | def lambda_handler(event, context): 346 | 347 | logger.info(event) 348 | 349 | instances = determine_instances_to_terminate(event['Instances']) 350 | 351 | logger.info(instances) 352 | 353 | response = { 354 | 'InstanceIDs': instances 355 | } 356 | 357 | logger.info(response) 358 | 359 | return response 360 | Description: Returns instances to terminate during scale-in events. 361 | 362 | CustomTerminationPolicyResourceFunctionRole: 363 | Type: "AWS::IAM::Role" 364 | Properties: 365 | AssumeRolePolicyDocument: 366 | Version: "2012-10-17" 367 | Statement: 368 | - Effect: Allow 369 | Principal: 370 | Service: lambda.amazonaws.com 371 | Action: "sts:AssumeRole" 372 | Policies: 373 | - PolicyName: CustomTerminationPolicyResourceFunctionLogsPolicy 374 | PolicyDocument: 375 | Version: "2012-10-17" 376 | Statement: 377 | - Effect: Allow 378 | Action: 379 | - "logs:CreateLogGroup" 380 | - "logs:CreateLogStream" 381 | - "logs:PutLogEvents" 382 | Resource: "*" 383 | - PolicyName: CustomTerminationPolicyResourceFunctionUpdateAutoScalingGroupPolicy 384 | PolicyDocument: 385 | Version: "2012-10-17" 386 | Statement: 387 | - Effect: Allow 388 | Action: 389 | - "autoscaling:UpdateAutoScalingGroup" 390 | Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AutoScalingGroupName}" 391 | 392 | CustomTerminationPolicyResource: 393 | Type: 'Custom::CustomTerminationPolicyResourceFunction' 394 | DependsOn: AutoScalingGroup 395 | Properties: 396 | ServiceToken: !GetAtt [ CustomTerminationPolicyResourceFunction, Arn ] 397 | 398 | CustomTerminationPolicyResourceFunction: 399 | Type: AWS::Lambda::Function 400 | Properties: 401 | Handler: index.lambda_handler 402 | Role: !GetAtt [ CustomTerminationPolicyResourceFunctionRole, Arn ] 403 | Runtime: python3.8 404 | Timeout: 600 405 | Environment: 406 | Variables: 407 | AUTOSCALING_GROUP_NAME: !Ref AutoScalingGroup 408 | CUSTOM_TERMINATION_POLICY_LAMBDA_ARN: !GetAtt [ CustomTerminationPolicyFunction, Arn ] 409 | Code: 410 | ZipFile: | 411 | import boto3 412 | import os 413 | import logging 414 | import cfnresponse 415 | 416 | from botocore.exceptions import ClientError 417 | 418 | logger = logging.getLogger() 419 | logger.setLevel(logging.INFO) 420 | 421 | autoscaling = boto3.client('autoscaling') 422 | 423 | autoscaling_group_name = os.environ['AUTOSCALING_GROUP_NAME'] 424 | custom_termination_policy_lambda_arn = os.environ['CUSTOM_TERMINATION_POLICY_LAMBDA_ARN'] 425 | 426 | def lambda_handler(event, context): 427 | 428 | logger.info(event) 429 | 430 | if event['RequestType'] == 'Create': 431 | 432 | try: 433 | 434 | response = autoscaling.update_auto_scaling_group( 435 | AutoScalingGroupName=autoscaling_group_name, 436 | TerminationPolicies=[ 437 | custom_termination_policy_lambda_arn, 438 | ], 439 | ) 440 | 441 | logger.info(response) 442 | cfnresponse.send(event, context, cfnresponse.SUCCESS, response, "ResponseMetadata") 443 | 444 | except ClientError as e: 445 | message = 'Error creating custom termination policy: {}'.format(e) 446 | logger.info(message) 447 | cfnresponse.send(event, context, cfnresponse.FAILED, message, "ErrorMessage") 448 | raise Exception(message) 449 | 450 | if event['RequestType'] == 'Delete': 451 | 452 | try: 453 | 454 | response = autoscaling.update_auto_scaling_group( 455 | AutoScalingGroupName=autoscaling_group_name, 456 | TerminationPolicies=[ 457 | 'Default' 458 | ], 459 | ) 460 | 461 | logger.info(response) 462 | cfnresponse.send(event, context, cfnresponse.SUCCESS, response, "ResponseMetadata") 463 | 464 | except ClientError as e: 465 | message = 'Error deleting custom termination policy: {}'.format(e) 466 | logger.info(message) 467 | cfnresponse.send(event, context, cfnresponse.FAILED, message, "ErrorMessage") 468 | raise Exception(message) 469 | 470 | else: 471 | cfnresponse.send(event, context, cfnresponse.SUCCESS, event, "ResponseMetadata") 472 | 473 | return 474 | Description: Creates a custom termination policy during stack creation. 475 | -------------------------------------------------------------------------------- /features/faster-target-tracking/FasterScalingCFN.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: This template deploys a demo of Faster Scaling with EC2 Auto Scaling (SOXXXX) 3 | Parameters: 4 | EnvironmentName: 5 | Description: Prefix name to be applied to resources created by this Demo 6 | Type: String 7 | Default: FasterScalingDemo 8 | AMI: 9 | Description: AMI to be used, Defaulting to Graviton-based AL2023 (default us-east-1) 10 | Type: String 11 | Default: ami-02801556a781a4499 12 | 13 | VpcCIDR: 14 | Description: VPC CIDR Block to be Used 15 | Type: String 16 | Default: 10.0.0.0/16 17 | EC2InstanceConnectCIDR: 18 | Description: CIDR Block for EC2 Instance Connect feature on the chosen region 19 | (default us-east-1) 20 | Type: String 21 | Default: 18.206.107.24/29 22 | Resources: 23 | VPC: 24 | Type: AWS::EC2::VPC 25 | Properties: 26 | CidrBlock: !Ref VpcCIDR 27 | EnableDnsSupport: true 28 | EnableDnsHostnames: true 29 | Tags: 30 | - Key: Name 31 | Value: !Sub ${EnvironmentName}_VPC 32 | InternetGateway: 33 | Type: AWS::EC2::InternetGateway 34 | Properties: 35 | Tags: 36 | - Key: Name 37 | Value: !Sub ${EnvironmentName}_IGW 38 | InternetGatewayAttachment: 39 | Type: AWS::EC2::VPCGatewayAttachment 40 | Properties: 41 | InternetGatewayId: !Ref InternetGateway 42 | VpcId: !Ref VPC 43 | PublicRouteTable: 44 | Type: AWS::EC2::RouteTable 45 | Properties: 46 | VpcId: !Ref VPC 47 | Tags: 48 | - Key: Name 49 | Value: !Sub ${EnvironmentName}_RTB 50 | DefaultPublicRoute: 51 | Type: AWS::EC2::Route 52 | DependsOn: InternetGatewayAttachment 53 | Properties: 54 | RouteTableId: !Ref PublicRouteTable 55 | DestinationCidrBlock: 0.0.0.0/0 56 | GatewayId: !Ref InternetGateway 57 | PublicSubnet1RouteTableAssociation: 58 | Type: AWS::EC2::SubnetRouteTableAssociation 59 | Properties: 60 | RouteTableId: !Ref PublicRouteTable 61 | SubnetId: !Ref PublicSubnet1 62 | 63 | PublicSubnet2RouteTableAssociation: 64 | Type: AWS::EC2::SubnetRouteTableAssociation 65 | Properties: 66 | RouteTableId: !Ref PublicRouteTable 67 | SubnetId: !Ref PublicSubnet2 68 | PublicSubnet3RouteTableAssociation: 69 | Type: AWS::EC2::SubnetRouteTableAssociation 70 | Properties: 71 | RouteTableId: !Ref PublicRouteTable 72 | SubnetId: !Ref PublicSubnet3 73 | PublicSubnet1: 74 | Type: AWS::EC2::Subnet 75 | Properties: 76 | VpcId: !Ref VPC 77 | AvailabilityZone: !Select 78 | - 0 79 | - !GetAZs '' 80 | MapPublicIpOnLaunch: true 81 | CidrBlock: !Select 82 | - 0 83 | - !Cidr 84 | - !GetAtt VPC.CidrBlock 85 | - 3 86 | - 8 87 | Tags: 88 | - Key: Name 89 | Value: !Sub ${EnvironmentName}_PubSubnet1 90 | PublicSubnet2: 91 | Type: AWS::EC2::Subnet 92 | Properties: 93 | VpcId: !Ref VPC 94 | AvailabilityZone: !Select 95 | - 1 96 | - !GetAZs '' 97 | MapPublicIpOnLaunch: true 98 | CidrBlock: !Select 99 | - 1 100 | - !Cidr 101 | - !GetAtt VPC.CidrBlock 102 | - 3 103 | - 8 104 | Tags: 105 | - Key: Name 106 | Value: !Sub ${EnvironmentName}_PubSubnet2 107 | PublicSubnet3: 108 | Type: AWS::EC2::Subnet 109 | Properties: 110 | VpcId: !Ref VPC 111 | AvailabilityZone: !Select 112 | - 2 113 | - !GetAZs '' 114 | MapPublicIpOnLaunch: true 115 | CidrBlock: !Select 116 | - 2 117 | - !Cidr 118 | - !GetAtt VPC.CidrBlock 119 | - 3 120 | - 8 121 | Tags: 122 | - Key: Name 123 | Value: !Sub ${EnvironmentName}_PubSubnet3 124 | InstanceSecurityGroup: 125 | Type: AWS::EC2::SecurityGroup 126 | Properties: 127 | GroupDescription: Security Group for Faster Scaling demo 128 | VpcId: !Ref VPC 129 | SecurityGroupIngress: 130 | - IpProtocol: tcp 131 | FromPort: 22 132 | ToPort: 22 133 | CidrIp: !Ref EC2InstanceConnectCIDR 134 | Description: Allows to connect to the EC2 instances through EC2 Instance Connect 135 | SecurityGroupEgress: 136 | - IpProtocol: tcp 137 | FromPort: 0 138 | ToPort: 65535 139 | CidrIp: 0.0.0.0/0 140 | Description: Allows EC2 Instance to retrieve software updates and access AWS 141 | Resources 142 | Tags: 143 | - Key: Name 144 | Value: !Sub ${EnvironmentName}_InstanceSG 145 | 146 | LaunchTemplate: 147 | Type: AWS::EC2::LaunchTemplate 148 | Properties: 149 | LaunchTemplateName: !Sub ${EnvironmentName}_Instances 150 | LaunchTemplateData: 151 | DisableApiTermination: true 152 | ImageId: !Ref AMI 153 | SecurityGroupIds: 154 | - !GetAtt InstanceSecurityGroup.GroupId 155 | IamInstanceProfile: 156 | Arn: !GetAtt InstanceProfile.Arn 157 | Monitoring: 158 | Enabled: true 159 | MetadataOptions: 160 | HttpTokens: required 161 | UserData: !Base64 162 | Fn::Sub: | 163 | #!/bin/bash 164 | yum install amazon-cloudwatch-agent -y 165 | sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c ssm:/cw-agent-asg-aggregate-cpu -s 166 | TagSpecifications: 167 | - ResourceType: instance 168 | Tags: 169 | - Key: Name 170 | Value: !Sub ${EnvironmentName}_EC2 171 | 172 | IAMInstanceRole: 173 | Type: AWS::IAM::Role 174 | Properties: 175 | AssumeRolePolicyDocument: 176 | Version: '2012-10-17' 177 | Statement: 178 | - Effect: Allow 179 | Principal: 180 | Service: 181 | - ec2.amazonaws.com 182 | Action: 183 | - sts:AssumeRole 184 | Path: / 185 | Policies: 186 | - PolicyName: FasterScalingDemo 187 | PolicyDocument: 188 | Version: '2012-10-17' 189 | Statement: 190 | - Effect: Allow 191 | Action: 192 | - cloudwatch:PutMetricData 193 | - ec2:DescribeTags 194 | - ssm:GetParameter 195 | Resource: '*' 196 | Tags: 197 | - Key: Name 198 | Value: !Sub ${EnvironmentName}_IAMROLE 199 | 200 | InstanceProfile: 201 | 202 | Type: AWS::IAM::InstanceProfile 203 | Properties: 204 | Path: / 205 | Roles: 206 | - !Ref IAMInstanceRole 207 | 208 | AutoScalingGroup: 209 | Type: AWS::AutoScaling::AutoScalingGroup 210 | Properties: 211 | DesiredCapacity: '1' 212 | MaxSize: '10' 213 | MinSize: '0' 214 | VPCZoneIdentifier: 215 | - !Ref PublicSubnet1 216 | - !Ref PublicSubnet2 217 | - !Ref PublicSubnet3 218 | MixedInstancesPolicy: 219 | LaunchTemplate: 220 | LaunchTemplateSpecification: 221 | LaunchTemplateId: !Ref LaunchTemplate 222 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 223 | Overrides: 224 | - InstanceRequirements: 225 | VCpuCount: 226 | Min: 2 227 | Max: 4 228 | MemoryMiB: 229 | Min: 8192 230 | CpuManufacturers: 231 | - amazon-web-services 232 | Tags: 233 | - Key: Name 234 | Value: !Sub ${EnvironmentName}_ASG 235 | PropagateAtLaunch: false 236 | 237 | FasterScalingPolicy: 238 | Type: AWS::AutoScaling::ScalingPolicy 239 | Properties: 240 | AutoScalingGroupName: !Ref AutoScalingGroup 241 | PolicyType: TargetTrackingScaling 242 | EstimatedInstanceWarmup: 10 243 | TargetTrackingConfiguration: 244 | CustomizedMetricSpecification: 245 | MetricName: CPUUtilization 246 | Namespace: FasterScalingDemo 247 | Dimensions: 248 | - Name: AutoScalingGroupName 249 | Value: !Ref AutoScalingGroup 250 | Statistic: Average 251 | Unit: Percent 252 | Period: 10 253 | TargetValue: 60 254 | 255 | CloudWatchMetricsSSMParameter: 256 | Type: AWS::SSM::Parameter 257 | Properties: 258 | Name: cw-agent-asg-aggregate-cpu 259 | Type: String 260 | Value: '{"agent":{"metrics_collection_interval":10,"run_as_user":"cwagent"},"metrics":{"force_flush_interval":10,"aggregation_dimensions":[["AutoScalingGroupName"]],"append_dimensions":{"AutoScalingGroupName":"${aws:AutoScalingGroupName}"},"namespace":"FasterScalingDemo","metrics_collected":{"cpu":{"drop_original_metrics":["cpu_usage_active"],"measurement":[{"name":"cpu_usage_active","rename":"CPUUtilization"}]}}}}' 261 | Tier: Intelligent-Tiering 262 | Description: Custom metric specification for CloudWatch Agent -------------------------------------------------------------------------------- /features/faster-target-tracking/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /features/faster-target-tracking/README.md: -------------------------------------------------------------------------------- 1 | # Faster Scaling with EC2 Auto Scaling 2 | 3 | This repository contains a sample AWS CloudFormation template to demonstrate how to scale with sub-minute alarms using EC2 Auto Scaling target tracking scaling policies in its own Virtual Private Cloud (VPC) consisting of: 4 | 5 | * An AWS Systems Manager (SSM) parameter holding a Unified Cloudwatch (CW) Agent configuration for reporting a custom CPUUtilization metric at a 10 seconds interval to a Cloudwatch FasterScalingDemo namespace 6 | * An IAM Role and corresponding IAM Instance Profile with permissions to put CW metrics and retrieve SSM parameters 7 | * A Security Group (SG) with rules allowing for ingress of TCP traffic on port 443 from the VPC's Cidr and on port 22 from the EC2 Instance Connect rage for the region. 8 | * An EC2 Launch Template (LT) hosting the base configuration for the EC2 instance 9 | * An EC2 Auto Scaling group using Graviton-based EC2 instances running Amazon Linux 2003 10 | * A Target Tracking scaling policy set to scale on the custom CPUUtilization metric 11 | 12 | ## Adapting to your environment: 13 | Consider if any of the following adaptations need to be made to the template before using in your environment: 14 | 15 | * Update the launch template for your use case/application (e.g. using your custom AMI, or adapting the UserData script to install different software). 16 | * Add a Load Balancer to the EC2 Auto Scaling group. 17 | * Use a different metric that's meaningful to your application. Keeping in mind the considerations for [metrics that work with Target Tracking](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-scaling-target-tracking.html#target-tracking-considerations). 18 | * Adapt the IAM Role's permissions and Security Group rules according to your organization's requirements. 19 | 20 | 21 | ## Using this Sample 22 | ### Deploying 23 | * Navigate to the [Cloud Formation Console](https://us-east-1.console.aws.amazon.com/cloudformation/) in the us-east-1 (N. Virginia) region. 24 | * Click the Create Stack button and select "With existing resources (import resources)" and follow the steps to upload the the `FasterScalingCFN.yml` template. 25 | * When specifying Stack details, give the stack a name such as `FasterScalingDemo` and adapt any entries as needed. 26 | * Be sure to perform this operation with an IAM User that has permissions 27 | 28 | ### Deletening/Cleaning-up 29 | * Navigate to the [Cloud Formation Console](https://us-east-1.console.aws.amazon.com/cloudformation/) in the us-east-1 (N. Virginia) region. 30 | * Select the Stack you deployed on the previous step and hit the "Delete" button. 31 | 32 | 33 | ## Notes 34 | - This sample can be deployed on other AWS Regions, make sure to adapt the stack details accordingly, like AMI and the ranges for EC2 Instance Connect. 35 | - This sample deploys resources in a public subnet with public IP addresses to allow them to update packages from online repositories. They are not accessible externally by default. If you need to connect to the instances make sure to use [EC2 Instance Connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-methods.html#ec2-instance-connect-connecting-console). 36 | - This sample creates resources that incur charges to your AWS Account. Be sure to clean-up and delete the resources when not in use. 37 | 38 | --- 39 | For more information, see [Faster Scaling with EC2 Auto Scaling](https://aws.amazon.com/blogs/compute/faster-scaling-with-amazon-ec2-auto-scaling-target-tracking/). -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-linux/README.md: -------------------------------------------------------------------------------- 1 | # Auto Scaling Group Lifecycle Hooks Example - Lambda Managed Linux 2 | 3 | This example solution deploys an Auto Scaling group within a VPC. A lifecycle hook is enabled for the Auto Scaling group and a Lambda Function invokes in response to Lifecycle Action Events. The Lambda function uses AWS Systems Manager to install a sample application onto instances in the Auto Scaling group as they are Launched. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 8 | 9 | ### Prerequisites 10 | 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 12 | * [Python 3 installed](https://www.python.org/downloads/) 13 | * [Docker installed](https://www.docker.com/community-edition) 14 | * [SAM CLI installed](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 15 | 16 | ## Deployment Steps 17 | 18 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 19 | 20 | 1. Create a `S3 bucket` where we can upload our packaged Lambda functions for deployment. 21 | 22 | ```bash 23 | aws s3 mb s3://BUCKET_NAME 24 | ``` 25 | 26 | 2. Change directories to this example. 27 | 28 | ```bash 29 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/lifecycle-hooks/lambda-managed-linux 30 | ``` 31 | 32 | 3. Build the solution. 33 | 34 | ```bash 35 | sam build --use-container 36 | ``` 37 | 38 | 5. Package the solution. You will need to replace `REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME` with the name of the S3 bucket created in the first step. 39 | 40 | ```bash 41 | sam package \ 42 | --output-template-file packaged.yaml \ 43 | --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME 44 | ``` 45 | 46 | 6. Deploy the solution. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 47 | 48 | ```bash 49 | sam deploy \ 50 | --template-file packaged.yaml \ 51 | --stack-name lifecycle-hook-example \ 52 | --capabilities CAPABILITY_IAM \ 53 | --parameter-overrides \ 54 | InstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 55 | ``` 56 | 57 | ## Clean Up 58 | 59 | 1. Delete the stack. 60 | 61 | ```bash 62 | aws cloudformation delete-stack --stack-name lifecycle-hook-example 63 | ``` -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-linux/source/LifecycleFunction/app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | import boto3 17 | import os 18 | import json 19 | import logging 20 | import time 21 | 22 | from botocore.exceptions import ClientError 23 | 24 | logger = logging.getLogger() 25 | logger.setLevel(logging.INFO) 26 | 27 | autoscaling = boto3.client('autoscaling') 28 | ssm = boto3.client('ssm') 29 | 30 | def send_lifecycle_action(event, result): 31 | try: 32 | response = autoscaling.complete_lifecycle_action( 33 | LifecycleHookName=event['detail']['LifecycleHookName'], 34 | AutoScalingGroupName=event['detail']['AutoScalingGroupName'], 35 | LifecycleActionToken=event['detail']['LifecycleActionToken'], 36 | LifecycleActionResult=result, 37 | InstanceId=event['detail']['EC2InstanceId'] 38 | ) 39 | 40 | logger.info(response) 41 | except ClientError as e: 42 | message = 'Error completing lifecycle action: {}'.format(e) 43 | logger.error(message) 44 | raise Exception(message) 45 | 46 | return 47 | 48 | def run_command(event, command): 49 | 50 | # Run Command 51 | logger.info('Calling SendCommand: {} for instance: {}'.format(command, event['detail']['EC2InstanceId'])) 52 | attempt = 0 53 | while attempt < 10: 54 | attempt = attempt + 1 55 | try: 56 | time.sleep(5 * attempt) 57 | logger.info('SendCommand, attempt #: {}'.format(attempt)) 58 | response = ssm.send_command( 59 | InstanceIds=[event['detail']['EC2InstanceId']], 60 | DocumentName='AWS-RunShellScript', 61 | Parameters={ 62 | 'commands': [ 63 | command 64 | ] 65 | } 66 | ) 67 | 68 | logger.info(response) 69 | if 'Command' in response: 70 | break 71 | 72 | if attempt == 10: 73 | message = 'Command did not execute succesfully in time allowed.' 74 | raise Exception(message) 75 | 76 | except ClientError as e: 77 | message = 'Error calling SendCommand: {}'.format(e) 78 | logger.error(message) 79 | continue 80 | 81 | # Check Command Status 82 | command_id = response['Command']['CommandId'] 83 | logger.info('Calling GetCommandInvocation for command: {} for instance: {}'.format(command_id, event['detail']['EC2InstanceId'])) 84 | attempt = 0 85 | 86 | while attempt < 20: 87 | attempt = attempt + 1 88 | try: 89 | time.sleep(5 * attempt) 90 | logger.info('GetCommandInvocation, attempt #: {}'.format(attempt)) 91 | result = ssm.get_command_invocation( 92 | CommandId=command_id, 93 | InstanceId=event['detail']['EC2InstanceId'], 94 | ) 95 | if result['Status'] == 'InProgress': 96 | logger.info('Command is running.') 97 | continue 98 | elif result['Status'] == 'Success': 99 | logger.info('Command completed successfully: {}'.format(result['StandardOutputContent'])) 100 | break 101 | elif result['Status'] == 'Failed': 102 | message = 'Command did not execute successfully: {}'.format(e) 103 | logger.error(message) 104 | raise Exception(message) 105 | else: 106 | message = 'Command has an unhandled status, will continue: {}'.format(e) 107 | logger.warning(message) 108 | continue 109 | except ssm.exceptions.InvocationDoesNotExist as e: 110 | message = 'Error calling GetCommandInvocation: {}'.format(e) 111 | logger.error(message) 112 | raise Exception(message) 113 | 114 | if result['Status'] == 'Success': 115 | return 116 | else: 117 | message = 'Command did not execute succesfully in time allowed.' 118 | raise Exception(message) 119 | 120 | def lambda_handler(event, context): 121 | 122 | logger.info(event) 123 | 124 | # If Instance is Launching into AutoScalingGroup 125 | if event['detail']['Origin'] == 'EC2' and event['detail']['Destination'] == 'AutoScalingGroup': 126 | logger.info('Instance Launched Into AutoScalingGroup') 127 | try: 128 | command = "sudo yum -y install httpd && sudo service httpd start && sleep 60" 129 | run_command(event, command) 130 | send_lifecycle_action(event, 'CONTINUE') 131 | except Exception as e: 132 | message = 'Error running command: {}'.format(e) 133 | logger.error(message) 134 | send_lifecycle_action(event, 'ABANDON') 135 | logger.info('Execution Complete') 136 | return 137 | 138 | # If Instance is Launching into WarmPool 139 | if event['detail']['Origin'] == 'EC2' and event['detail']['Destination'] == 'WarmPool': 140 | logger.info('Instance Launched into WarmPool') 141 | try: 142 | command = "sudo yum -y install httpd && sudo service httpd start && sleep 60" 143 | run_command(event, command) 144 | send_lifecycle_action(event, 'CONTINUE') 145 | except Exception as e: 146 | message = 'Error running command: {}'.format(e) 147 | logger.error(message) 148 | send_lifecycle_action(event, 'ABANDON') 149 | logger.info('Execution Complete') 150 | return 151 | 152 | # If Instance is Moving into ASG from WarmPool 153 | if event['detail']['Origin'] == 'WarmPool' and event['detail']['Destination'] == 'AutoScalingGroup': 154 | logger.info('Instance Moved from WarmPool to AutoScalingGroup') 155 | try: 156 | command = "sudo service httpd start" 157 | run_command(event, command) 158 | send_lifecycle_action(event, 'CONTINUE') 159 | except Exception as e: 160 | message = 'Error running command: {}'.format(e) 161 | logger.error(message) 162 | send_lifecycle_action(event, 'ABANDON') 163 | logger.info('Execution Complete') 164 | return 165 | 166 | # Else 167 | logger.info('An unhandled lifecycle action occured, abandoning.') 168 | send_lifecycle_action(event, 'ABANDON') 169 | logger.info('Execution Complete') 170 | 171 | # End 172 | return -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-linux/source/LifecycleFunction/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-auto-scaling-group-examples/cffadfc4e51b37c7581eb73d5316961a57bb20aa/features/lifecycle-hooks/lambda-managed-linux/source/LifecycleFunction/requirements.txt -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-linux/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | Transform: AWS::Serverless-2016-10-31 18 | 19 | Description: > 20 | amazon-ec2-autoscaling-lifecycle-hook-lambda-linux-example 21 | 22 | Sample CloudFormation template that deploys an Auto Scaling Group with Lifecycle Hooks that are managed from a Lambda function. 23 | 24 | Parameters: 25 | AmiId: 26 | Description: AMI Id 27 | Type: 'AWS::SSM::Parameter::Value' 28 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 29 | AutoScalingGroupName: 30 | Description: TBD 31 | Type: String 32 | Default: Example Auto Scaling Group 33 | AutoScalingGroupMinSize: 34 | Description: TBD 35 | Type: Number 36 | Default: 0 37 | AutoScalingGroupMaxSize: 38 | Description: TBD 39 | Type: Number 40 | Default: 2 41 | AutoScalingGroupDesiredCapacity: 42 | Description: TBD 43 | Type: Number 44 | Default: 0 45 | InstanceType: 46 | Description: Amazon EC2 Instance Type 47 | Type: String 48 | Default: "t2.micro" 49 | InstanceKeyPair: 50 | Description: Amazon EC2 Key Pair 51 | Type: "AWS::EC2::KeyPair::KeyName" 52 | LifecycleHookName: 53 | Description: TBD 54 | Type: String 55 | Default: "app-install-hook" 56 | VpcCIDR: 57 | Description: Please enter the IP range (CIDR notation) for this VPC 58 | Type: String 59 | Default: 10.192.0.0/16 60 | AvailabilityZone1CIDR: 61 | Description: Please enter the IP range (CIDR notation) for the app subnet in the first Availability Zone 62 | Type: String 63 | Default: 10.192.10.0/24 64 | AvailabilityZone2CIDR: 65 | Description: Please enter the IP range (CIDR notation) for the app subnet in the second Availability Zone 66 | Type: String 67 | Default: 10.192.11.0/24 68 | AvailabilityZone3CIDR: 69 | Description: Please enter the IP range (CIDR notation) for the app subnet in the third Availability Zone 70 | Type: String 71 | Default: 10.192.12.0/24 72 | 73 | Metadata: 74 | AWS::CloudFormation::Interface: 75 | ParameterGroups: 76 | - 77 | Label: 78 | default: "VPC Configuration" 79 | Parameters: 80 | - VpcCIDR 81 | - AvailabilityZone1CIDR 82 | - AvailabilityZone2CIDR 83 | - AvailabilityZone3CIDR 84 | - 85 | Label: 86 | default: "Instance Configuration" 87 | Parameters: 88 | - AmiId 89 | - InstanceType 90 | - InstanceKeyPair 91 | - 92 | Label: 93 | default: "Auto Scaling Group Configuration" 94 | Parameters: 95 | - AutoScalingGroupName 96 | - AutoScalingGroupVpcID 97 | - AutoScalingGroupSubnetIDs 98 | - AutoScalingGroupMinSize 99 | - AutoScalingGroupMaxSize 100 | - AutoScalingGroupDesiredCapacity 101 | - LifecycleHookName 102 | 103 | Resources: 104 | 105 | # VPC/Networking Resources 106 | 107 | VPC: 108 | Type: AWS::EC2::VPC 109 | Properties: 110 | CidrBlock: !Ref VpcCIDR 111 | EnableDnsHostnames: true 112 | 113 | InternetGateway: 114 | Type: AWS::EC2::InternetGateway 115 | 116 | InternetGatewayAttachment: 117 | Type: AWS::EC2::VPCGatewayAttachment 118 | Properties: 119 | InternetGatewayId: !Ref InternetGateway 120 | VpcId: !Ref VPC 121 | 122 | AvailabilityZoneSubnet1: 123 | Type: AWS::EC2::Subnet 124 | Properties: 125 | VpcId: !Ref VPC 126 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 127 | CidrBlock: !Ref AvailabilityZone1CIDR 128 | MapPublicIpOnLaunch: true 129 | 130 | AvailabilityZoneSubnet2: 131 | Type: AWS::EC2::Subnet 132 | Properties: 133 | VpcId: !Ref VPC 134 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 135 | CidrBlock: !Ref AvailabilityZone2CIDR 136 | MapPublicIpOnLaunch: true 137 | 138 | AvailabilityZoneSubnet3: 139 | Type: AWS::EC2::Subnet 140 | Properties: 141 | VpcId: !Ref VPC 142 | AvailabilityZone: !Select [ 2, !GetAZs '' ] 143 | CidrBlock: !Ref AvailabilityZone3CIDR 144 | MapPublicIpOnLaunch: true 145 | 146 | InstanceSecurityGroup: 147 | Type: AWS::EC2::SecurityGroup 148 | Properties: 149 | VpcId: !Ref VPC 150 | GroupDescription: Instance Security Group 151 | 152 | RouteTable: 153 | Type: AWS::EC2::RouteTable 154 | Properties: 155 | VpcId: !Ref VPC 156 | 157 | DefaultPublicRoute: 158 | Type: AWS::EC2::Route 159 | DependsOn: InternetGatewayAttachment 160 | Properties: 161 | RouteTableId: !Ref RouteTable 162 | DestinationCidrBlock: 0.0.0.0/0 163 | GatewayId: !Ref InternetGateway 164 | 165 | AvailabilityZoneSubnet1RouteTableAssociation: 166 | Type: AWS::EC2::SubnetRouteTableAssociation 167 | Properties: 168 | RouteTableId: !Ref RouteTable 169 | SubnetId: !Ref AvailabilityZoneSubnet1 170 | 171 | AvailabilityZoneSubnet2RouteTableAssociation: 172 | Type: AWS::EC2::SubnetRouteTableAssociation 173 | Properties: 174 | RouteTableId: !Ref RouteTable 175 | SubnetId: !Ref AvailabilityZoneSubnet2 176 | 177 | AvailabilityZoneSubnet3RouteTableAssociation: 178 | Type: AWS::EC2::SubnetRouteTableAssociation 179 | Properties: 180 | RouteTableId: !Ref RouteTable 181 | SubnetId: !Ref AvailabilityZoneSubnet3 182 | 183 | # Instance/Auto Scaling Group Resources 184 | 185 | LaunchTemplate: 186 | Type: AWS::EC2::LaunchTemplate 187 | Properties: 188 | LaunchTemplateData: 189 | ImageId: !Ref AmiId 190 | InstanceType: !Ref InstanceType 191 | KeyName: !Ref InstanceKeyPair 192 | SecurityGroupIds: 193 | - !Ref InstanceSecurityGroup 194 | IamInstanceProfile: 195 | Arn: !GetAtt 196 | - InstanceProfile 197 | - Arn 198 | TagSpecifications: 199 | - ResourceType: instance 200 | Tags: 201 | - Key: Name 202 | Value: LifecycleHookExampleInstance 203 | UserData: 204 | 'Fn::Base64': 205 | !Sub 206 | - |- 207 | Content-Type: multipart/mixed; boundary="//" 208 | MIME-Version: 1.0 209 | 210 | --// 211 | Content-Type: text/cloud-config; charset="us-ascii" 212 | MIME-Version: 1.0 213 | Content-Transfer-Encoding: 7bit 214 | Content-Disposition: attachment; filename="cloud-config.txt" 215 | 216 | #cloud-config 217 | cloud_final_modules: 218 | - [scripts-user, always] 219 | 220 | --// 221 | Content-Type: text/x-shellscript; charset="us-ascii" 222 | MIME-Version: 1.0 223 | Content-Transfer-Encoding: 7bit 224 | Content-Disposition: attachment; filename="userdata.txt" 225 | 226 | #!/bin/bash 227 | rpm -q stress &> /dev/null 228 | if [ $? -ne 0 ] 229 | then 230 | sudo amazon-linux-extras install epel -y 231 | sudo yum install stress -y 232 | fi 233 | --// 234 | - { 235 | autoScalingGroupName: !Ref AutoScalingGroupName, 236 | lifecycleHookName: !Ref LifecycleHookName 237 | } 238 | 239 | InstanceRole: 240 | Type: "AWS::IAM::Role" 241 | Properties: 242 | AssumeRolePolicyDocument: 243 | Version: "2012-10-17" 244 | Statement: 245 | - 246 | Effect: "Allow" 247 | Principal: 248 | Service: 249 | - "ec2.amazonaws.com" 250 | Action: 251 | - "sts:AssumeRole" 252 | Path: "/" 253 | ManagedPolicyArns: 254 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 255 | 256 | InstanceProfile: 257 | Type: "AWS::IAM::InstanceProfile" 258 | Properties: 259 | Path: "/" 260 | Roles: 261 | - 262 | Ref: "InstanceRole" 263 | 264 | AutoScalingGroup: 265 | Type: AWS::AutoScaling::AutoScalingGroup 266 | Properties: 267 | AutoScalingGroupName: !Ref AutoScalingGroupName 268 | LaunchTemplate: 269 | LaunchTemplateId: !Ref LaunchTemplate 270 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 271 | DesiredCapacity: !Ref AutoScalingGroupDesiredCapacity 272 | MaxSize: !Ref AutoScalingGroupMaxSize 273 | MinSize: !Ref AutoScalingGroupMinSize 274 | VPCZoneIdentifier: 275 | - !Ref AvailabilityZoneSubnet1 276 | - !Ref AvailabilityZoneSubnet2 277 | - !Ref AvailabilityZoneSubnet3 278 | 279 | LifecycleHook: 280 | Type: AWS::AutoScaling::LifecycleHook 281 | Properties: 282 | LifecycleHookName: !Ref LifecycleHookName 283 | AutoScalingGroupName: !Ref AutoScalingGroup 284 | DefaultResult: ABANDON 285 | HeartbeatTimeout: 900 286 | LifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING" 287 | 288 | # Lambda/CloudWatch Rule Resources 289 | 290 | LifecycleEventRule: 291 | Type: AWS::Events::Rule 292 | Properties: 293 | Description: "EventRule" 294 | EventPattern: 295 | source: 296 | - "aws.autoscaling" 297 | detail-type: 298 | - "EC2 Instance-launch Lifecycle Action" 299 | detail: 300 | AutoScalingGroupName: 301 | - !Ref AutoScalingGroup 302 | State: "ENABLED" 303 | Targets: 304 | - 305 | Arn: 306 | Fn::GetAtt: 307 | - "LifecycleFunction" 308 | - "Arn" 309 | Id: "LifecycleFunctionV1" 310 | 311 | PermissionForEventsToInvokeLifecycleLambda: 312 | Type: AWS::Lambda::Permission 313 | Properties: 314 | FunctionName: 315 | Ref: "LifecycleFunction" 316 | Action: "lambda:InvokeFunction" 317 | Principal: "events.amazonaws.com" 318 | SourceArn: 319 | Fn::GetAtt: 320 | - "LifecycleEventRule" 321 | - "Arn" 322 | 323 | LifecycleFunctionRole: 324 | Type: "AWS::IAM::Role" 325 | Properties: 326 | AssumeRolePolicyDocument: 327 | Version: "2012-10-17" 328 | Statement: 329 | - Effect: Allow 330 | Principal: 331 | Service: lambda.amazonaws.com 332 | Action: "sts:AssumeRole" 333 | Policies: 334 | - PolicyName: LifecycleFunctionLogsPolicy 335 | PolicyDocument: 336 | Version: "2012-10-17" 337 | Statement: 338 | - Effect: Allow 339 | Action: 340 | - "logs:CreateLogGroup" 341 | - "logs:CreateLogStream" 342 | - "logs:PutLogEvents" 343 | Resource: "*" 344 | - PolicyName: LifecycleFunctionCompleteLifecycleActionPolicy 345 | PolicyDocument: 346 | Version: "2012-10-17" 347 | Statement: 348 | - Effect: Allow 349 | Action: 350 | - "autoscaling:CompleteLifecycleAction" 351 | Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AutoScalingGroupName}" 352 | - PolicyName: LifecycleFunctionSSMSendCommandDocumentPolicy 353 | PolicyDocument: 354 | Version: "2012-10-17" 355 | Statement: 356 | - Effect: Allow 357 | Action: 358 | - "ssm:SendCommand" 359 | Resource: !Sub "arn:aws:ssm:${AWS::Region}::document/AWS-RunShellScript" 360 | - PolicyName: LifecycleFunctionSSMSendCommandInstancePolicy 361 | PolicyDocument: 362 | Version: "2012-10-17" 363 | Statement: 364 | - Effect: Allow 365 | Action: 366 | - "ssm:SendCommand" 367 | Resource: "arn:aws:ec2:*:*:instance/*" 368 | Condition: 369 | StringEquals: 370 | ssm:ResourceTag/aws:autoscaling:groupName: 371 | - !Ref AutoScalingGroup 372 | - PolicyName: LifecycleFunctionSSMGetCommandInvocationPolicy 373 | PolicyDocument: 374 | Version: "2012-10-17" 375 | Statement: 376 | - Effect: Allow 377 | Action: 378 | - "ssm:GetCommandInvocation" 379 | Resource: "*" 380 | 381 | LifecycleFunction: 382 | Type: AWS::Serverless::Function 383 | Properties: 384 | CodeUri: source/LifecycleFunction/ 385 | Handler: app.lambda_handler 386 | Role: !GetAtt [ LifecycleFunctionRole, Arn ] 387 | Runtime: python3.8 388 | Timeout: 600 -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-windows/README.md: -------------------------------------------------------------------------------- 1 | # Auto Scaling Group Lifecycle Hooks Example - Lambda Managed Windows 2 | 3 | This example solution deploys an Auto Scaling group within a VPC. A lifecycle hook is enabled for the Auto Scaling group and a Lambda Function invokes in response to Lifecycle Action Events. The Lambda function uses AWS Systems Manager to install a sample application onto instances in the Auto Scaling group as they are Launched. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 8 | 9 | ### Prerequisites 10 | 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 12 | * [Python 3 installed](https://www.python.org/downloads/) 13 | * [Docker installed](https://www.docker.com/community-edition) 14 | * [SAM CLI installed](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 15 | 16 | ## Deployment Steps 17 | 18 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 19 | 20 | 1. Create a `S3 bucket` where we can upload our packaged Lambda functions for deployment. 21 | 22 | ```bash 23 | aws s3 mb s3://BUCKET_NAME 24 | ``` 25 | 26 | 2. Change directories to this example. 27 | 28 | ```bash 29 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/lifecycle-hooks/lambda-managed-windows 30 | ``` 31 | 32 | 3. Build the solution. 33 | 34 | ```bash 35 | sam build --use-container 36 | ``` 37 | 38 | 5. Package the solution. You will need to replace `REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME` with the name of the S3 bucket created in the first step. 39 | 40 | ```bash 41 | sam package \ 42 | --output-template-file packaged.yaml \ 43 | --s3-bucket REPLACE_THIS_WITH_YOUR_S3_BUCKET_NAME 44 | ``` 45 | 46 | 6. Deploy the solution. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 47 | 48 | ```bash 49 | sam deploy \ 50 | --template-file packaged.yaml \ 51 | --stack-name lifecycle-hook-example \ 52 | --capabilities CAPABILITY_IAM \ 53 | --parameter-overrides \ 54 | InstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 55 | ``` 56 | 57 | ## Clean Up 58 | 59 | 1. Delete the stack. 60 | 61 | ```bash 62 | aws cloudformation delete-stack --stack-name lifecycle-hook-example 63 | ``` -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-windows/source/LifecycleFunction/app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | import boto3 17 | import os 18 | import json 19 | import logging 20 | import time 21 | 22 | from botocore.exceptions import ClientError 23 | 24 | logger = logging.getLogger() 25 | logger.setLevel(logging.INFO) 26 | 27 | autoscaling = boto3.client('autoscaling') 28 | ssm = boto3.client('ssm') 29 | 30 | def send_lifecycle_action(event, result): 31 | try: 32 | response = autoscaling.complete_lifecycle_action( 33 | LifecycleHookName=event['detail']['LifecycleHookName'], 34 | AutoScalingGroupName=event['detail']['AutoScalingGroupName'], 35 | LifecycleActionToken=event['detail']['LifecycleActionToken'], 36 | LifecycleActionResult=result, 37 | InstanceId=event['detail']['EC2InstanceId'] 38 | ) 39 | 40 | logger.info(response) 41 | except ClientError as e: 42 | message = 'Error completing lifecycle action: {}'.format(e) 43 | logger.error(message) 44 | raise Exception(message) 45 | 46 | return 47 | 48 | def run_command(event, command): 49 | 50 | # Run Command 51 | logger.info('Calling SendCommand: {} for instance: {}'.format(command, event['detail']['EC2InstanceId'])) 52 | attempt = 0 53 | while attempt < 10: 54 | attempt = attempt + 1 55 | try: 56 | time.sleep(10 * attempt) 57 | logger.info('SendCommand, attempt #: {}'.format(attempt)) 58 | response = ssm.send_command( 59 | InstanceIds=[event['detail']['EC2InstanceId']], 60 | DocumentName='AWS-RunPowerShellScript', 61 | Parameters={ 62 | 'commands': [ 63 | command 64 | ] 65 | } 66 | ) 67 | 68 | logger.info(response) 69 | if 'Command' in response: 70 | break 71 | 72 | if attempt == 10: 73 | message = 'Command did not execute succesfully in time allowed.' 74 | raise Exception(message) 75 | 76 | except ClientError as e: 77 | message = 'Error calling SendCommand: {}'.format(e) 78 | logger.error(message) 79 | continue 80 | 81 | # Check Command Status 82 | command_id = response['Command']['CommandId'] 83 | logger.info('Calling GetCommandInvocation for command: {} for instance: {}'.format(command_id, event['detail']['EC2InstanceId'])) 84 | attempt = 0 85 | 86 | while attempt < 20: 87 | attempt = attempt + 1 88 | try: 89 | time.sleep(10 * attempt) 90 | logger.info('GetCommandInvocation, attempt #: {}'.format(attempt)) 91 | result = ssm.get_command_invocation( 92 | CommandId=command_id, 93 | InstanceId=event['detail']['EC2InstanceId'], 94 | ) 95 | if result['Status'] == 'InProgress': 96 | logger.info('Command is running.') 97 | continue 98 | elif result['Status'] == 'Success': 99 | logger.info('Command completed successfully: {}'.format(result['StandardOutputContent'])) 100 | break 101 | elif result['Status'] == 'Failed': 102 | message = 'Command did not execute successfully: {}'.format(e) 103 | logger.error(message) 104 | raise Exception(message) 105 | else: 106 | message = 'Command has an unhandled status, will continue: {}'.format(e) 107 | logger.warning(message) 108 | continue 109 | except ssm.exceptions.InvocationDoesNotExist as e: 110 | message = 'Error calling GetCommandInvocation: {}'.format(e) 111 | logger.error(message) 112 | raise Exception(message) 113 | 114 | if result['Status'] == 'Success': 115 | return 116 | else: 117 | message = 'Command did not execute succesfully in time allowed.' 118 | raise Exception(message) 119 | 120 | def lambda_handler(event, context): 121 | 122 | logger.info(event) 123 | 124 | # If Instance is Launching into AutoScalingGroup 125 | if event['detail']['Origin'] == 'EC2' and event['detail']['Destination'] == 'AutoScalingGroup': 126 | logger.info('Instance Launched Into AutoScalingGroup') 127 | try: 128 | command = """ 129 | if ((Get-WindowsFeature Web-Server).InstallState -ne "Installed") { 130 | Install-WindowsFeature -name Web-Server -IncludeManagementTools 131 | Start-Process "iisreset.exe" -NoNewWindow -Wait 132 | Write-Host "Sleeping for 120 Seconds to Simulate IIS Configuration." 133 | Start-Sleep -s 120 134 | } 135 | """ 136 | run_command(event, command) 137 | send_lifecycle_action(event, 'CONTINUE') 138 | except Exception as e: 139 | message = 'Error running command: {}'.format(e) 140 | logger.error(message) 141 | send_lifecycle_action(event, 'ABANDON') 142 | logger.info('Execution Complete') 143 | return 144 | 145 | # If Instance is Launching into WarmPool 146 | if event['detail']['Origin'] == 'EC2' and event['detail']['Destination'] == 'WarmPool': 147 | logger.info('Instance Launched into WarmPool') 148 | try: 149 | command = """ 150 | if ((Get-WindowsFeature Web-Server).InstallState -ne "Installed") { 151 | Install-WindowsFeature -name Web-Server -IncludeManagementTools 152 | Start-Process "iisreset.exe" -NoNewWindow -Wait 153 | Write-Host "Sleeping for 120 Seconds to Simulate IIS Configuration." 154 | Start-Sleep -s 120 155 | } 156 | """ 157 | run_command(event, command) 158 | send_lifecycle_action(event, 'CONTINUE') 159 | except Exception as e: 160 | message = 'Error running command: {}'.format(e) 161 | logger.error(message) 162 | send_lifecycle_action(event, 'ABANDON') 163 | logger.info('Execution Complete') 164 | return 165 | 166 | # If Instance is Moving into ASG from WarmPool 167 | if event['detail']['Origin'] == 'WarmPool' and event['detail']['Destination'] == 'AutoScalingGroup': 168 | logger.info('Instance Moved from WarmPool to AutoScalingGroup') 169 | try: 170 | command = """ 171 | if ((Get-WindowsFeature Web-Server).InstallState -eq "Installed") { 172 | Write-Host "IIS is installed, stopping and starting IIS" 173 | Start-Process "iisreset.exe" -NoNewWindow -Wait 174 | } 175 | """ 176 | run_command(event, command) 177 | send_lifecycle_action(event, 'CONTINUE') 178 | except Exception as e: 179 | message = 'Error running command: {}'.format(e) 180 | logger.error(message) 181 | send_lifecycle_action(event, 'ABANDON') 182 | logger.info('Execution Complete') 183 | return 184 | 185 | # Else 186 | logger.info('An unhandled lifecycle action occured, abandoning.') 187 | send_lifecycle_action(event, 'ABANDON') 188 | logger.info('Execution Complete') 189 | 190 | # End 191 | return -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-windows/source/LifecycleFunction/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-auto-scaling-group-examples/cffadfc4e51b37c7581eb73d5316961a57bb20aa/features/lifecycle-hooks/lambda-managed-windows/source/LifecycleFunction/requirements.txt -------------------------------------------------------------------------------- /features/lifecycle-hooks/lambda-managed-windows/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | Transform: AWS::Serverless-2016-10-31 18 | 19 | Description: > 20 | amazon-ec2-autoscaling-lifecycle-hook-lambda-windows-example 21 | 22 | Sample CloudFormation template that deploys an Auto Scaling Group with Lifecycle Hooks that are managed from a Lambda function. 23 | 24 | Parameters: 25 | AmiId: 26 | Description: AMI Id 27 | Type: 'AWS::SSM::Parameter::Value' 28 | Default: '/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base' 29 | AutoScalingGroupName: 30 | Description: TBD 31 | Type: String 32 | Default: Example Auto Scaling Group 33 | AutoScalingGroupMinSize: 34 | Description: TBD 35 | Type: Number 36 | Default: 0 37 | AutoScalingGroupMaxSize: 38 | Description: TBD 39 | Type: Number 40 | Default: 2 41 | AutoScalingGroupDesiredCapacity: 42 | Description: TBD 43 | Type: Number 44 | Default: 0 45 | InstanceType: 46 | Description: Amazon EC2 Instance Type 47 | Type: String 48 | Default: "t2.micro" 49 | InstanceKeyPair: 50 | Description: Amazon EC2 Key Pair 51 | Type: "AWS::EC2::KeyPair::KeyName" 52 | LifecycleHookName: 53 | Description: TBD 54 | Type: String 55 | Default: "app-install-hook" 56 | VpcCIDR: 57 | Description: Please enter the IP range (CIDR notation) for this VPC 58 | Type: String 59 | Default: 10.192.0.0/16 60 | AvailabilityZone1CIDR: 61 | Description: Please enter the IP range (CIDR notation) for the app subnet in the first Availability Zone 62 | Type: String 63 | Default: 10.192.10.0/24 64 | AvailabilityZone2CIDR: 65 | Description: Please enter the IP range (CIDR notation) for the app subnet in the second Availability Zone 66 | Type: String 67 | Default: 10.192.11.0/24 68 | AvailabilityZone3CIDR: 69 | Description: Please enter the IP range (CIDR notation) for the app subnet in the third Availability Zone 70 | Type: String 71 | Default: 10.192.12.0/24 72 | 73 | Metadata: 74 | AWS::CloudFormation::Interface: 75 | ParameterGroups: 76 | - 77 | Label: 78 | default: "VPC Configuration" 79 | Parameters: 80 | - VpcCIDR 81 | - AvailabilityZone1CIDR 82 | - AvailabilityZone2CIDR 83 | - AvailabilityZone3CIDR 84 | - 85 | Label: 86 | default: "Instance Configuration" 87 | Parameters: 88 | - AmiId 89 | - InstanceType 90 | - InstanceKeyPair 91 | - 92 | Label: 93 | default: "Auto Scaling Group Configuration" 94 | Parameters: 95 | - AutoScalingGroupName 96 | - AutoScalingGroupVpcID 97 | - AutoScalingGroupSubnetIDs 98 | - AutoScalingGroupMinSize 99 | - AutoScalingGroupMaxSize 100 | - AutoScalingGroupDesiredCapacity 101 | - LifecycleHookName 102 | 103 | Resources: 104 | 105 | # VPC/Networking Resources 106 | 107 | VPC: 108 | Type: AWS::EC2::VPC 109 | Properties: 110 | CidrBlock: !Ref VpcCIDR 111 | EnableDnsHostnames: true 112 | 113 | InternetGateway: 114 | Type: AWS::EC2::InternetGateway 115 | 116 | InternetGatewayAttachment: 117 | Type: AWS::EC2::VPCGatewayAttachment 118 | Properties: 119 | InternetGatewayId: !Ref InternetGateway 120 | VpcId: !Ref VPC 121 | 122 | AvailabilityZoneSubnet1: 123 | Type: AWS::EC2::Subnet 124 | Properties: 125 | VpcId: !Ref VPC 126 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 127 | CidrBlock: !Ref AvailabilityZone1CIDR 128 | MapPublicIpOnLaunch: true 129 | 130 | AvailabilityZoneSubnet2: 131 | Type: AWS::EC2::Subnet 132 | Properties: 133 | VpcId: !Ref VPC 134 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 135 | CidrBlock: !Ref AvailabilityZone2CIDR 136 | MapPublicIpOnLaunch: true 137 | 138 | AvailabilityZoneSubnet3: 139 | Type: AWS::EC2::Subnet 140 | Properties: 141 | VpcId: !Ref VPC 142 | AvailabilityZone: !Select [ 2, !GetAZs '' ] 143 | CidrBlock: !Ref AvailabilityZone3CIDR 144 | MapPublicIpOnLaunch: true 145 | 146 | InstanceSecurityGroup: 147 | Type: AWS::EC2::SecurityGroup 148 | Properties: 149 | VpcId: !Ref VPC 150 | GroupDescription: Instance Security Group 151 | 152 | RouteTable: 153 | Type: AWS::EC2::RouteTable 154 | Properties: 155 | VpcId: !Ref VPC 156 | 157 | DefaultPublicRoute: 158 | Type: AWS::EC2::Route 159 | DependsOn: InternetGatewayAttachment 160 | Properties: 161 | RouteTableId: !Ref RouteTable 162 | DestinationCidrBlock: 0.0.0.0/0 163 | GatewayId: !Ref InternetGateway 164 | 165 | AvailabilityZoneSubnet1RouteTableAssociation: 166 | Type: AWS::EC2::SubnetRouteTableAssociation 167 | Properties: 168 | RouteTableId: !Ref RouteTable 169 | SubnetId: !Ref AvailabilityZoneSubnet1 170 | 171 | AvailabilityZoneSubnet2RouteTableAssociation: 172 | Type: AWS::EC2::SubnetRouteTableAssociation 173 | Properties: 174 | RouteTableId: !Ref RouteTable 175 | SubnetId: !Ref AvailabilityZoneSubnet2 176 | 177 | AvailabilityZoneSubnet3RouteTableAssociation: 178 | Type: AWS::EC2::SubnetRouteTableAssociation 179 | Properties: 180 | RouteTableId: !Ref RouteTable 181 | SubnetId: !Ref AvailabilityZoneSubnet3 182 | 183 | # Instance/Auto Scaling Group Resources 184 | 185 | LaunchTemplate: 186 | Type: AWS::EC2::LaunchTemplate 187 | Properties: 188 | LaunchTemplateData: 189 | ImageId: !Ref AmiId 190 | InstanceType: !Ref InstanceType 191 | KeyName: !Ref InstanceKeyPair 192 | SecurityGroupIds: 193 | - !Ref InstanceSecurityGroup 194 | IamInstanceProfile: 195 | Arn: !GetAtt 196 | - InstanceProfile 197 | - Arn 198 | TagSpecifications: 199 | - ResourceType: instance 200 | Tags: 201 | - Key: Name 202 | Value: LifecycleHookExampleInstance 203 | UserData: 204 | 'Fn::Base64': 205 | !Sub 206 | - |- 207 | 208 | Write-Host "This is a command run from UserData." 209 | 210 | true 211 | - { 212 | autoScalingGroupName: !Ref AutoScalingGroupName, 213 | lifecycleHookName: !Ref LifecycleHookName 214 | } 215 | 216 | InstanceRole: 217 | Type: "AWS::IAM::Role" 218 | Properties: 219 | AssumeRolePolicyDocument: 220 | Version: "2012-10-17" 221 | Statement: 222 | - 223 | Effect: "Allow" 224 | Principal: 225 | Service: 226 | - "ec2.amazonaws.com" 227 | Action: 228 | - "sts:AssumeRole" 229 | Path: "/" 230 | ManagedPolicyArns: 231 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 232 | 233 | InstanceProfile: 234 | Type: "AWS::IAM::InstanceProfile" 235 | Properties: 236 | Path: "/" 237 | Roles: 238 | - 239 | Ref: "InstanceRole" 240 | 241 | AutoScalingGroup: 242 | Type: AWS::AutoScaling::AutoScalingGroup 243 | Properties: 244 | AutoScalingGroupName: !Ref AutoScalingGroupName 245 | LaunchTemplate: 246 | LaunchTemplateId: !Ref LaunchTemplate 247 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 248 | DesiredCapacity: !Ref AutoScalingGroupDesiredCapacity 249 | MaxSize: !Ref AutoScalingGroupMaxSize 250 | MinSize: !Ref AutoScalingGroupMinSize 251 | VPCZoneIdentifier: 252 | - !Ref AvailabilityZoneSubnet1 253 | - !Ref AvailabilityZoneSubnet2 254 | - !Ref AvailabilityZoneSubnet3 255 | 256 | LifecycleHook: 257 | Type: AWS::AutoScaling::LifecycleHook 258 | Properties: 259 | LifecycleHookName: !Ref LifecycleHookName 260 | AutoScalingGroupName: !Ref AutoScalingGroup 261 | DefaultResult: ABANDON 262 | HeartbeatTimeout: 900 263 | LifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING" 264 | 265 | # Lambda/CloudWatch Rule Resources 266 | 267 | LifecycleEventRule: 268 | Type: AWS::Events::Rule 269 | Properties: 270 | Description: "EventRule" 271 | EventPattern: 272 | source: 273 | - "aws.autoscaling" 274 | detail-type: 275 | - "EC2 Instance-launch Lifecycle Action" 276 | detail: 277 | AutoScalingGroupName: 278 | - !Ref AutoScalingGroup 279 | State: "ENABLED" 280 | Targets: 281 | - 282 | Arn: 283 | Fn::GetAtt: 284 | - "LifecycleFunction" 285 | - "Arn" 286 | Id: "LifecycleFunctionV1" 287 | 288 | PermissionForEventsToInvokeLifecycleLambda: 289 | Type: AWS::Lambda::Permission 290 | Properties: 291 | FunctionName: 292 | Ref: "LifecycleFunction" 293 | Action: "lambda:InvokeFunction" 294 | Principal: "events.amazonaws.com" 295 | SourceArn: 296 | Fn::GetAtt: 297 | - "LifecycleEventRule" 298 | - "Arn" 299 | 300 | LifecycleFunctionRole: 301 | Type: "AWS::IAM::Role" 302 | Properties: 303 | AssumeRolePolicyDocument: 304 | Version: "2012-10-17" 305 | Statement: 306 | - Effect: Allow 307 | Principal: 308 | Service: lambda.amazonaws.com 309 | Action: "sts:AssumeRole" 310 | Policies: 311 | - PolicyName: LifecycleFunctionLogsPolicy 312 | PolicyDocument: 313 | Version: "2012-10-17" 314 | Statement: 315 | - Effect: Allow 316 | Action: 317 | - "logs:CreateLogGroup" 318 | - "logs:CreateLogStream" 319 | - "logs:PutLogEvents" 320 | Resource: "*" 321 | - PolicyName: LifecycleFunctionCompleteLifecycleActionPolicy 322 | PolicyDocument: 323 | Version: "2012-10-17" 324 | Statement: 325 | - Effect: Allow 326 | Action: 327 | - "autoscaling:CompleteLifecycleAction" 328 | Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AutoScalingGroupName}" 329 | - PolicyName: LifecycleFunctionSSMSendCommandDocumentPolicy 330 | PolicyDocument: 331 | Version: "2012-10-17" 332 | Statement: 333 | - Effect: Allow 334 | Action: 335 | - "ssm:SendCommand" 336 | Resource: !Sub "arn:aws:ssm:${AWS::Region}::document/AWS-RunPowerShellScript" 337 | - PolicyName: LifecycleFunctionSSMSendCommandInstancePolicy 338 | PolicyDocument: 339 | Version: "2012-10-17" 340 | Statement: 341 | - Effect: Allow 342 | Action: 343 | - "ssm:SendCommand" 344 | Resource: "arn:aws:ec2:*:*:instance/*" 345 | Condition: 346 | StringEquals: 347 | ssm:ResourceTag/aws:autoscaling:groupName: 348 | - !Ref AutoScalingGroup 349 | - PolicyName: LifecycleFunctionSSMGetCommandInvocationPolicy 350 | PolicyDocument: 351 | Version: "2012-10-17" 352 | Statement: 353 | - Effect: Allow 354 | Action: 355 | - "ssm:GetCommandInvocation" 356 | Resource: "*" 357 | 358 | LifecycleFunction: 359 | Type: AWS::Serverless::Function 360 | Properties: 361 | CodeUri: source/LifecycleFunction/ 362 | Handler: app.lambda_handler 363 | Role: !GetAtt [ LifecycleFunctionRole, Arn ] 364 | Runtime: python3.8 365 | Timeout: 900 -------------------------------------------------------------------------------- /features/lifecycle-hooks/userdata-managed-linux/README.md: -------------------------------------------------------------------------------- 1 | # Auto Scaling Group Lifecycle Hooks Example - User Data Managed Linux 2 | 3 | This example solution deploys an Auto Scaling group within a VPC. A lifecycle hook is enabled for the Auto Scaling group and a userdata executeds during instance startup. The userdata script installs an application if it's not installed and completes the lifecycle hook action. If the application is already installed, the userdata script starts the appliction and completes the lifecycle hook action. The userdata script is configured to execute during every instance startup, so bootstrap actions can complete when the instance is initially launched and when restarted from a stopped state. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 8 | 9 | ### Prerequisites 10 | 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 12 | 13 | ## Deployment Steps 14 | 15 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 16 | 17 | 1. Change directories to this example. 18 | 19 | ```bash 20 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/lifecycle-hooks/userdata-managed-linux 21 | ``` 22 | 23 | 2. Deploy the CloudFormation Stack. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 24 | 25 | ```bash 26 | aws cloudformation deploy \ 27 | --template-file template.yaml \ 28 | --stack-name lifecycle-hook-example \ 29 | --capabilities CAPABILITY_IAM \ 30 | --parameter-overrides \ 31 | InstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 32 | ``` 33 | 34 | ## Clean Up 35 | 36 | Delete the CloudFormation Stack 37 | 38 | ```bash 39 | aws cloudformation delete-stack --stack-name lifecycle-hook-example 40 | ``` 41 | -------------------------------------------------------------------------------- /features/lifecycle-hooks/userdata-managed-linux/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | 18 | Description: > 19 | amazon-ec2-autoscaling-lifecycle-hook-userdata-linux-example 20 | 21 | Sample CloudFormation template that deploys an Auto Scaling Group with Lifecycle Hooks that are managed from User Data. 22 | 23 | Parameters: 24 | AmiId: 25 | Description: AMI Id 26 | Type: 'AWS::SSM::Parameter::Value' 27 | Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' 28 | AutoScalingGroupName: 29 | Description: TBD 30 | Type: String 31 | Default: Example Auto Scaling Group 32 | AutoScalingGroupMinSize: 33 | Description: TBD 34 | Type: Number 35 | Default: 0 36 | AutoScalingGroupMaxSize: 37 | Description: TBD 38 | Type: Number 39 | Default: 2 40 | AutoScalingGroupDesiredCapacity: 41 | Description: TBD 42 | Type: Number 43 | Default: 0 44 | InstanceType: 45 | Description: Amazon EC2 Instance Type 46 | Type: String 47 | Default: "t2.micro" 48 | InstanceKeyPair: 49 | Description: Amazon EC2 Key Pair 50 | Type: "AWS::EC2::KeyPair::KeyName" 51 | LifecycleHookName: 52 | Description: TBD 53 | Type: String 54 | Default: "app-install-hook" 55 | VpcCIDR: 56 | Description: Please enter the IP range (CIDR notation) for this VPC 57 | Type: String 58 | Default: 10.192.0.0/16 59 | AvailabilityZone1CIDR: 60 | Description: Please enter the IP range (CIDR notation) for the app subnet in the first Availability Zone 61 | Type: String 62 | Default: 10.192.10.0/24 63 | AvailabilityZone2CIDR: 64 | Description: Please enter the IP range (CIDR notation) for the app subnet in the second Availability Zone 65 | Type: String 66 | Default: 10.192.11.0/24 67 | AvailabilityZone3CIDR: 68 | Description: Please enter the IP range (CIDR notation) for the app subnet in the third Availability Zone 69 | Type: String 70 | Default: 10.192.12.0/24 71 | Metadata: 72 | AWS::CloudFormation::Interface: 73 | ParameterGroups: 74 | - 75 | Label: 76 | default: "VPC Configuration" 77 | Parameters: 78 | - VpcCIDR 79 | - AvailabilityZone1CIDR 80 | - AvailabilityZone2CIDR 81 | - AvailabilityZone3CIDR 82 | - 83 | Label: 84 | default: "Instance Configuration" 85 | Parameters: 86 | - AmiId 87 | - InstanceType 88 | - InstanceKeyPair 89 | - 90 | Label: 91 | default: "Auto Scaling Group Configuration" 92 | Parameters: 93 | - AutoScalingGroupName 94 | - AutoScalingGroupVpcID 95 | - AutoScalingGroupSubnetIDs 96 | - AutoScalingGroupMinSize 97 | - AutoScalingGroupMaxSize 98 | - AutoScalingGroupDesiredCapacity 99 | - LifecycleHookName 100 | 101 | 102 | Mappings: 103 | RegionMap: 104 | us-east-1: 105 | "HVM64": "ami-0ff8a91507f77f867" 106 | us-west-1: 107 | "HVM64": "ami-0bdb828fd58c52235" 108 | eu-west-1: 109 | "HVM64": "ami-047bb4163c506cd98" 110 | ap-southeast-1: 111 | "HVM64": "ami-08569b978cc4dfa10" 112 | ap-northeast-1: 113 | "HVM64": "ami-06cd52961ce9f0d85" 114 | 115 | Resources: 116 | 117 | # VPC/Networking Resources 118 | 119 | VPC: 120 | Type: AWS::EC2::VPC 121 | Properties: 122 | CidrBlock: !Ref VpcCIDR 123 | EnableDnsHostnames: true 124 | 125 | InternetGateway: 126 | Type: AWS::EC2::InternetGateway 127 | 128 | InternetGatewayAttachment: 129 | Type: AWS::EC2::VPCGatewayAttachment 130 | Properties: 131 | InternetGatewayId: !Ref InternetGateway 132 | VpcId: !Ref VPC 133 | 134 | AvailabilityZoneSubnet1: 135 | Type: AWS::EC2::Subnet 136 | Properties: 137 | VpcId: !Ref VPC 138 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 139 | CidrBlock: !Ref AvailabilityZone1CIDR 140 | MapPublicIpOnLaunch: true 141 | 142 | AvailabilityZoneSubnet2: 143 | Type: AWS::EC2::Subnet 144 | Properties: 145 | VpcId: !Ref VPC 146 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 147 | CidrBlock: !Ref AvailabilityZone2CIDR 148 | MapPublicIpOnLaunch: true 149 | 150 | AvailabilityZoneSubnet3: 151 | Type: AWS::EC2::Subnet 152 | Properties: 153 | VpcId: !Ref VPC 154 | AvailabilityZone: !Select [ 2, !GetAZs '' ] 155 | CidrBlock: !Ref AvailabilityZone3CIDR 156 | MapPublicIpOnLaunch: true 157 | 158 | InstanceSecurityGroup: 159 | Type: AWS::EC2::SecurityGroup 160 | Properties: 161 | VpcId: !Ref VPC 162 | GroupDescription: Instance Security Group 163 | 164 | RouteTable: 165 | Type: AWS::EC2::RouteTable 166 | Properties: 167 | VpcId: !Ref VPC 168 | 169 | DefaultPublicRoute: 170 | Type: AWS::EC2::Route 171 | DependsOn: InternetGatewayAttachment 172 | Properties: 173 | RouteTableId: !Ref RouteTable 174 | DestinationCidrBlock: 0.0.0.0/0 175 | GatewayId: !Ref InternetGateway 176 | 177 | AvailabilityZoneSubnet1RouteTableAssociation: 178 | Type: AWS::EC2::SubnetRouteTableAssociation 179 | Properties: 180 | RouteTableId: !Ref RouteTable 181 | SubnetId: !Ref AvailabilityZoneSubnet1 182 | 183 | AvailabilityZoneSubnet2RouteTableAssociation: 184 | Type: AWS::EC2::SubnetRouteTableAssociation 185 | Properties: 186 | RouteTableId: !Ref RouteTable 187 | SubnetId: !Ref AvailabilityZoneSubnet2 188 | 189 | AvailabilityZoneSubnet3RouteTableAssociation: 190 | Type: AWS::EC2::SubnetRouteTableAssociation 191 | Properties: 192 | RouteTableId: !Ref RouteTable 193 | SubnetId: !Ref AvailabilityZoneSubnet3 194 | 195 | # Instance/Auto Scaling Group Resources 196 | 197 | LaunchTemplate: 198 | Type: AWS::EC2::LaunchTemplate 199 | Properties: 200 | LaunchTemplateData: 201 | ImageId: !Ref AmiId 202 | InstanceType: !Ref InstanceType 203 | KeyName: !Ref InstanceKeyPair 204 | SecurityGroupIds: 205 | - !Ref InstanceSecurityGroup 206 | IamInstanceProfile: 207 | Arn: !GetAtt 208 | - InstanceProfile 209 | - Arn 210 | TagSpecifications: 211 | - ResourceType: instance 212 | Tags: 213 | - Key: Name 214 | Value: LifecycleHookExampleInstance 215 | UserData: 216 | 'Fn::Base64': 217 | !Sub 218 | - |- 219 | Content-Type: multipart/mixed; boundary="//" 220 | MIME-Version: 1.0 221 | 222 | --// 223 | Content-Type: text/cloud-config; charset="us-ascii" 224 | MIME-Version: 1.0 225 | Content-Transfer-Encoding: 7bit 226 | Content-Disposition: attachment; filename="cloud-config.txt" 227 | 228 | #cloud-config 229 | cloud_final_modules: 230 | - [scripts-user, always] 231 | 232 | --// 233 | Content-Type: text/x-shellscript; charset="us-ascii" 234 | MIME-Version: 1.0 235 | Content-Transfer-Encoding: 7bit 236 | Content-Disposition: attachment; filename="userdata.txt" 237 | 238 | #!/bin/bash 239 | rpm -q httpd &> /dev/null 240 | if [ $? -ne 0 ] 241 | then 242 | sudo amazon-linux-extras install epel -y 243 | sudo yum install stress -y 244 | fi 245 | 246 | rpm -q httpd &> /dev/null 247 | if [ $? -ne 0 ] 248 | then 249 | echo "Application is not installed, install and start it." 250 | INSTANCE_ID="`wget -q -O - http://instance-data/latest/meta-data/instance-id`" && \ 251 | sudo yum -y install httpd && \ 252 | sudo service httpd start && \ 253 | echo "Sleeping for 120 seconds to simulate additional configuration time." && \ 254 | sleep 120 && \ 255 | aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $INSTANCE_ID --lifecycle-hook-name "${lifecycleHookName}" --auto-scaling-group-name "${autoScalingGroupName}" --region ${AWS::Region} || \ 256 | aws autoscaling complete-lifecycle-action --lifecycle-action-result ABANDON --instance-id $INSTANCE_ID --lifecycle-hook-name "${lifecycleHookName}" --auto-scaling-group-name "${autoScalingGroupName}" --region ${AWS::Region} 257 | else 258 | echo "Application is installed, start it." 259 | INSTANCE_ID="`wget -q -O - http://instance-data/latest/meta-data/instance-id`" && \ 260 | sudo service httpd start && \ 261 | aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $INSTANCE_ID --lifecycle-hook-name "${lifecycleHookName}" --auto-scaling-group-name "${autoScalingGroupName}" --region ${AWS::Region} || \ 262 | aws autoscaling complete-lifecycle-action --lifecycle-action-result ABANDON --instance-id $INSTANCE_ID --lifecycle-hook-name "${lifecycleHookName}" --auto-scaling-group-name "${autoScalingGroupName}" --region ${AWS::Region} 263 | fi 264 | --// 265 | - { 266 | autoScalingGroupName: !Ref AutoScalingGroupName, 267 | lifecycleHookName: !Ref LifecycleHookName 268 | } 269 | 270 | InstanceRole: 271 | Type: "AWS::IAM::Role" 272 | Properties: 273 | Policies: 274 | - 275 | PolicyName: "CompleteLifecycleActionAllowPolicy" 276 | PolicyDocument: 277 | Version: "2012-10-17" 278 | Statement: 279 | - 280 | Effect: "Allow" 281 | Action: "autoscaling:CompleteLifecycleAction" 282 | Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AutoScalingGroupName}" 283 | AssumeRolePolicyDocument: 284 | Version: "2012-10-17" 285 | Statement: 286 | - 287 | Effect: "Allow" 288 | Principal: 289 | Service: 290 | - "ec2.amazonaws.com" 291 | Action: 292 | - "sts:AssumeRole" 293 | Path: "/" 294 | ManagedPolicyArns: 295 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 296 | 297 | InstanceProfile: 298 | Type: "AWS::IAM::InstanceProfile" 299 | Properties: 300 | Path: "/" 301 | Roles: 302 | - 303 | Ref: "InstanceRole" 304 | 305 | AutoScalingGroup: 306 | Type: AWS::AutoScaling::AutoScalingGroup 307 | Properties: 308 | AutoScalingGroupName: !Ref AutoScalingGroupName 309 | LaunchTemplate: 310 | LaunchTemplateId: !Ref LaunchTemplate 311 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 312 | DesiredCapacity: !Ref AutoScalingGroupDesiredCapacity 313 | MaxSize: !Ref AutoScalingGroupMaxSize 314 | MinSize: !Ref AutoScalingGroupMinSize 315 | VPCZoneIdentifier: 316 | - !Ref AvailabilityZoneSubnet1 317 | - !Ref AvailabilityZoneSubnet2 318 | - !Ref AvailabilityZoneSubnet3 319 | 320 | LifecycleHook: 321 | Type: AWS::AutoScaling::LifecycleHook 322 | Properties: 323 | LifecycleHookName: !Ref LifecycleHookName 324 | AutoScalingGroupName: !Ref AutoScalingGroup 325 | DefaultResult: ABANDON 326 | HeartbeatTimeout: 900 327 | LifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING" -------------------------------------------------------------------------------- /features/lifecycle-hooks/userdata-managed-windows-multi-reboot/README.md: -------------------------------------------------------------------------------- 1 | # Auto Scaling Group Lifecycle Hooks Example - User Data Managed Windows Multi Reboot 2 | 3 | This example solution deploys an Auto Scaling group within a VPC. A lifecycle hook is enabled for the Auto Scaling group and a userdata executeds during instance startup. The userdata script installs an application if it's not installed and completes the lifecycle hook action. If the application is already installed, the userdata script starts the appliction and completes the lifecycle hook action. The userdata script is configured to execute during every instance startup, so bootstrap actions can complete when the instance is initially launched and when restarted from a stopped state. This example demonstrates how you could handle a scenario where multiple reboots were required to install and configure an application before bringing it into service. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 8 | 9 | ### Prerequisites 10 | 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 12 | 13 | ## Deployment Steps 14 | 15 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 16 | 17 | 1. Change directories to this example. 18 | 19 | ```bash 20 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/lifecycle-hooks/userdata-managed-windows-multi-reboot 21 | ``` 22 | 23 | 2. Deploy the CloudFormation Stack. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 24 | 25 | ```bash 26 | aws cloudformation deploy \ 27 | --template-file template.yaml \ 28 | --stack-name lifecycle-hook-example \ 29 | --capabilities CAPABILITY_IAM \ 30 | --parameter-overrides \ 31 | InstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 32 | ``` 33 | 34 | ## Clean Up 35 | 36 | Delete the CloudFormation Stack 37 | 38 | ```bash 39 | aws cloudformation delete-stack --stack-name lifecycle-hook-example 40 | ``` 41 | -------------------------------------------------------------------------------- /features/lifecycle-hooks/userdata-managed-windows-multi-reboot/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | 18 | Description: > 19 | amazon-ec2-autoscaling-lifecycle-hook-userdata-windows-example 20 | 21 | Sample CloudFormation template that deploys an Auto Scaling Group with Lifecycle Hooks that are managed from User Data. 22 | 23 | Parameters: 24 | AmiId: 25 | Description: AMI Id 26 | Type: 'AWS::SSM::Parameter::Value' 27 | Default: '/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base' 28 | AutoScalingGroupName: 29 | Description: TBD 30 | Type: String 31 | Default: Example Auto Scaling Group 32 | AutoScalingGroupMinSize: 33 | Description: TBD 34 | Type: Number 35 | Default: 0 36 | AutoScalingGroupMaxSize: 37 | Description: TBD 38 | Type: Number 39 | Default: 2 40 | AutoScalingGroupDesiredCapacity: 41 | Description: TBD 42 | Type: Number 43 | Default: 0 44 | InstanceType: 45 | Description: Amazon EC2 Instance Type 46 | Type: String 47 | Default: "t2.micro" 48 | InstanceKeyPair: 49 | Description: Amazon EC2 Key Pair 50 | Type: "AWS::EC2::KeyPair::KeyName" 51 | LifecycleHookName: 52 | Description: TBD 53 | Type: String 54 | Default: "app-install-hook" 55 | VpcCIDR: 56 | Description: Please enter the IP range (CIDR notation) for this VPC 57 | Type: String 58 | Default: 10.192.0.0/16 59 | AvailabilityZone1CIDR: 60 | Description: Please enter the IP range (CIDR notation) for the app subnet in the first Availability Zone 61 | Type: String 62 | Default: 10.192.10.0/24 63 | AvailabilityZone2CIDR: 64 | Description: Please enter the IP range (CIDR notation) for the app subnet in the second Availability Zone 65 | Type: String 66 | Default: 10.192.11.0/24 67 | AvailabilityZone3CIDR: 68 | Description: Please enter the IP range (CIDR notation) for the app subnet in the third Availability Zone 69 | Type: String 70 | Default: 10.192.12.0/24 71 | Metadata: 72 | AWS::CloudFormation::Interface: 73 | ParameterGroups: 74 | - 75 | Label: 76 | default: "VPC Configuration" 77 | Parameters: 78 | - VpcCIDR 79 | - AvailabilityZone1CIDR 80 | - AvailabilityZone2CIDR 81 | - AvailabilityZone3CIDR 82 | - 83 | Label: 84 | default: "Instance Configuration" 85 | Parameters: 86 | - AmiId 87 | - InstanceType 88 | - InstanceKeyPair 89 | - 90 | Label: 91 | default: "Auto Scaling Group Configuration" 92 | Parameters: 93 | - AutoScalingGroupName 94 | - AutoScalingGroupVpcID 95 | - AutoScalingGroupSubnetIDs 96 | - AutoScalingGroupMinSize 97 | - AutoScalingGroupMaxSize 98 | - AutoScalingGroupDesiredCapacity 99 | - LifecycleHookName 100 | 101 | Resources: 102 | 103 | # VPC/Networking Resources 104 | 105 | VPC: 106 | Type: AWS::EC2::VPC 107 | Properties: 108 | CidrBlock: !Ref VpcCIDR 109 | EnableDnsHostnames: true 110 | 111 | InternetGateway: 112 | Type: AWS::EC2::InternetGateway 113 | 114 | InternetGatewayAttachment: 115 | Type: AWS::EC2::VPCGatewayAttachment 116 | Properties: 117 | InternetGatewayId: !Ref InternetGateway 118 | VpcId: !Ref VPC 119 | 120 | AvailabilityZoneSubnet1: 121 | Type: AWS::EC2::Subnet 122 | Properties: 123 | VpcId: !Ref VPC 124 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 125 | CidrBlock: !Ref AvailabilityZone1CIDR 126 | MapPublicIpOnLaunch: true 127 | 128 | AvailabilityZoneSubnet2: 129 | Type: AWS::EC2::Subnet 130 | Properties: 131 | VpcId: !Ref VPC 132 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 133 | CidrBlock: !Ref AvailabilityZone2CIDR 134 | MapPublicIpOnLaunch: true 135 | 136 | AvailabilityZoneSubnet3: 137 | Type: AWS::EC2::Subnet 138 | Properties: 139 | VpcId: !Ref VPC 140 | AvailabilityZone: !Select [ 2, !GetAZs '' ] 141 | CidrBlock: !Ref AvailabilityZone3CIDR 142 | MapPublicIpOnLaunch: true 143 | 144 | InstanceSecurityGroup: 145 | Type: AWS::EC2::SecurityGroup 146 | Properties: 147 | VpcId: !Ref VPC 148 | GroupDescription: Instance Security Group 149 | 150 | RouteTable: 151 | Type: AWS::EC2::RouteTable 152 | Properties: 153 | VpcId: !Ref VPC 154 | 155 | DefaultPublicRoute: 156 | Type: AWS::EC2::Route 157 | DependsOn: InternetGatewayAttachment 158 | Properties: 159 | RouteTableId: !Ref RouteTable 160 | DestinationCidrBlock: 0.0.0.0/0 161 | GatewayId: !Ref InternetGateway 162 | 163 | AvailabilityZoneSubnet1RouteTableAssociation: 164 | Type: AWS::EC2::SubnetRouteTableAssociation 165 | Properties: 166 | RouteTableId: !Ref RouteTable 167 | SubnetId: !Ref AvailabilityZoneSubnet1 168 | 169 | AvailabilityZoneSubnet2RouteTableAssociation: 170 | Type: AWS::EC2::SubnetRouteTableAssociation 171 | Properties: 172 | RouteTableId: !Ref RouteTable 173 | SubnetId: !Ref AvailabilityZoneSubnet2 174 | 175 | AvailabilityZoneSubnet3RouteTableAssociation: 176 | Type: AWS::EC2::SubnetRouteTableAssociation 177 | Properties: 178 | RouteTableId: !Ref RouteTable 179 | SubnetId: !Ref AvailabilityZoneSubnet3 180 | 181 | # Instance/Auto Scaling Group Resources 182 | 183 | LaunchTemplate: 184 | Type: AWS::EC2::LaunchTemplate 185 | Properties: 186 | LaunchTemplateData: 187 | ImageId: !Ref AmiId 188 | InstanceType: !Ref InstanceType 189 | KeyName: !Ref InstanceKeyPair 190 | SecurityGroupIds: 191 | - !Ref InstanceSecurityGroup 192 | IamInstanceProfile: 193 | Arn: !GetAtt 194 | - InstanceProfile 195 | - Arn 196 | TagSpecifications: 197 | - ResourceType: instance 198 | Tags: 199 | - Key: Name 200 | Value: LifecycleHookExampleInstance 201 | UserData: 202 | 'Fn::Base64': 203 | !Sub 204 | - |- 205 | 206 | # This simplified example is using files to track completion for demo purposes, but you could use something else such as tags or registry keys. 207 | $first_config = "c:\temp\first_config.txt" 208 | $second_config = "c:\temp\second_config.txt" 209 | $third_config = "c:\temp\third_config.txt" 210 | 211 | if (-not(Test-Path -Path $first_config -PathType Leaf)) { 212 | Write-Host "Starting First Configuration" 213 | try { 214 | $null = New-Item -ItemType File -Path $first_config -Force -ErrorAction Stop 215 | Write-Host "First Configuration Completed, Rebooting." 216 | Restart-Computer 217 | exit 218 | } 219 | catch { 220 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 221 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult ABANDON 222 | exit 223 | } 224 | } 225 | elseif (-not(Test-Path -Path $second_config -PathType Leaf)) { 226 | Write-Host "Starting Second Configuration" 227 | try { 228 | $null = New-Item -ItemType File -Path $second_config -Force -ErrorAction Stop 229 | Write-Host "Second Configuration Completed, Rebooting." 230 | Restart-Computer 231 | exit 232 | } 233 | catch { 234 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 235 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult ABANDON 236 | exit 237 | } 238 | } 239 | elseif (-not(Test-Path -Path $third_config -PathType Leaf)) { 240 | Write-Host "Starting Third Configuration" 241 | try { 242 | $null = New-Item -ItemType File -Path $third_config -Force -ErrorAction Stop 243 | Write-Host "Third Configuration Completed, System Configured." 244 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 245 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult CONTINUE 246 | exit 247 | } 248 | catch { 249 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 250 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult ABANDON 251 | exit 252 | } 253 | } 254 | else 255 | { 256 | Write-Host "System already configured." 257 | } 258 | 259 | true 260 | - { 261 | autoScalingGroupName: !Ref AutoScalingGroupName, 262 | lifecycleHookName: !Ref LifecycleHookName 263 | } 264 | 265 | InstanceRole: 266 | Type: "AWS::IAM::Role" 267 | Properties: 268 | Policies: 269 | - 270 | PolicyName: "CompleteLifecycleActionAllowPolicy" 271 | PolicyDocument: 272 | Version: "2012-10-17" 273 | Statement: 274 | - 275 | Effect: "Allow" 276 | Action: "autoscaling:CompleteLifecycleAction" 277 | Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AutoScalingGroupName}" 278 | AssumeRolePolicyDocument: 279 | Version: "2012-10-17" 280 | Statement: 281 | - 282 | Effect: "Allow" 283 | Principal: 284 | Service: 285 | - "ec2.amazonaws.com" 286 | Action: 287 | - "sts:AssumeRole" 288 | Path: "/" 289 | ManagedPolicyArns: 290 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 291 | 292 | InstanceProfile: 293 | Type: "AWS::IAM::InstanceProfile" 294 | Properties: 295 | Path: "/" 296 | Roles: 297 | - 298 | Ref: "InstanceRole" 299 | 300 | AutoScalingGroup: 301 | Type: AWS::AutoScaling::AutoScalingGroup 302 | Properties: 303 | AutoScalingGroupName: !Ref AutoScalingGroupName 304 | LaunchTemplate: 305 | LaunchTemplateId: !Ref LaunchTemplate 306 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 307 | DesiredCapacity: !Ref AutoScalingGroupDesiredCapacity 308 | MaxSize: !Ref AutoScalingGroupMaxSize 309 | MinSize: !Ref AutoScalingGroupMinSize 310 | VPCZoneIdentifier: 311 | - !Ref AvailabilityZoneSubnet1 312 | - !Ref AvailabilityZoneSubnet2 313 | - !Ref AvailabilityZoneSubnet3 314 | 315 | LifecycleHook: 316 | Type: AWS::AutoScaling::LifecycleHook 317 | Properties: 318 | LifecycleHookName: !Ref LifecycleHookName 319 | AutoScalingGroupName: !Ref AutoScalingGroup 320 | DefaultResult: ABANDON 321 | HeartbeatTimeout: 900 322 | LifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING" -------------------------------------------------------------------------------- /features/lifecycle-hooks/userdata-managed-windows/README.md: -------------------------------------------------------------------------------- 1 | # Auto Scaling Group Lifecycle Hooks Example - User Data Managed Windows 2 | 3 | This example solution deploys an Auto Scaling group within a VPC. A lifecycle hook is enabled for the Auto Scaling group and a userdata executeds during instance startup. The userdata script installs an application if it's not installed and completes the lifecycle hook action. If the application is already installed, the userdata script starts the appliction and completes the lifecycle hook action. The userdata script is configured to execute during every instance startup, so bootstrap actions can complete when the instance is initially launched and when restarted from a stopped state. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 8 | 9 | ### Prerequisites 10 | 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 12 | 13 | ## Deployment Steps 14 | 15 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 16 | 17 | 1. Change directories to this example. 18 | 19 | ```bash 20 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/lifecycle-hooks/userdata-managed-windows 21 | ``` 22 | 23 | 2. Deploy the CloudFormation Stack. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 24 | 25 | ```bash 26 | aws cloudformation deploy \ 27 | --template-file template.yaml \ 28 | --stack-name lifecycle-hook-example \ 29 | --capabilities CAPABILITY_IAM \ 30 | --parameter-overrides \ 31 | InstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 32 | ``` 33 | 34 | ## Clean Up 35 | 36 | Delete the CloudFormation Stack 37 | 38 | ```bash 39 | aws cloudformation delete-stack --stack-name lifecycle-hook-example 40 | ``` -------------------------------------------------------------------------------- /features/lifecycle-hooks/userdata-managed-windows/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | AWSTemplateFormatVersion: '2010-09-09' 17 | 18 | Description: > 19 | amazon-ec2-autoscaling-lifecycle-hook-userdata-windows-example 20 | 21 | Sample CloudFormation template that deploys an Auto Scaling Group with Lifecycle Hooks that are managed from User Data. 22 | 23 | Parameters: 24 | AmiId: 25 | Description: AMI Id 26 | Type: 'AWS::SSM::Parameter::Value' 27 | Default: '/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base' 28 | AutoScalingGroupName: 29 | Description: TBD 30 | Type: String 31 | Default: Example Auto Scaling Group 32 | AutoScalingGroupMinSize: 33 | Description: TBD 34 | Type: Number 35 | Default: 0 36 | AutoScalingGroupMaxSize: 37 | Description: TBD 38 | Type: Number 39 | Default: 2 40 | AutoScalingGroupDesiredCapacity: 41 | Description: TBD 42 | Type: Number 43 | Default: 0 44 | InstanceType: 45 | Description: Amazon EC2 Instance Type 46 | Type: String 47 | Default: "t2.micro" 48 | InstanceKeyPair: 49 | Description: Amazon EC2 Key Pair 50 | Type: "AWS::EC2::KeyPair::KeyName" 51 | LifecycleHookName: 52 | Description: TBD 53 | Type: String 54 | Default: "app-install-hook" 55 | VpcCIDR: 56 | Description: Please enter the IP range (CIDR notation) for this VPC 57 | Type: String 58 | Default: 10.192.0.0/16 59 | AvailabilityZone1CIDR: 60 | Description: Please enter the IP range (CIDR notation) for the app subnet in the first Availability Zone 61 | Type: String 62 | Default: 10.192.10.0/24 63 | AvailabilityZone2CIDR: 64 | Description: Please enter the IP range (CIDR notation) for the app subnet in the second Availability Zone 65 | Type: String 66 | Default: 10.192.11.0/24 67 | AvailabilityZone3CIDR: 68 | Description: Please enter the IP range (CIDR notation) for the app subnet in the third Availability Zone 69 | Type: String 70 | Default: 10.192.12.0/24 71 | Metadata: 72 | AWS::CloudFormation::Interface: 73 | ParameterGroups: 74 | - 75 | Label: 76 | default: "VPC Configuration" 77 | Parameters: 78 | - VpcCIDR 79 | - AvailabilityZone1CIDR 80 | - AvailabilityZone2CIDR 81 | - AvailabilityZone3CIDR 82 | - 83 | Label: 84 | default: "Instance Configuration" 85 | Parameters: 86 | - AmiId 87 | - InstanceType 88 | - InstanceKeyPair 89 | - 90 | Label: 91 | default: "Auto Scaling Group Configuration" 92 | Parameters: 93 | - AutoScalingGroupName 94 | - AutoScalingGroupVpcID 95 | - AutoScalingGroupSubnetIDs 96 | - AutoScalingGroupMinSize 97 | - AutoScalingGroupMaxSize 98 | - AutoScalingGroupDesiredCapacity 99 | - LifecycleHookName 100 | 101 | Resources: 102 | 103 | # VPC/Networking Resources 104 | 105 | VPC: 106 | Type: AWS::EC2::VPC 107 | Properties: 108 | CidrBlock: !Ref VpcCIDR 109 | EnableDnsHostnames: true 110 | 111 | InternetGateway: 112 | Type: AWS::EC2::InternetGateway 113 | 114 | InternetGatewayAttachment: 115 | Type: AWS::EC2::VPCGatewayAttachment 116 | Properties: 117 | InternetGatewayId: !Ref InternetGateway 118 | VpcId: !Ref VPC 119 | 120 | AvailabilityZoneSubnet1: 121 | Type: AWS::EC2::Subnet 122 | Properties: 123 | VpcId: !Ref VPC 124 | AvailabilityZone: !Select [ 0, !GetAZs '' ] 125 | CidrBlock: !Ref AvailabilityZone1CIDR 126 | MapPublicIpOnLaunch: true 127 | 128 | AvailabilityZoneSubnet2: 129 | Type: AWS::EC2::Subnet 130 | Properties: 131 | VpcId: !Ref VPC 132 | AvailabilityZone: !Select [ 1, !GetAZs '' ] 133 | CidrBlock: !Ref AvailabilityZone2CIDR 134 | MapPublicIpOnLaunch: true 135 | 136 | AvailabilityZoneSubnet3: 137 | Type: AWS::EC2::Subnet 138 | Properties: 139 | VpcId: !Ref VPC 140 | AvailabilityZone: !Select [ 2, !GetAZs '' ] 141 | CidrBlock: !Ref AvailabilityZone3CIDR 142 | MapPublicIpOnLaunch: true 143 | 144 | InstanceSecurityGroup: 145 | Type: AWS::EC2::SecurityGroup 146 | Properties: 147 | VpcId: !Ref VPC 148 | GroupDescription: Instance Security Group 149 | 150 | RouteTable: 151 | Type: AWS::EC2::RouteTable 152 | Properties: 153 | VpcId: !Ref VPC 154 | 155 | DefaultPublicRoute: 156 | Type: AWS::EC2::Route 157 | DependsOn: InternetGatewayAttachment 158 | Properties: 159 | RouteTableId: !Ref RouteTable 160 | DestinationCidrBlock: 0.0.0.0/0 161 | GatewayId: !Ref InternetGateway 162 | 163 | AvailabilityZoneSubnet1RouteTableAssociation: 164 | Type: AWS::EC2::SubnetRouteTableAssociation 165 | Properties: 166 | RouteTableId: !Ref RouteTable 167 | SubnetId: !Ref AvailabilityZoneSubnet1 168 | 169 | AvailabilityZoneSubnet2RouteTableAssociation: 170 | Type: AWS::EC2::SubnetRouteTableAssociation 171 | Properties: 172 | RouteTableId: !Ref RouteTable 173 | SubnetId: !Ref AvailabilityZoneSubnet2 174 | 175 | AvailabilityZoneSubnet3RouteTableAssociation: 176 | Type: AWS::EC2::SubnetRouteTableAssociation 177 | Properties: 178 | RouteTableId: !Ref RouteTable 179 | SubnetId: !Ref AvailabilityZoneSubnet3 180 | 181 | # Instance/Auto Scaling Group Resources 182 | 183 | LaunchTemplate: 184 | Type: AWS::EC2::LaunchTemplate 185 | Properties: 186 | LaunchTemplateData: 187 | ImageId: !Ref AmiId 188 | InstanceType: !Ref InstanceType 189 | KeyName: !Ref InstanceKeyPair 190 | SecurityGroupIds: 191 | - !Ref InstanceSecurityGroup 192 | IamInstanceProfile: 193 | Arn: !GetAtt 194 | - InstanceProfile 195 | - Arn 196 | TagSpecifications: 197 | - ResourceType: instance 198 | Tags: 199 | - Key: Name 200 | Value: LifecycleHookExampleInstance 201 | UserData: 202 | 'Fn::Base64': 203 | !Sub 204 | - |- 205 | 206 | if ((Get-WindowsFeature Web-Server).InstallState -eq "Installed") { 207 | Write-Host "IIS is installed, stopping and starting IIS" 208 | try 209 | { 210 | Start-Process "iisreset.exe" -NoNewWindow -Wait 211 | Write-Host "Completing Lifecycle Action with CONTINUE" 212 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 213 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult CONTINUE 214 | } 215 | catch 216 | { 217 | Write-Output "An error occured, Completing Lifecycle Action with ABANDON." 218 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 219 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult ABANDON 220 | } 221 | } 222 | else { 223 | Write-Host "IIS is not installed, installing, stopping and starting IIS." 224 | try 225 | { 226 | Install-WindowsFeature -name Web-Server -IncludeManagementTools 227 | Start-Process "iisreset.exe" -NoNewWindow -Wait 228 | Write-Host "Sleeping for 120 Seconds to Simulate IIS Configuration." 229 | Start-Sleep -s 120 230 | Write-Host "Completing Lifecycle Action with CONTINUE" 231 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 232 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult CONTINUE 233 | } 234 | catch 235 | { 236 | Write-Output "An error occured, Completing Lifecycle Action with ABANDON." 237 | $INSTANCE = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id") 238 | Complete-ASLifecycleAction -InstanceId $INSTANCE -LifecycleHookName "${lifecycleHookName}" -AutoScalingGroupName "${autoScalingGroupName}" -LifecycleActionResult ABANDON 239 | } 240 | } 241 | 242 | true 243 | - { 244 | autoScalingGroupName: !Ref AutoScalingGroupName, 245 | lifecycleHookName: !Ref LifecycleHookName 246 | } 247 | 248 | InstanceRole: 249 | Type: "AWS::IAM::Role" 250 | Properties: 251 | Policies: 252 | - 253 | PolicyName: "CompleteLifecycleActionAllowPolicy" 254 | PolicyDocument: 255 | Version: "2012-10-17" 256 | Statement: 257 | - 258 | Effect: "Allow" 259 | Action: "autoscaling:CompleteLifecycleAction" 260 | Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AutoScalingGroupName}" 261 | AssumeRolePolicyDocument: 262 | Version: "2012-10-17" 263 | Statement: 264 | - 265 | Effect: "Allow" 266 | Principal: 267 | Service: 268 | - "ec2.amazonaws.com" 269 | Action: 270 | - "sts:AssumeRole" 271 | Path: "/" 272 | ManagedPolicyArns: 273 | - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 274 | 275 | InstanceProfile: 276 | Type: "AWS::IAM::InstanceProfile" 277 | Properties: 278 | Path: "/" 279 | Roles: 280 | - 281 | Ref: "InstanceRole" 282 | 283 | AutoScalingGroup: 284 | Type: AWS::AutoScaling::AutoScalingGroup 285 | Properties: 286 | AutoScalingGroupName: !Ref AutoScalingGroupName 287 | LaunchTemplate: 288 | LaunchTemplateId: !Ref LaunchTemplate 289 | Version: !GetAtt LaunchTemplate.LatestVersionNumber 290 | DesiredCapacity: !Ref AutoScalingGroupDesiredCapacity 291 | MaxSize: !Ref AutoScalingGroupMaxSize 292 | MinSize: !Ref AutoScalingGroupMinSize 293 | VPCZoneIdentifier: 294 | - !Ref AvailabilityZoneSubnet1 295 | - !Ref AvailabilityZoneSubnet2 296 | - !Ref AvailabilityZoneSubnet3 297 | 298 | LifecycleHook: 299 | Type: AWS::AutoScaling::LifecycleHook 300 | Properties: 301 | LifecycleHookName: !Ref LifecycleHookName 302 | AutoScalingGroupName: !Ref AutoScalingGroup 303 | DefaultResult: ABANDON 304 | HeartbeatTimeout: 900 305 | LifecycleTransition: "autoscaling:EC2_INSTANCE_LAUNCHING" -------------------------------------------------------------------------------- /features/predictive-scaling-blue-green-deployment/README.md: -------------------------------------------------------------------------------- 1 | # Auto Scaling Group Predictive Scaling Example for Blue/Green Deployment 2 | 3 | This example deploys three Auto Scaling groups within a new VPC. Two of the Auto Scaling groups contains instances that host a simple web application behind an Application Load Balancer. The third Auto Scaling group contains instances that generate recurring load against the Application Load Balancer for one of the application running Auto Scaling groups. This stack will launch billable resources, so please keep in mind that you will be charged for any usage. You can adjust the instance types and number of instances deployed via the stack's CloudFormation parameters to reduce costs as needed. 4 | 5 | We recommend following [this blog post](https://aws.amazon.com/blogs/compute/retaining-metrics-across-blue-green-deployment-for-predictive-scaling/) for an example walk-through using this stack with Predictive Scaling. 6 | 7 | 8 | ## Getting Started 9 | 10 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 11 | 12 | ### Prerequisites 13 | 14 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 15 | 16 | ## Deployment Steps 17 | 18 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 19 | 20 | 1. Change directories to this example. 21 | 22 | ```bash 23 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/predictive-scaling-blue-green-deployment 24 | ``` 25 | 26 | 2. Deploy the CloudFormation Stack. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 27 | 28 | ```bash 29 | aws cloudformation deploy \ 30 | --template-file template.yaml \ 31 | --stack-name preditive-scaling-example \ 32 | --capabilities CAPABILITY_IAM \ 33 | --parameter-overrides \ 34 | ApplicationInstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME \ 35 | LoadInstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 36 | ``` 37 | 38 | ## Clean Up 39 | 40 | Delete the CloudFormation Stack 41 | 42 | ```bash 43 | aws cloudformation delete-stack --stack-name preditive-scaling-example 44 | ``` -------------------------------------------------------------------------------- /features/predictive-scaling-blue-green-deployment/predictive-scaling-policy-cpu.json: -------------------------------------------------------------------------------- 1 | { 2 | "MetricSpecifications": [ 3 | { 4 | "TargetValue": 25, 5 | "CustomizedScalingMetricSpecification": { 6 | "MetricDataQueries": [ 7 | { 8 | "Id": "load_sum", 9 | "Expression": "SUM(SEARCH('{AWS/EC2,AutoScalingGroupName} MetricName=\"CPUUtilization\" ASG-myapp', 'Sum', 300))", 10 | "ReturnData": false 11 | }, 12 | { 13 | "Id": "capacity_sum", 14 | "Expression": "SUM(SEARCH('{AWS/AutoScaling,AutoScalingGroupName} MetricName=\"GroupInServiceInstances\" ASG-myapp', 'Average', 300))", 15 | "ReturnData": false 16 | }, 17 | { 18 | "Id": "weighted_average", 19 | "Expression": "load_sum / capacity_sum" 20 | } 21 | ] 22 | }, 23 | "CustomizedLoadMetricSpecification": { 24 | "MetricDataQueries": [ 25 | { 26 | "Id": "load_sum", 27 | "Expression": "SUM(SEARCH('{AWS/EC2,AutoScalingGroupName} MetricName=\"CPUUtilization\" ASG-myapp', 'Sum', 3600))" 28 | } 29 | ] 30 | }, 31 | "CustomizedCapacityMetricSpecification": { 32 | "MetricDataQueries": [ 33 | { 34 | "Id": "capacity_sum", 35 | "Expression": "SUM(SEARCH('{AWS/AutoScaling,AutoScalingGroupName} MetricName=\"GroupInServiceInstances\" ASG-myapp', 'Average', 300))" 36 | } 37 | ] 38 | } 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /features/predictive-scaling/README.md: -------------------------------------------------------------------------------- 1 | # Auto Scaling Group Predictive Scaling Example 2 | 3 | This example deploys two Auto Scaling groups within a new VPC. One of the Auto Scaling groups contains instances that host a simple web application behind an Application Load Balancer. The second Auto Scaling group contains instances that generate recurring load against the Application Load Balancer. This stack will launch billable resources, so please keep in mind that you will be charged for any usage. You can adjust the instance types and number of instances deployed via the stack's CloudFormation parameters to reduce costs as needed. 4 | 5 | We recommend following [this blog post](https://aws.amazon.com/blogs/compute/introducing-native-support-for-predictive-scaling-with-amazon-ec2-auto-scaling/) for an example walk-through using this stack with Predictve Scaling. 6 | 7 | ## Getting Started 8 | 9 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 10 | 11 | ### Prerequisites 12 | 13 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 14 | 15 | ## Deployment Steps 16 | 17 | Once you've deployed and accessed the [Example AWS Cloud9 Environment](/environment/README.md) execute the following steps from within the Example AWS Cloud9 Environment to deploy this example. 18 | 19 | 1. Change directories to this example. 20 | 21 | ```bash 22 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/predictive-scaling 23 | ``` 24 | 25 | 2. Deploy the CloudFormation Stack. You will need to replace `REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME` with the name of an SSH key in the region you are deploying the example to. 26 | 27 | ```bash 28 | aws cloudformation deploy \ 29 | --template-file template.yaml \ 30 | --stack-name preditive-scaling-example \ 31 | --capabilities CAPABILITY_IAM \ 32 | --parameter-overrides \ 33 | ApplicationInstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME \ 34 | LoadInstanceKeyPair=REPLACE_THIS_WITH_YOUR_KEY_PAIR_NAME 35 | ``` 36 | 37 | ## Clean Up 38 | 39 | Delete the CloudFormation Stack 40 | 41 | ```bash 42 | aws cloudformation delete-stack --stack-name preditive-scaling-example 43 | ``` -------------------------------------------------------------------------------- /features/warm-pools/README.md: -------------------------------------------------------------------------------- 1 | # Warm Pools 2 | 3 | This example walks you through configuring Warm Pools for an Auto Scaling Group and measuring the launch time when launching pre-initialized instances from a Warm Pool as compared to launching instances directly into the Auto Scaling group and completing bootstrapping actions. 4 | 5 | ## Getting Started 6 | 7 | We recommend deploying the following [Example AWS Cloud9 Environment](/environment/README.md) to get started quickly with this example. Otherwise, you can attempt to run this example using your own environment with the following prerequisites installed. 8 | 9 | ### Prerequisites 10 | 11 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with Administrator credentials. 12 | * [Python 3 installed](https://www.python.org/downloads/) 13 | * [Docker installed](https://www.docker.com/community-edition) 14 | * [SAM CLI installed](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 15 | * [jq](https://stedolan.github.io/jq/download/) 16 | * [dateutils](http://www.fresse.org/dateutils/) 17 | 18 | ## Deploy Example Auto Saling Group CloudFormation Template 19 | 20 | This example requires that an Auto Scaling group has been configured within the account you are running the example in. This example works best if this Auto Scaling group is configured with lifecycle hooks to manage the lifecycle of your instances. A common example is using life cycle Hooks to install and start an application prior to the instance being brought in service. You can use one of the following CloudFormation templates to deploy an example Auto Scaling group for use with this walk through. 21 | 22 | ### Auto Scaling group w/ Life Cycle Hooks controlled via Userdata 23 | 24 | * [Deploy the sample CloudFormation template](../lifecycle-hooks/userdata-managed-linux/README.md) 25 | 26 | ### Auto Scaling group w/ Life Cycle Hooks controlled via a Lambda Function 27 | 28 | * [Deploy the sample CloudFormation template](../lifecycle-hooks/lambda-managed-linux/README.md) 29 | 30 | ## Activity 1: Measure the Launch Speed of Instances Launched Directly into an Auto Scaling Group 31 | 32 | With our Auto Scaling group deployed, and CLI utilities installed, we can begin our first activity. In this activity we will launch an instance directly into an Auto Scaling group. The example Auto Scaling groups deployed earlier use lifecycle hooks to manage the application installation process. 33 | 34 | The userdata managed example uses a script that execute on the instance when the instance first boots, and every time the instance starts. This script detects if the application is installed, and if not, installs and starts it. If the application is already installed it ensures that it's started. Once the application is installed or started, a command is executed to complete the lifecycle action and allow the instance to transtion to the next lifecycle step. 35 | 36 | The lambda managed example uses a Lambda function that executes in response to Amazon EventBridge events that are generated as instances transition through their lifecycle. The Lambda function can perform different actions as the instance is first launched, launched into a warm pool, or started from a warm pool. This allows the Lambda function to perform actions such as installing an application, registering an instance with a primary node, or ensuring that an application is started prior to the instance being moved in-service. 37 | 38 | ### Prerequisite 39 | 40 | Change directories to this example. 41 | 42 | ```bash 43 | cd ~/environment/amazon-ec2-auto-scaling-group-examples/features/warm-pools 44 | ``` 45 | 46 | ### Step 1: Increase Desired Capacity 47 | 48 | Set the desired capacity of the Auto Scaling group to 1 to launch an instance directly into the Auto Scaling group. 49 | 50 | ``` 51 | aws autoscaling set-desired-capacity --auto-scaling-group-name "Example Auto Scaling Group" --desired-capacity 1 52 | ``` 53 | 54 | ### Step 2: Measure Launch Speed 55 | 56 | Now, let's measure the launch speed of the instance. You will need to wait a few minutes for the instance to be launched by the previous step. 57 | 58 | ``` 59 | activities=$(aws autoscaling describe-scaling-activities --auto-scaling-group-name "Example Auto Scaling Group") 60 | for row in $(echo "${activities}" | jq -r '.Activities[] | @base64'); do 61 | _jq() { 62 | echo ${row} | base64 --decode | jq -r ${1} 63 | } 64 | 65 | start_time=$(_jq '.StartTime') 66 | end_time=$(_jq '.EndTime') 67 | activity=$(_jq '.Description') 68 | 69 | echo $activity Duration: $(datediff $start_time $end_time) 70 | done 71 | ``` 72 | 73 | ### Step 3: Observe Launch Duration 74 | 75 | Because the instance launched directly into the Auto Scaling group, all initialization actions needed to complete to prepare the instance to be placed in-service. From the results below we can see that these actions took a long time to complete, delaying how quickly our Auto Scaling group can scale. 76 | 77 | ``` 78 | Launching a new EC2 instance: i-075fa0ad6a018cdfc Duration: 243s 79 | ``` 80 | 81 | ## Activity 2: Enable Warm Pools for the Auto Scaling Group 82 | 83 | Let's add a Warm Pool to our Auto Scaling group so we can pre-initialize our instances so that they can be brought into service more rapidly. 84 | 85 | ### Step 1: Put Warm Pool Configuration 86 | 87 | We can add a Warm Pool to our Auto Scaling group with a PutWarmPool API call. We will keep our Warm Pool instances in a stopped state after they have completed their initialization actions. We will omit the optional Warm Pool sizing parameters (--min-size and --max-group-prepared-capacity) meaning our Warm Pool will have a minimum size of 0 and a maximum repared capacity equal to the max size of the Auto Scaling group. The maximum prepared capacity will include instances launched into the Auto Scaling group, and instances launched into the Warm Pool. If you deployed one of the example Auto Scaling groups, this will be set to 2 as a default. 88 | 89 | ``` 90 | aws autoscaling put-warm-pool --auto-scaling-group-name "Example Auto Scaling Group" --pool-state Stopped 91 | ``` 92 | 93 | ### Step 2: Describe Warm Pool Configuration 94 | 95 | By using a DescribeWarmPool API call, we can now see that one instance was launched into our Warm Pool. This is because our Warm Pool's maximum prepared capacity is equal to the Auto Scaling group max size. Since we have one instance already in service, only one additional instance was launched into the Warm Pool to equal the maximum prepared capacity of 2. 96 | 97 | ``` 98 | aws autoscaling describe-warm-pool --auto-scaling-group-name "Example Auto Scaling Group" 99 | ``` 100 | 101 | When an instance is launched into a Warm Pool it will transition through lifecycle states, with Warmed:Pending. 102 | 103 | ``` 104 | { 105 | "WarmPoolConfiguration": { 106 | "MinSize": 0, 107 | "PoolState": "Stopped" 108 | }, 109 | "Instances": [ 110 | { 111 | "InstanceId": "i-0ea10fdc59a07df6e", 112 | "InstanceType": "t2.micro", 113 | "AvailabilityZone": "us-west-2a", 114 | "LifecycleState": "Warmed:Pending", 115 | "HealthStatus": "Healthy", 116 | "LaunchTemplate": { 117 | "LaunchTemplateId": "lt-0356f1c452b0eb0eb", 118 | "LaunchTemplateName": "LaunchTemplate_O7hvkiPu9hmf", 119 | "Version": "1" 120 | } 121 | } 122 | ] 123 | } 124 | ``` 125 | 126 | If a lifecycle hook is configured, the instance can wait in a Warmed:Pending:Wait state until initialization actions are completed. 127 | 128 | ``` 129 | { 130 | "WarmPoolConfiguration": { 131 | "MinSize": 0, 132 | "PoolState": "Stopped" 133 | }, 134 | "Instances": [ 135 | { 136 | "InstanceId": "i-0ea10fdc59a07df6e", 137 | "InstanceType": "t2.micro", 138 | "AvailabilityZone": "us-west-2a", 139 | "LifecycleState": "Warmed:Pending:Wait", 140 | "HealthStatus": "Healthy", 141 | "LaunchTemplate": { 142 | "LaunchTemplateId": "lt-0356f1c452b0eb0eb", 143 | "LaunchTemplateName": "LaunchTemplate_O7hvkiPu9hmf", 144 | "Version": "1" 145 | } 146 | } 147 | ] 148 | } 149 | ``` 150 | 151 | After initialization actions are completed, and the lifecycle hook is sent a CONTINUE signal, the instance will move to a Warmed:Pending:Proceed state. 152 | 153 | ``` 154 | { 155 | "WarmPoolConfiguration": { 156 | "MinSize": 0, 157 | "PoolState": "Stopped" 158 | }, 159 | "Instances": [ 160 | { 161 | "InstanceId": "i-0ea10fdc59a07df6e", 162 | "InstanceType": "t2.micro", 163 | "AvailabilityZone": "us-west-2a", 164 | "LifecycleState": "Warmed:Pending:Proceed", 165 | "HealthStatus": "Healthy", 166 | "LaunchTemplate": { 167 | "LaunchTemplateId": "lt-0356f1c452b0eb0eb", 168 | "LaunchTemplateName": "LaunchTemplate_O7hvkiPu9hmf", 169 | "Version": "1" 170 | } 171 | } 172 | ] 173 | } 174 | ``` 175 | 176 | Since we configured instances in our Warm Pool to be stopped after initialization, the instance launch will complete with the instance in a Warmed:Stopped state. The instance is now pre-initialized and ready to be launched into the Auto Scaling group as additional capacity is needed. 177 | 178 | ``` 179 | { 180 | "WarmPoolConfiguration": { 181 | "MinSize": 0, 182 | "PoolState": "Stopped" 183 | }, 184 | "Instances": [ 185 | { 186 | "InstanceId": "i-0ea10fdc59a07df6e", 187 | "InstanceType": "t2.micro", 188 | "AvailabilityZone": "us-west-2a", 189 | "LifecycleState": "Warmed:Stopped", 190 | "HealthStatus": "Healthy", 191 | "LaunchTemplate": { 192 | "LaunchTemplateId": "lt-0356f1c452b0eb0eb", 193 | "LaunchTemplateName": "LaunchTemplate_O7hvkiPu9hmf", 194 | "Version": "1" 195 | } 196 | } 197 | ] 198 | } 199 | ``` 200 | 201 | ### Observe Launch Speed into Warm Pool 202 | 203 | Now let's see how long it took to launch the instance into the Warm Pool. 204 | 205 | ``` 206 | activities=$(aws autoscaling describe-scaling-activities --auto-scaling-group-name "Example Auto Scaling Group") 207 | for row in $(echo "${activities}" | jq -r '.Activities[] | @base64'); do 208 | _jq() { 209 | echo ${row} | base64 --decode | jq -r ${1} 210 | } 211 | 212 | start_time=$(_jq '.StartTime') 213 | end_time=$(_jq '.EndTime') 214 | activity=$(_jq '.Description') 215 | 216 | echo $activity Duration: $(datediff $start_time $end_time) 217 | done 218 | ``` 219 | 220 | As you can see from the following results, launching an instance into a Warm Pool took a similar length of time to launching an instance directly into the Auto Scaling group. 221 | 222 | ``` 223 | Launching a new EC2 instance into warm pool: i-0ea10fdc59a07df6e Duration: 260s 224 | ``` 225 | 226 | ## Activity 3: Measure the Launch Speed of Instances Launched From Warm Pool into an Auto Scaling group 227 | 228 | Now that we have pre-initialized instance in the Warm Pool, we can scale our Auto Scaling group and launch the pre-initialized instance rather than launching a new instance that has not been pre-initialized. 229 | 230 | ### Step 1: Increase Desired Capacity 231 | 232 | Let's increase the desired capacity of our Auto Scaling group to 2. 233 | 234 | ``` 235 | aws autoscaling set-desired-capacity --auto-scaling-group-name "Example Auto Scaling Group" --desired-capacity 2 236 | ``` 237 | 238 | ### Step 2: Observe Warm Pool Change 239 | 240 | Now, let's describe our Warm Pool and observe any changes. As you can see below, the instance we previously launched is no longer in our Warm Pool. This is beause it was launched from the Warm Pool, into the Auto Scaling group in response to our increase in desired capacity. 241 | 242 | ``` 243 | aws autoscaling describe-warm-pool --auto-scaling-group-name "Example Auto Scaling Group" 244 | ``` 245 | 246 | ``` 247 | { 248 | "WarmPoolConfiguration": { 249 | "MinSize": 0, 250 | "PoolState": "Stopped" 251 | }, 252 | "Instances": [] 253 | } 254 | ``` 255 | 256 | ### Step 3: Measure Launch Speed 257 | 258 | We can now measure the launch speed of the instance from the Warm Pool to the Auto Scaling group. 259 | 260 | ``` 261 | activities=$(aws autoscaling describe-scaling-activities --auto-scaling-group-name "Example Auto Scaling Group") 262 | for row in $(echo "${activities}" | jq -r '.Activities[] | @base64'); do 263 | _jq() { 264 | echo ${row} | base64 --decode | jq -r ${1} 265 | } 266 | 267 | start_time=$(_jq '.StartTime') 268 | end_time=$(_jq '.EndTime') 269 | activity=$(_jq '.Description') 270 | 271 | echo $activity Duration: $(datediff $start_time $end_time) 272 | done 273 | ``` 274 | 275 | As you can see from the following results, because our instance was pre-initialized our launch was duration was significantly reduced. This means we can now more rapidly place instances into service in response to load placed on our workload by launching pre-initialized instances from the Warm Pool. 276 | 277 | ``` 278 | Launching a new EC2 instance from warm pool: i-0ea10fdc59a07df6e Duration: 36s 279 | ``` 280 | 281 | ## Cleanup 282 | 283 | Follow the clean-up instructions for the stack you deployed. 284 | 285 | * [Auto Scaling Group w/ Life Cycle Hooks controlled via Userdata](../lifecycle-hooks/userdata-managed-linux/README.md) 286 | * [Auto Scaling Group w/ Life Cycle Hooks controlled via a Lambda Function](../lifecycle-hooks/lambda-managed-linux/README.md) -------------------------------------------------------------------------------- /features/warm-pools/scaling-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "AutoScalingGroupName": "Example Auto Scaling Group", 3 | "PolicyName": "automaticScalingTargetTracking", 4 | "PolicyType": "TargetTrackingScaling", 5 | "EstimatedInstanceWarmup": 30, 6 | "TargetTrackingConfiguration": { 7 | "PredefinedMetricSpecification": { 8 | "PredefinedMetricType": "ASGAverageCPUUtilization" 9 | }, 10 | "TargetValue": 40, 11 | "DisableScaleIn": false 12 | } 13 | } -------------------------------------------------------------------------------- /features/warm-pools/ssm-stress.json: -------------------------------------------------------------------------------- 1 | { 2 | "Targets": [ 3 | { 4 | "Key": "tag:Name", 5 | "Values": [ 6 | "LifecycleHookExampleInstance" 7 | ] 8 | } 9 | ], 10 | "DocumentName": "AWS-RunShellScript", 11 | "Comment": "LifecycleHookExampleInstance stress", 12 | "Parameters": { 13 | "commands": [ 14 | "sudo stress --cpu 1 -v --timeout 300s" 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /tools/launch-configuration-inventory/.gitignore: -------------------------------------------------------------------------------- 1 | # Outputs 2 | inventory.csv -------------------------------------------------------------------------------- /tools/launch-configuration-inventory/README.md: -------------------------------------------------------------------------------- 1 | # Launch Configuration Inventory Script 2 | 3 | This example script demonstrates how you can use AWS APIs to create an inventory of Launch Configurations in a single AWS account, or an entire [AWS Organization](https://aws.amazon.com/organizations/). 4 | 5 | ## Running the Script in AWS CloudShell 6 | 7 | The simplest way to run this script is to copy it into an [AWS CloudShell](https://aws.amazon.com/cloudshell/) environment and execute it. 8 | 9 | 1. Access an [AWS CloudShell Environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/working-with-cloudshell.html) 10 | 2. Copy inventory.py to your local environment. 11 | ``` 12 | curl -O "https://raw.githubusercontent.com/aws-samples/amazon-ec2-auto-scaling-group-examples/main/tools/launch-configuration-inventory/inventory.py" 13 | ``` 14 | 3. Execute the script with the below arguments. See the examples below for some suggestions. For a CloudShell environment you should use the -r ROLE_ARN argument to specific a role to assume, ie: `python3 inventory.py -r arn:aws:iam::[ACCOUNT_ID]:role/[ROLE_NAME]` as the script currently does not support CloudShell's inherited credentials. 15 | 16 | ## Running the Script Locally 17 | 18 | If you want to run this script locally, you will need to ensure you have the following installed and configured. 19 | 20 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) installed and configured with the required credentials and permissions (see below). 21 | * [Python 3 installed](https://www.python.org/downloads/) 22 | 23 | ## Script Syntax and Arguments 24 | 25 | ``` 26 | usage: inventory.py [-h] [-f FILE] [-o] [-p PROFILE] [-or ORG_ROLE_NAME] [-r ROLE_ARN] 27 | 28 | Generate an inventory of Launch Configurations. 29 | 30 | optional arguments: 31 | -h, --help show this help message and exit 32 | -f FILE, --file FILE Directs the output to a file of your choice 33 | -o, --org Scan all accounts in current organization. 34 | -p PROFILE, --profile PROFILE 35 | Use a specific AWS config profile, defaults to default profile. 36 | -or ORG_ROLE_NAME, --org_role_name ORG_ROLE_NAME 37 | Name of role that will be assumed to make API calls in Org accounts, required for Org. 38 | -r ROLE_ARN, --role_arn ROLE_ARN 39 | Arn of role that will be assumed to make API calls instead of profile credentials. 40 | -i, --in_use Inventories only the launch configurations that are currently in-use. 41 | ``` 42 | 43 | ## Script Output 44 | 45 | The script will output details as it performs the inventory. You can use these details to review and monitor progress. 46 | 47 | ``` 48 | # python3 inventory.py -r arn:aws:iam::ACCOUNT_ID:role/OrganizationAccountAccessRole 49 | 2021-09-28 11:27:59,886 - INFO - Attempting to assume role: arn:aws:iam::ACCOUNT_ID:role/OrganizationAccountAccessRole 50 | 2021-09-28 11:27:59,900 - INFO - Found credentials in shared credentials file: ~/.aws/credentials 51 | 2021-09-28 11:28:00,900 - INFO - Getting inventory for account ACCOUNT_ID: 52 | 2021-09-28 11:28:00,900 - INFO - Getting a list of regions enabled for account ACCOUNT_ID. 53 | 2021-09-28 11:28:01,135 - INFO - Getting Launch Configurations for Region: eu-north-1 54 | 2021-09-28 11:28:02,096 - INFO - Getting Launch Configurations for Region: ap-south-1 55 | 2021-09-28 11:28:03,366 - INFO - Getting Launch Configurations for Region: eu-west-3 56 | 2021-09-28 11:28:04,221 - INFO - Getting Launch Configurations for Region: eu-west-2 57 | 2021-09-28 11:28:04,987 - INFO - Getting Launch Configurations for Region: eu-west-1 58 | 2021-09-28 11:28:05,821 - INFO - Getting Launch Configurations for Region: ap-northeast-3 59 | 2021-09-28 11:28:06,475 - INFO - Getting Launch Configurations for Region: ap-northeast-2 60 | 2021-09-28 11:28:07,201 - INFO - Getting Launch Configurations for Region: ap-northeast-1 61 | 2021-09-28 11:28:07,859 - INFO - Getting Launch Configurations for Region: sa-east-1 62 | 2021-09-28 11:28:08,830 - INFO - Getting Launch Configurations for Region: ca-central-1 63 | 2021-09-28 11:28:09,388 - INFO - Getting Launch Configurations for Region: ap-southeast-1 64 | 2021-09-28 11:28:10,457 - INFO - Getting Launch Configurations for Region: ap-southeast-2 65 | 2021-09-28 11:28:11,262 - INFO - Getting Launch Configurations for Region: eu-central-1 66 | 2021-09-28 11:28:12,338 - INFO - Getting Launch Configurations for Region: us-east-1 67 | 2021-09-28 11:28:13,195 - INFO - Getting Launch Configurations for Region: us-east-2 68 | 2021-09-28 11:28:14,226 - INFO - Getting Launch Configurations for Region: us-west-1 69 | 2021-09-28 11:28:14,879 - INFO - Getting Launch Configurations for Region: us-west-2 70 | 2021-09-28 11:28:15,592 - INFO - Saving results to output file: inventory.csv 71 | 2021-09-28 11:28:15,593 - INFO - You have 3 launch configurations across 1 accounts and 17 regions. 72 | ``` 73 | 74 | ## Errors 75 | 76 | If the script encounters any exceptions they will be logged to the output as errors. In most cases the inventory will continue to run (this is useful if you have a role with access to most, but not all, accounts in an Organization). 77 | 78 | ``` 79 | python3 inventory.py -r arn:aws:iam::ACCOUNT_ID:role/OrganizationAccountAccessRole -o -or OrganizationAccountAccessRole 80 | 2021-09-28 11:54:31,881 - INFO - Attempting to assume role: arn:aws:iam::ACCOUNT_ID:role/OrganizationAccountAccessRole 81 | 2021-09-28 11:54:31,904 - INFO - Found credentials in shared credentials file: ~/.aws/credentials 82 | 2021-09-28 11:54:32,392 - INFO - Getting a list of accounts in this organization. 83 | 2021-09-28 11:54:33,145 - INFO - Inventorying account: SECOND_ACCOUNT_ID 84 | 2021-09-28 11:54:33,145 - INFO - Getting credentials to inventory account: SECOND_ACCOUNT_ID 85 | 2021-09-28 11:54:33,145 - INFO - Attempting to assume role: arn:aws:iam::SECOND_ACCOUNT_ID:role/OrganizationAccountAccessRole 86 | 2021-09-28 11:54:33,584 - INFO - Getting a list of regions enabled for account SECOND_ACCOUNT_ID. 87 | 2021-09-28 11:54:33,810 - ERROR - Error getting list of regions: An error occurred (UnauthorizedOperation) when calling the DescribeRegions operation: You are not authorized to perform this operation. 88 | ``` 89 | 90 | ## Output File 91 | 92 | The inventory outputs to a file named `inventory.csv` by default. You can redirect this to another file by using the -f argument. 93 | 94 | ``` 95 | account_id,region,count,launch_configuratons 96 | ACCOUNT_ID,eu-north-1,0,[] 97 | ACCOUNT_ID,ap-south-1,0,[] 98 | ACCOUNT_ID,eu-west-3,0,[] 99 | ACCOUNT_ID,eu-west-2,0,[] 100 | ACCOUNT_ID,eu-west-1,0,[] 101 | ACCOUNT_ID,ap-northeast-3,0,[] 102 | ACCOUNT_ID,ap-northeast-2,0,[] 103 | ACCOUNT_ID,ap-northeast-1,0,[] 104 | ACCOUNT_ID,sa-east-1,0,[] 105 | ACCOUNT_ID,ca-central-1,0,[] 106 | ACCOUNT_ID,ap-southeast-1,0,[] 107 | ACCOUNT_ID,ap-southeast-2,0,[] 108 | ACCOUNT_ID,eu-central-1,0,[] 109 | ACCOUNT_ID,us-east-1,0,[] 110 | ACCOUNT_ID,us-east-2,0,[] 111 | ACCOUNT_ID,us-west-1,0,[] 112 | ACCOUNT_ID,us-west-2,3,"['ExampleOne', 'ExampleTwo', 'test']" 113 | ``` 114 | 115 | ## Examples 116 | 117 | Performs an inventory using the configured credentials in your default profile. 118 | ``` 119 | python3 inventory.py 120 | ``` 121 | 122 | Performs an inventory of in-use launch configurations using the configured credentials in your default profile. **Note: In-use means that they are actively associated with an Auto Scaling group.** 123 | ``` 124 | python3 inventory.py -i 125 | ``` 126 | 127 | Performs an inventory using the configured credentials in a profile named PROFILE_NAME. 128 | ``` 129 | python3 inventory.py -p PROFILE_NAME 130 | ``` 131 | 132 | Performs an inventory of an account by assuming the provided role ARN. 133 | ``` 134 | python3 inventory.py -r arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME 135 | ``` 136 | 137 | Performs an inventory of all accounts in an AWS Organization by assuming the provided role ARN to get a list of accounts and then assumes a role named ORG_ROLE_NAME in each account in the organization. 138 | ``` 139 | python3 inventory.py -o -or ORG_ROLE_NAME -r arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME 140 | ``` 141 | 142 | ## Required Permissions 143 | 144 | If you need help configuring your AWS CLI profile credentials to be able to assume a role, we suggest this [knowledge center article](https://aws.amazon.com/premiumsupport/knowledge-center/iam-assume-role-cli/). 145 | 146 | | Argument | Permissions | 147 | |--- |--- | 148 | | NONE | **Profile Credentials Require** | 149 | | | - ec2:DescribeRegions | 150 | | | - autoscaling:DescribeLaunchConfigurations | 151 | | | | 152 | | -r ROLE_ARN | **Profile Credentials Require** | 153 | | | - sts:AssumeRole (for ROLE_ARN) | 154 | | | **ROLE_ARN Requires** | 155 | | | - ec2:DescribeRegions | 156 | | | - autoscaling:DescribeLaunchConfigurations | 157 | | | - autoscaling:DescribeAutoScalingGroups | 158 | | | - organizations:ListAccounts | 159 | | | - sts:AssumeRole (for ROLE_NAME if using -o and -or) | 160 | | | | 161 | | -o | **Profile Credentials Require** | 162 | | | - sts:AssumeRole(for ROLE_NAME or ROLE_ARN if using -r) | 163 | | | - organizations:ListAccounts (if not using -r) | 164 | | | | 165 | | -or ROLE_NAME | **Profile Credentials Require** | 166 | | | - sts:AssumeRole(for ROLE_NAME) | 167 | | | **ROLE_NAME Requires** | 168 | | | - ec2:DescribeRegions | 169 | | | - autoscaling:DescribeLaunchConfigurations | 170 | | | | 171 | | -i | **Profile Credentials Require** | 172 | | | - autoscaling:DescribeAutoScalingGroups (if not using -r)| 173 | | | **ROLE_NAME Requires** | 174 | | | - autoscaling:DescribeAutoScalingGroups | -------------------------------------------------------------------------------- /tools/launch-configuration-inventory/inventory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | # software and associated documentation files (the "Software"), to deal in the Software 5 | # without restriction, including without limitation the rights to use, copy, modify, 6 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | # permit persons to whom the Software is furnished to do so. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | import boto3 17 | import logging 18 | import sys 19 | import csv 20 | import argparse 21 | 22 | from botocore.exceptions import ClientError 23 | 24 | # Defaults 25 | default_output_file = "inventory.csv" 26 | default_aws_profile = 'default' 27 | 28 | # Arguments 29 | parser = argparse.ArgumentParser(description='Generate an inventory of Launch Configurations.') 30 | parser.add_argument("-f", "--file", help="Directs the output to a file of your choice", default=default_output_file) 31 | parser.add_argument("-o", "--org", help="Scan all accounts in current organization.", action='store_true') 32 | parser.add_argument("-p", "--profile", help='Use a specific AWS config profile, defaults to default profile.') 33 | parser.add_argument("-or", "--org_role_name", help="Name of role that will be assumed to make API calls in Org accounts, required for Org.") 34 | parser.add_argument("-r", "--role_arn", help="Arn of role that will be assumed to make API calls instead of profile credentials.") 35 | parser.add_argument("-i", "--in_use", help="Inventories only the launch configurations that are currently in-use.", action='store_true') 36 | 37 | parser.set_defaults(org=False) 38 | parser.set_defaults(in_use=False) 39 | args = parser.parse_args() 40 | 41 | if args.org and (args.org_role_name is None): 42 | parser.error("--org requires --org_role_name") 43 | 44 | # Logging 45 | logger = logging.getLogger() 46 | logger.setLevel(logging.INFO) 47 | handler = logging.StreamHandler(sys.stdout) 48 | handler.setLevel(logging.INFO) 49 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 50 | handler.setFormatter(formatter) 51 | logger.addHandler(handler) 52 | 53 | # Get and Return Credentials for Organization Role 54 | def get_credentials_for_role(role_arn, credentials): 55 | 56 | logger.info('Attempting to assume role: {}'.format(role_arn)) 57 | 58 | try: 59 | sts = None 60 | if credentials: 61 | sts = boto3.client('sts', **credentials) 62 | else: 63 | sts = boto3.client('sts') 64 | 65 | response = sts.assume_role( 66 | RoleArn=role_arn, 67 | RoleSessionName="RoleAssume" 68 | ) 69 | 70 | # Get Credentials From Session 71 | credentials = { 72 | 'aws_access_key_id' : response["Credentials"]["AccessKeyId"], 73 | 'aws_secret_access_key' : response["Credentials"]["SecretAccessKey"], 74 | 'aws_session_token' : response["Credentials"]["SessionToken"], 75 | } 76 | 77 | return credentials 78 | 79 | except Exception as e: 80 | message = 'Could not assume role: {} : {}'.format(role_arn, e) 81 | logger.error(message) 82 | return None 83 | 84 | # Get and Return Credentials for Provided AWS Profile 85 | def get_credentials_for_profile(profile_name): 86 | logger.info('Attempting to get credentials for profile: {}'.format(profile_name)) 87 | 88 | try: 89 | session = boto3.Session(profile_name=profile_name) 90 | session_credentials = session.get_credentials() 91 | credentials = { 92 | 'aws_access_key_id' : session_credentials.access_key, 93 | 'aws_secret_access_key' : session_credentials.secret_key 94 | } 95 | return credentials 96 | 97 | except Exception as e: 98 | message = 'Could not load profile: {} : {}'.format(profile_name, e) 99 | logger.error(message) 100 | return None 101 | 102 | # Paginates Responses from API Calls 103 | def paginate(method, **kwargs): 104 | client = method.__self__ 105 | 106 | try: 107 | paginator = client.get_paginator(method.__name__) 108 | for page in paginator.paginate(**kwargs).result_key_iters(): 109 | for item in page: 110 | yield item 111 | 112 | except ClientError as e: 113 | message = 'Error describing instances: {}'.format(e) 114 | logger.error(message) 115 | raise Exception(message) 116 | 117 | # Gets a List of Accounts in Organization 118 | def get_organization_accounts(credentials): 119 | logger.info("Getting a list of accounts in this organization.") 120 | 121 | accounts = [] 122 | try: 123 | organizations = boto3.client('organizations', **credentials) 124 | response = paginate(organizations.list_accounts) 125 | 126 | for account in response: 127 | accounts.append(account) 128 | 129 | return accounts 130 | 131 | except ClientError as e: 132 | message = 'Error getting a list of accounts in the organization: {}'.format(e) 133 | logger.error(message) 134 | 135 | return accounts 136 | 137 | # Gets Regions Enabled for Account 138 | def get_regions(account_id, credentials): 139 | logger.info('Getting a list of regions enabled for account {}.'.format(account_id)) 140 | 141 | regions = [] 142 | try: 143 | ec2 = boto3.client('ec2', **credentials) 144 | 145 | response = ec2.describe_regions( 146 | AllRegions=False 147 | ) 148 | 149 | for region in response['Regions']: 150 | regions.append(region['RegionName']) 151 | 152 | except ClientError as e: 153 | message = 'Error getting list of regions: {}'.format(e) 154 | logger.error(message) 155 | 156 | return regions 157 | 158 | # Gets Launch Configurations in Account and Region 159 | def get_launch_configurations(account_id, region, credentials): 160 | logger.info('Getting Launch Configurations for Region: {}'.format(region)) 161 | 162 | launch_configurations = [] 163 | try: 164 | autoscaling = boto3.client('autoscaling', region_name=region, **credentials) 165 | 166 | response = paginate(autoscaling.describe_launch_configurations) 167 | 168 | for launch_configuration in response: 169 | launch_configurations.append(launch_configuration['LaunchConfigurationName']) 170 | 171 | return { 172 | 'account_id' : account_id, 173 | 'region' : region, 174 | 'count' : len(launch_configurations), 175 | 'launch_configuratons' : launch_configurations 176 | } 177 | 178 | except ClientError as e: 179 | message = 'Error getting list of launch configurations: {}'.format(e) 180 | logger.error(message) 181 | 182 | return {} 183 | 184 | # Gets Launch Configurations In-Use in Account and Region 185 | def get_launch_configurations_in_use(account_id, region, credentials): 186 | logger.info('Getting Launch Configurations In-Use for Region: {}'.format(region)) 187 | 188 | launch_configurations = [] 189 | try: 190 | autoscaling = boto3.client('autoscaling', region_name=region, **credentials) 191 | 192 | response = paginate(autoscaling.describe_auto_scaling_groups) 193 | 194 | for auto_scaling_group in response: 195 | if 'LaunchConfigurationName' in auto_scaling_group: 196 | launch_configurations.append({ 197 | 'auto_scaling_group' : auto_scaling_group['AutoScalingGroupName'], 198 | 'launch_configuration' : auto_scaling_group['LaunchConfigurationName'] 199 | }) 200 | 201 | return { 202 | 'account_id' : account_id, 203 | 'region' : region, 204 | 'count' : len(launch_configurations), 205 | 'launch_configuratons' : launch_configurations 206 | } 207 | 208 | except ClientError as e: 209 | message = 'Error getting list of launch configurations: {}'.format(e) 210 | logger.error(message) 211 | 212 | return {} 213 | 214 | 215 | # Writes an Inventory File of Launch Configurations 216 | def write_inventory_file(file, inventory): 217 | logger.info('Saving results to output file: {}'.format(file)) 218 | 219 | data_file = open(file, 'w', newline='') 220 | csv_writer = csv.writer(data_file) 221 | 222 | count = 0 223 | for data in inventory: 224 | if count == 0: 225 | header = data.keys() 226 | csv_writer.writerow(header) 227 | count += 1 228 | csv_writer.writerow(data.values()) 229 | 230 | data_file.close() 231 | 232 | return 233 | 234 | # Outputs Summary of Inventory 235 | def write_summary(inventory): 236 | 237 | launch_configurations = 0 238 | accounts = [] 239 | regions = [] 240 | 241 | for item in inventory: 242 | launch_configurations = launch_configurations + item['count'] 243 | if item['account_id'] not in accounts: accounts.append(item['account_id']) 244 | if item['region'] not in regions: regions.append(item['region']) 245 | 246 | logger.info('You have {} launch configurations across {} accounts and {} regions.'.format(launch_configurations, len(accounts), len(regions))) 247 | 248 | return 249 | 250 | def main(): 251 | 252 | inventory = [] 253 | profile_name = args.profile 254 | role_arn = args.role_arn 255 | org_role_name = args.org_role_name 256 | inventory_file = args.file 257 | 258 | # Get Credentials From Profile or Environment 259 | credentials = None 260 | if role_arn: 261 | credentials = get_credentials_for_role(role_arn, None) 262 | else: 263 | credentials = get_credentials_for_profile(profile_name) 264 | 265 | if credentials is not None: 266 | 267 | # Inventorying Entire Organization 268 | if args.org is True: 269 | accounts = get_organization_accounts(credentials) 270 | 271 | # For Each Account, Attempt to Assume Role and Get Launch Configurations 272 | for account in accounts: 273 | account_id = account['Id'] 274 | logger.info('Inventorying account: {}'.format(account_id)) 275 | 276 | try: 277 | # Setup Session in Account 278 | role_arn = 'arn:aws:iam::{}:role/{}'.format(account_id, org_role_name) 279 | logger.info('Getting credentials to inventory account: {}'.format(account_id)) 280 | role_credentials = get_credentials_for_role(role_arn, credentials) 281 | 282 | if role_credentials is not None: 283 | 284 | # Get List of Regions Enabled for Account 285 | regions = get_regions(account_id, role_credentials) 286 | 287 | # For Each Region Get Launch Configurations 288 | for region in regions: 289 | if args.in_use: 290 | response = get_launch_configurations_in_use(account_id, region, role_credentials) 291 | inventory.append(response) 292 | else: 293 | response = get_launch_configurations(account_id, region, role_credentials) 294 | inventory.append(response) 295 | 296 | # Catch and Store Errors 297 | except ClientError as e: 298 | message = 'Error setting up session with account: {}'.format(e) 299 | logger.error(message) 300 | 301 | # Inventorying Single Account 302 | if args.org is False: 303 | 304 | try: 305 | account_id = boto3.client('sts', **credentials).get_caller_identity().get('Account') 306 | 307 | logger.info('Getting inventory for account {}:'.format(account_id)) 308 | 309 | regions = get_regions(account_id, credentials) 310 | 311 | # For Each Region Get Launch Configurations 312 | for region in regions: 313 | if args.in_use: 314 | response = get_launch_configurations_in_use(account_id, region, credentials) 315 | inventory.append(response) 316 | else: 317 | response = get_launch_configurations(account_id, region, credentials) 318 | inventory.append(response) 319 | 320 | except ClientError as e: 321 | message = 'Error getting inventory, check your credential configuration or try with the -r argument: {}'.format(e) 322 | logger.error(message) 323 | 324 | 325 | # Write Outputs 326 | write_inventory_file(inventory_file, inventory) 327 | write_summary(inventory) 328 | return inventory 329 | 330 | else: 331 | logger.error("No credentials to perform inventory.") 332 | return None 333 | 334 | if __name__ == "__main__": 335 | main() -------------------------------------------------------------------------------- /tools/launch-configuration-inventory/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 --------------------------------------------------------------------------------