├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── RunSSMAutomationBeforeTermination.json /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. 5 | -------------------------------------------------------------------------------- /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 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 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. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | 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. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | 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. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /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 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Run code before terminating an EC2 Auto Scaling instance 2 | 3 | CloudFormation template related to [this blog](https://aws.amazon.com/blogs/infrastructure-and-automation/run-code-before-terminating-an-ec2-auto-scaling-instance/) published on the AWS [Infrastructure & Automation blog](https://aws.amazon.com/blogs/infrastructure-and-automation/) 4 | 5 | ## Overview 6 | This is a sample solution using [Amazon EC2 Auto Scaling Lifecycle Hooks](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html) to perform any desired actions before terminating the instance within the Auto Scaling group. [lifecycle hook](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks-overview.html) puts the instance in `Terminating:Wait` status. The `Terminating:Wait` status will be monitored by an Amazon CloudWatch event, which triggers an AWS Systems Manager automation document to perform the action you want. 7 | 8 | ## Deployment 9 | ### Prerequisites 10 | - An Amazon EC2 Auto Scaling group. 11 | - A [String parameter](https://docs.aws.amazon.com/systems-manager/latest/userguide/param-create-cli.html#param-create-cli-string) for the domain user. The user must have permission to remove the computer from the domain. I refer to this parameter as DomainUserName. 12 | - A [SecureString parameter](https://docs.aws.amazon.com/systems-manager/latest/userguide/param-create-cli.html#param-create-cli-securestring) that contains the DomainUserName password. I refer to this parameter as DomainPassword. 13 | 14 | ### Walkthrough 15 | 16 | The CloudFormation template [RunSSMAutomationBeforeTermination.json](/RunSSMAutomationBeforeTermination.json) will go through the following steps: 17 | 1. Add a [lifecycle hook](https://docs.aws.amazon.com/autoscaling/ec2/userguide/adding-lifecycle-hooks.html). 18 | 2. Create a [Systems Manager automation document](https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-documents.html). The automation document goes through the following steps. 19 | - Run a Windows PowerShell script to remove the computer from the domain. 20 | - Create an AMI of the EC2 instance. 21 | - Execute AWS API [CompleteLifecycleAction](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_CompleteLifecycleAction.html) to terminate the instance. 22 | 3. Create a [CloudWatch Events rule](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule.html). 23 | 4. Add a [Systems Manager automation document as a CloudWatch Event target](https://docs.aws.amazon.com/systems-manager/latest/userguide/running-automations-event-bridge.html). 24 | 5. (Optional) Create AWS Identity and Access Management (IAM) [policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create.html) and a [role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html) to delegate permissions to the Systems Manager automation document. 25 | 6. (Optional) Create AWS Identity and Access Management (IAM) [policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create.html) and a [role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html) to delegate permissions to Amazon CloudWatch Events, which invokes the Systems Manager automation document. 26 | 27 | For more details about launching a stack, refer to [Creating a Stack on the AWS CloudFormation Console](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-console-create-stack.html). 28 | 29 | ### Template Parameters 30 | The stack template includes the following parameters: 31 | 32 | | Parameter | Required | Description | 33 | | --- | --- | --- | 34 | | AutoScalingGN | Yes | Enter the name of the auto scaling group to monitor and add the [lifecycle hook](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html). | 35 | | DomainUserName | Yes | The name of the [String parameter](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html#parameter-type-string) for the DomainUser. The user would need to have enough permissions to remove the computer from the domain. | 36 | | DomainPassword | Yes | The name of the [SecureString parameter](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html#parameter-type-securestring) that have the password of DomainUserName. | 37 | | ExistingAutomationAssumeRole | No | The ARN of [AWS Systems Manager Automation assume role](https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-setup.html#automation-setup-configure-role). If not specified, the template will create a role with minimum permissions as describe in the blog. | 38 | | ExistingCloudWatchEventRole | No | The Role ARN to be used by CloudWatch event to trigger the AWS Systems Manager Automation execution.If not specified, the template will create a role with minimum permissions as describe in the blog. | 39 | 40 | ## Security 41 | 42 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 43 | 44 | ## License 45 | 46 | This library is licensed under the MIT-0 License. See the LICENSE file. 47 | -------------------------------------------------------------------------------- /RunSSMAutomationBeforeTermination.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Parameters": { 4 | "AutoScalingGN": { 5 | "Type": "String", 6 | "Description": "(Required) Enter the name of the AutoScalingGroup." 7 | }, 8 | "DomainUserName": { 9 | "Type": "String", 10 | "Description": "(Required) The name of the String parameter for the DomainUser. The user would need to have enough permissions to remove the computer from the domain." 11 | }, 12 | "DomainPassword": { 13 | "Type": "String", 14 | "Description": "(Required) The name of the SecureString parameter that have the password of DomainUserName." 15 | }, 16 | "ExistingAutomationAssumeRole": { 17 | "Type": "String", 18 | "Description": "(Optional) The ARN of the role that allows AWS Systems Manager Automation execution to perform the actions in the document. If not specified, the template will create a rule with minimum permissions." 19 | }, 20 | "ExistingCloudWatchEventRole": { 21 | "Type": "String", 22 | "Description": "(Optional) The Role ARN to be used by CloudWatch event to trigger the AWS Systems Manager Automation execution.If not specified, the template will create a rule with minimum permissions." 23 | } 24 | }, 25 | "Conditions": { 26 | "CreateCloudWatchEventRoleCondition": { 27 | "Fn::Equals": [ 28 | { 29 | "Ref": "ExistingCloudWatchEventRole" 30 | }, 31 | "" 32 | ] 33 | }, 34 | "CreateAutomationAssumeRoleCondition": { 35 | "Fn::Equals": [ 36 | { 37 | "Ref": "ExistingAutomationAssumeRole" 38 | }, 39 | "" 40 | ] 41 | } 42 | }, 43 | "Resources": { 44 | "CloudWatchEventRole": { 45 | "Type": "AWS::IAM::Role", 46 | "Condition": "CreateCloudWatchEventRoleCondition", 47 | "Properties": { 48 | "AssumeRolePolicyDocument": { 49 | "Version": "2012-10-17", 50 | "Statement": [ 51 | { 52 | "Effect": "Allow", 53 | "Principal": { 54 | "Service": [ 55 | "events.amazonaws.com" 56 | ] 57 | }, 58 | "Action": [ 59 | "sts:AssumeRole" 60 | ] 61 | } 62 | ] 63 | }, 64 | "Policies": [ 65 | { 66 | "PolicyDocument": { 67 | "Version": "2012-10-17", 68 | "Statement": [ 69 | { 70 | "Effect": "Allow", 71 | "Action": [ 72 | "ssm:StartAutomationExecution" 73 | ], 74 | "Resource": { 75 | "Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${SSMAutomationDocument}:$DEFAULT" 76 | } 77 | } 78 | ] 79 | }, 80 | "PolicyName": "Start-SSM-Automation-Policy" 81 | }, 82 | { 83 | "PolicyDocument": { 84 | "Version": "2012-10-17", 85 | "Statement": [ 86 | { 87 | "Effect": "Allow", 88 | "Action": [ 89 | "iam:PassRole" 90 | ], 91 | "Resource": { 92 | "Fn::If": [ 93 | "CreateAutomationAssumeRoleCondition", 94 | { 95 | "Fn::GetAtt": [ 96 | "AutomationAssumeRole", 97 | "Arn" 98 | ] 99 | }, 100 | { 101 | "Ref": "ExistingAutomationAssumeRole" 102 | } 103 | ] 104 | } 105 | } 106 | ] 107 | }, 108 | "PolicyName": "Pass-Role-SSM-Automation-Policy" 109 | } 110 | ] 111 | } 112 | }, 113 | "AutomationAssumeRole": { 114 | "Type": "AWS::IAM::Role", 115 | "Condition": "CreateAutomationAssumeRoleCondition", 116 | "Properties": { 117 | "AssumeRolePolicyDocument": { 118 | "Version": "2012-10-17", 119 | "Statement": [ 120 | { 121 | "Effect": "Allow", 122 | "Principal": { 123 | "Service": [ 124 | "ssm.amazonaws.com" 125 | ] 126 | }, 127 | "Action": [ 128 | "sts:AssumeRole" 129 | ] 130 | } 131 | ] 132 | }, 133 | "Policies": [ 134 | { 135 | "PolicyDocument": { 136 | "Version": "2012-10-17", 137 | "Statement": [ 138 | { 139 | "Effect": "Allow", 140 | "Action": [ 141 | "ec2:CreateImage", 142 | "ec2:DescribeImages", 143 | "ssm:DescribeInstanceInformation", 144 | "ssm:ListCommands", 145 | "ssm:ListCommandInvocations" 146 | ], 147 | "Resource": "*" 148 | }, 149 | { 150 | "Effect": "Allow", 151 | "Action": [ 152 | "ssm:SendCommand" 153 | ], 154 | "Resource": { 155 | "Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunPowerShellScript" 156 | } 157 | }, 158 | { 159 | "Action": [ 160 | "ssm:SendCommand" 161 | ], 162 | "Resource": { 163 | "Fn::Sub": "arn:${AWS::Partition}:ec2:*:*:instance/*" 164 | }, 165 | "Effect": "Allow" 166 | } 167 | ] 168 | }, 169 | "PolicyName": "SSM-Automation-Policy" 170 | }, 171 | { 172 | "PolicyDocument": { 173 | "Version": "2012-10-17", 174 | "Statement": [ 175 | { 176 | "Effect": "Allow", 177 | "Action": [ 178 | "autoscaling:CompleteLifecycleAction" 179 | ], 180 | "Resource": { 181 | "Fn::Sub": "arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AutoScalingGN}" 182 | } 183 | } 184 | ] 185 | }, 186 | "PolicyName": "SSM-Automation-Permission-to-CompleteLifecycle-Policy" 187 | } 188 | ] 189 | } 190 | }, 191 | "CreateLifeCycleHook": { 192 | "Type": "AWS::AutoScaling::LifecycleHook", 193 | "Properties": { 194 | "AutoScalingGroupName": { 195 | "Ref": "AutoScalingGN" 196 | }, 197 | "DefaultResult": "CONTINUE", 198 | "HeartbeatTimeout": 7200, 199 | "LifecycleHookName": "LCH_EC2_INSTANCE_TERMINATING", 200 | "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" 201 | } 202 | }, 203 | "SSMAutomationDocument": { 204 | "Type": "AWS::SSM::Document", 205 | "Properties": { 206 | "DocumentType": "Automation", 207 | "Content": { 208 | "schemaVersion": "0.3", 209 | "assumeRole": "{{AutomationAssumeRole}}", 210 | "description": "This Document Created as part of CloudFormation stack named. This document will disjoin instances From an Active Directory, create an AMI of the instance, send a signal to the LifeCycleHook to terminate the instance", 211 | "parameters": { 212 | "InstanceId": { 213 | "type": "String" 214 | }, 215 | "ASGName": { 216 | "type": "String", 217 | "default": { 218 | "Ref": "AutoScalingGN" 219 | }, 220 | "description": "The name of the AutoScaling Group." 221 | }, 222 | "LCHName": { 223 | "type": "String", 224 | "default": "LCH_EC2_INSTANCE_TERMINATING", 225 | "description": "The name of the Life Cycle Hook." 226 | }, 227 | "DomainAdminUserName": { 228 | "type": "String", 229 | "default": { 230 | "Ref": "DomainUserName" 231 | }, 232 | "description": "The name of the String Parameter for the domain user. The user would need to have enough permissions to remove the computer from the domain." 233 | }, 234 | "DomainAdminPassword": { 235 | "type": "String", 236 | "default": { 237 | "Ref": "DomainPassword" 238 | }, 239 | "description": "The name of the SecureString Parameter that have the password of DomainUserName" 240 | }, 241 | "AutomationAssumeRole": { 242 | "type": "String", 243 | "default": { 244 | "Fn::If": [ 245 | "CreateAutomationAssumeRoleCondition", 246 | { 247 | "Fn::GetAtt": [ 248 | "AutomationAssumeRole", 249 | "Arn" 250 | ] 251 | }, 252 | { 253 | "Ref": "ExistingAutomationAssumeRole" 254 | } 255 | ] 256 | }, 257 | "description": "(Required) The ARN of the role that allows Automation to perform the actions on your behalf." 258 | } 259 | }, 260 | "mainSteps": [ 261 | { 262 | "name": "RunCommand", 263 | "action": "aws:runCommand", 264 | "inputs": { 265 | "DocumentName": "AWS-RunPowerShellScript", 266 | "InstanceIds": [ 267 | "{{ InstanceId }}" 268 | ], 269 | "Parameters": { 270 | "executionTimeout": "7200", 271 | "commands": [ 272 | "$name = $env:computerName", 273 | "$PartOfDomain = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain", 274 | "if($PartOfDomain -eq $true){", 275 | "$username = (Get-SSMParameterValue -Name {{DomainAdminUserName}}).Parameters[0].Value", 276 | "$password = (Get-SSMParameterValue -Name {{DomainAdminPassword}} -WithDecryption $True).Parameters[0].Value | ConvertTo-SecureString -asPlainText -Force", 277 | "$credential = New-Object System.Management.Automation.PSCredential($username,$password)", 278 | "Write-Output \"Removing computer $name from the domain\"", 279 | "Remove-Computer -ComputerName $name -Credential $credential -PassThru -Restart -Force}", 280 | "else{", 281 | "Write-Output \"Cannot remove computer $name because it is not in a domain\"}" 282 | ] 283 | } 284 | } 285 | }, 286 | { 287 | "name": "createAMI", 288 | "action": "aws:createImage", 289 | "inputs": { 290 | "InstanceId": "{{ InstanceId }}", 291 | "ImageName": "{{ InstanceId }}_{{automation:EXECUTION_ID}}", 292 | "NoReboot": true, 293 | "ImageDescription": "My newly created AMI - ASGName: {{ ASGName }}" 294 | } 295 | }, 296 | { 297 | "name": "TerminateTheInstance", 298 | "action": "aws:executeAwsApi", 299 | "inputs": { 300 | "Service": "autoscaling", 301 | "Api": "CompleteLifecycleAction", 302 | "AutoScalingGroupName": "{{ ASGName }}", 303 | "InstanceId": "{{ InstanceId }}", 304 | "LifecycleActionResult": "CONTINUE", 305 | "LifecycleHookName": "{{ LCHName }}" 306 | } 307 | } 308 | ], 309 | "outputs": [ 310 | "createAMI.ImageId" 311 | ] 312 | } 313 | } 314 | }, 315 | "CreateCloudWatchEvent": { 316 | "Type": "AWS::Events::Rule", 317 | "Properties": { 318 | "Description": "CloudWatch Event rule that will trigger AWS Systems Manager Automation document when an instance go in Terminate:wait. This is created as a part of a CloudFormation.", 319 | "EventPattern": { 320 | "source": [ 321 | "aws.autoscaling" 322 | ], 323 | "detail-type": [ 324 | "EC2 Instance-terminate Lifecycle Action" 325 | ], 326 | "detail": { 327 | "AutoScalingGroupName": [ 328 | { 329 | "Ref": "AutoScalingGN" 330 | } 331 | ] 332 | } 333 | }, 334 | "Name": "RunSSMAutomationforAutoScalingBeforeTermination", 335 | "Targets": [ 336 | { 337 | "Arn": { 338 | "Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${SSMAutomationDocument}:$DEFAULT" 339 | }, 340 | "RoleArn": { 341 | "Fn::If": [ 342 | "CreateCloudWatchEventRoleCondition", 343 | { 344 | "Fn::GetAtt": [ 345 | "CloudWatchEventRole", 346 | "Arn" 347 | ] 348 | }, 349 | { 350 | "Ref": "ExistingCloudWatchEventRole" 351 | } 352 | ] 353 | }, 354 | "Id": "TargetFunctionV1", 355 | "InputTransformer": { 356 | "InputPathsMap": { 357 | "instanceid": "$.detail.EC2InstanceId" 358 | }, 359 | "InputTemplate": { 360 | "Fn::Join": [ 361 | "", 362 | [ 363 | "{\"InstanceId\":[]}" 364 | ] 365 | ] 366 | } 367 | } 368 | } 369 | ] 370 | } 371 | } 372 | } 373 | } 374 | --------------------------------------------------------------------------------