Here is a solution using Lifecycle Hooks, Automation and Run Command, based on this article:
Resources:
MyTerminationHook:
Type: AWS::AutoScaling::LifecycleHook
Properties:
AutoScalingGroupName: !Ref MyAutoScalingGroup
DefaultResult: CONTINUE
HeartbeatTimeout: 900
LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING
MyTerminationDocument:
Type: AWS::SSM::Document
Properties:
DocumentType: Automation
Content:
description: 'Run command before terminating instance'
schemaVersion: '0.3'
assumeRole: !GetAtt MyTerminationDocumentRole.Arn
parameters:
instanceId:
type: String
mainSteps:
- name: RunCommand
action: aws:runCommand
inputs:
DocumentName: AWS-RunShellScript
InstanceIds:
- '{{ instanceId }}'
TimeoutSeconds: 60
Parameters:
commands: /etc/my-termination-script.sh
executionTimeout: '900'
- name: TerminateInstance
action: aws:executeAwsApi
inputs:
Api: CompleteLifecycleAction
AutoScalingGroupName: !Ref MyAutoScalingGroup
InstanceId: '{{ instanceId }}'
LifecycleActionResult: CONTINUE
LifecycleHookName: !Ref MyTerminationHook
Service: autoscaling
MyTerminationRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.autoscaling
detail-type:
- EC2 Instance-terminate Lifecycle Action
detail:
AutoScalingGroupName:
- !Ref MyAutoScalingGroup
Targets:
- Id: my-termination-document
Arn: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${MyTerminationDocument}:$DEFAULT'
RoleArn: !GetAtt MyTerminationRuleRole.Arn
InputTransformer:
InputPathsMap:
instanceId: '$.detail.EC2InstanceId'
InputTemplate: '{"instanceId":[<instanceId>]}'
MyTerminationRuleRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: start-automation
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- ssm:StartAutomationExecution
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${MyTerminationDocument}:$DEFAULT'
MyTerminationDocumentRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: ssm.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: run-command-and-complete-lifecycle
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- autoscaling:CompleteLifecycleAction
Resource: !Sub 'arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${MyAutoScalingGroup}'
- Effect: Allow
Action:
- ssm:DescribeInstanceInformation
- ssm:ListCommands
- ssm:ListCommandInvocations
Resource: '*'
- Effect: Allow
Action:
- ssm:SendCommand
Resource: 'arn:aws:ssm:*::document/AWS-RunShellScript'
- Effect: Allow
Action:
- ssm:SendCommand
Resource: !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*'
The permissions required to deploy these are
- Sid: CreateDocument
Effect: Allow
Action:
- "ssm:CreateDocument"
- "ssm:GetDocument"
- "ssm:DeleteDocument"
- "ssm:ListTagsForResource"
Resource: !Sub "arn:aws:ssm:<...>"
- Sid: InstallLifecycleHook
Effect: Allow
Action:
- "autoscaling:DeleteLifecycleHook"
- "autoscaling:CreateLifecycleHook"
Resource: !Sub "arn:aws:autoscaling:<...>"
- Sid: ManageRules
Effect: Allow
Action:
- "events:PutRule"
- "events:ListRules"
- "events:DescribeRule"
- "events:DeleteRule"
- "events:PutTargets"
- "events:RemoveTargets"
Resource: !Sub "arn:aws:events:<...>"
There might be more; these are the ones I had to add to our existing deployment policy. They may also not all be required, but I was fed up redeploying and adding them piecemeal so I added some of the Rule ones as an educated guess.