forked from aws-samples/aws-lambda-lifecycle-hooks-function
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request aws-samples#2 from chris-redekop/add-cfn-template
Add a CloudFormation template
- Loading branch information
Showing
2 changed files
with
355 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import os | ||
import boto3 | ||
import json | ||
import logging | ||
import time | ||
|
||
logger = logging.getLogger() | ||
logger.setLevel(logging.DEBUG) | ||
ssm_client = boto3.client("ssm") | ||
|
||
LIFECYCLE_KEY = "LifecycleHookName" | ||
ASG_KEY = "AutoScalingGroupName" | ||
EC2_KEY = "EC2InstanceId" | ||
DOCUMENT_NAME = os.environ['DOCUMENT_NAME'] | ||
RESPONSE_DOCUMENT_KEY = "DocumentIdentifiers" | ||
|
||
def check_response(response_json): | ||
try: | ||
if response_json['ResponseMetadata']['HTTPStatusCode'] == 200: | ||
return True | ||
else: | ||
return False | ||
except KeyError: | ||
return False | ||
|
||
def list_document(): | ||
document_filter_parameters = {'key': 'Name', 'value': DOCUMENT_NAME} | ||
response = ssm_client.list_documents( | ||
DocumentFilterList=[ document_filter_parameters ] | ||
) | ||
return response | ||
|
||
def check_document(): | ||
# If the document already exists, it will not create it. | ||
try: | ||
response = list_document() | ||
if check_response(response): | ||
logger.info("Documents list: %s", response) | ||
if response[RESPONSE_DOCUMENT_KEY]: | ||
logger.info("Documents exists: %s", response) | ||
return True | ||
else: | ||
return False | ||
else: | ||
logger.error("Documents' list error: %s", response) | ||
return False | ||
except Exception, e: | ||
logger.error("Document error: %s", str(e)) | ||
return None | ||
|
||
def send_command(instance_id): | ||
# Until the document is not ready, waits in accordance to a backoff mechanism. | ||
while True: | ||
timewait = 1 | ||
response = list_document() | ||
if any(response[RESPONSE_DOCUMENT_KEY]): | ||
break | ||
time.sleep(timewait) | ||
timewait += timewait | ||
try: | ||
response = ssm_client.send_command( | ||
InstanceIds = [ instance_id ], | ||
DocumentName = DOCUMENT_NAME, | ||
TimeoutSeconds = 120 | ||
) | ||
if check_response(response): | ||
logger.info("Command sent: %s", response) | ||
return response['Command']['CommandId'] | ||
else: | ||
logger.error("Command could not be sent: %s", response) | ||
return None | ||
except Exception, e: | ||
logger.error("Command could not be sent: %s", str(e)) | ||
return None | ||
|
||
def check_command(command_id, instance_id): | ||
timewait = 1 | ||
while True: | ||
response_iterator = ssm_client.list_command_invocations( | ||
CommandId = command_id, | ||
InstanceId = instance_id, | ||
Details=False | ||
) | ||
if check_response(response_iterator): | ||
response_iterator_status = response_iterator['CommandInvocations'][0]['Status'] | ||
if response_iterator_status != 'Pending': | ||
if response_iterator_status == 'InProgress' or response_iterator_status == 'Success': | ||
logging.info( "Status: %s", response_iterator_status) | ||
return True | ||
else: | ||
logging.error("ERROR: status: %s", response_iterator) | ||
return False | ||
time.sleep(timewait) | ||
timewait += timewait | ||
|
||
def abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id): | ||
asg_client = boto3.client('autoscaling') | ||
try: | ||
response = asg_client.complete_lifecycle_action( | ||
LifecycleHookName=life_cycle_hook, | ||
AutoScalingGroupName=auto_scaling_group, | ||
LifecycleActionResult='ABANDON', | ||
InstanceId=instance_id | ||
) | ||
if check_response(response): | ||
logger.info("Lifecycle hook abandoned correctly: %s", response) | ||
else: | ||
logger.error("Lifecycle hook could not be abandoned: %s", response) | ||
except Exception, e: | ||
logger.error("Lifecycle hook abandon could not be executed: %s", str(e)) | ||
return None | ||
|
||
def lambda_handler(event, context): | ||
try: | ||
logger.info(json.dumps(event)) | ||
message = event['detail'] | ||
if LIFECYCLE_KEY in message and ASG_KEY in message: | ||
life_cycle_hook = message[LIFECYCLE_KEY] | ||
auto_scaling_group = message[ASG_KEY] | ||
instance_id = message[EC2_KEY] | ||
if check_document(): | ||
command_id = send_command(instance_id) | ||
if command_id != None: | ||
if check_command(command_id, instance_id): | ||
logger.info("Lambda executed correctly") | ||
else: | ||
abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id) | ||
else: | ||
abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id) | ||
else: | ||
abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id) | ||
else: | ||
logger.error("No valid JSON message: %s", parsed_message) | ||
except Exception, e: | ||
logger.error("Error: %s", str(e)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
--- | ||
AWSTemplateFormatVersion: '2010-09-09' | ||
|
||
Parameters: | ||
Ec2KeyName: | ||
Type: AWS::EC2::KeyPair::KeyName | ||
Description: The key pair that controls access to the auto-scaled EC2 instances | ||
|
||
Ec2Subnet: | ||
Type: AWS::EC2::Subnet::Id | ||
Description: The subnet that hosts the auto-scaled EC2 instances | ||
|
||
Ec2IngressCidrIp: | ||
Type: String | ||
Description: The range of IP addresses that are granted SSH access to the | ||
autoscaled EC2 instances | ||
|
||
SnsEmail: | ||
Type: String | ||
Description: The email address that receives SNS notifications | ||
|
||
Mappings: | ||
RegionMapping: | ||
ap-south-1: { '64': ami-47205e28 } | ||
eu-west-2: { '64': ami-ed100689 } | ||
eu-west-1: { '64': ami-d7b9a2b1 } | ||
ap-northeast-2: { '64': ami-e21cc38c } | ||
ap-northeast-1: { '64': ami-3bd3c45c } | ||
sa-east-1: { '64': ami-87dab1eb } | ||
ca-central-1: { '64': ami-a7aa15c3 } | ||
ap-southeast-1: { '64': ami-77af2014 } | ||
ap-southeast-2: { '64': ami-10918173 } | ||
eu-central-1: { '64': ami-82be18ed } | ||
us-east-1: { '64': ami-a4c7edb2 } | ||
us-east-2: { '64': ami-8a7859ef } | ||
us-west-1: { '64': ami-327f5352 } | ||
us-west-2: { '64': ami-6df1e514 } | ||
|
||
Resources: | ||
Topic: | ||
Type: AWS::SNS::Topic | ||
|
||
Subscription: | ||
Type: AWS::SNS::Subscription | ||
Properties: | ||
Endpoint: !Ref SnsEmail | ||
Protocol: email | ||
TopicArn: !Ref Topic | ||
|
||
Policy: | ||
Type: AWS::IAM::ManagedPolicy | ||
Properties: | ||
PolicyDocument: | ||
Version: 2012-10-17 | ||
Statement: | ||
- Effect: Allow | ||
Resource: "*" | ||
Action: | ||
- autoscaling:CompleteLifecycleAction | ||
- sns:Publish | ||
|
||
LambdaRole: | ||
Type: AWS::IAM::Role | ||
Properties: | ||
ManagedPolicyArns: | ||
- arn:aws:iam::aws:policy/AmazonSSMFullAccess | ||
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole | ||
- !Ref Policy | ||
AssumeRolePolicyDocument: | ||
Version: '2012-10-17' | ||
Statement: | ||
- Effect: Allow | ||
Principal: | ||
Service: | ||
- lambda.amazonaws.com | ||
Action: sts:AssumeRole | ||
|
||
InstanceRole: | ||
Type: AWS::IAM::Role | ||
Properties: | ||
ManagedPolicyArns: | ||
- arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM | ||
- !Ref Policy | ||
AssumeRolePolicyDocument: | ||
Version: '2012-10-17' | ||
Statement: | ||
- Effect: Allow | ||
Principal: | ||
Service: | ||
- ec2.amazonaws.com | ||
Action: sts:AssumeRole | ||
|
||
InstanceProfile: | ||
Type: AWS::IAM::InstanceProfile | ||
Properties: | ||
Roles: | ||
- !Ref InstanceRole | ||
|
||
SecurityGroup: | ||
Type: AWS::EC2::SecurityGroup | ||
Properties: | ||
GroupDescription: Grants SSH access to the specified CidrIp | ||
SecurityGroupIngress: | ||
- IpProtocol: tcp | ||
FromPort: 22 | ||
ToPort: 22 | ||
CidrIp: !Ref Ec2IngressCidrIp | ||
|
||
LaunchConfiguration: | ||
Type: AWS::AutoScaling::LaunchConfiguration | ||
Properties: | ||
AssociatePublicIpAddress: true | ||
IamInstanceProfile: !GetAtt InstanceProfile.Arn | ||
ImageId: !FindInMap [ RegionMapping, !Ref 'AWS::Region', '64' ] | ||
InstanceType: t2.nano | ||
KeyName: !Ref Ec2KeyName | ||
SecurityGroups: [ !GetAtt SecurityGroup.GroupId ] | ||
UserData: | ||
Fn::Base64: !Sub | | ||
#!/bin/bash | ||
sudo yum install amazon-ssm-agent -y | ||
sudo /sbin/start amazon-ssm-agent | ||
|
||
AutoScalingGroup: | ||
Type: AWS::AutoScaling::AutoScalingGroup | ||
Properties: | ||
VPCZoneIdentifier: [!Ref Ec2Subnet] | ||
DesiredCapacity: 2 | ||
LaunchConfigurationName: !Ref LaunchConfiguration | ||
MaxSize: 3 | ||
MinSize: 1 | ||
|
||
LifecycleHook: | ||
Type: AWS::AutoScaling::LifecycleHook | ||
Properties: | ||
AutoScalingGroupName: !Ref AutoScalingGroup | ||
LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING | ||
|
||
Bucket: | ||
Type: AWS::S3::Bucket | ||
Properties: | ||
AccessControl: Private | ||
VersioningConfiguration: | ||
Status: Suspended | ||
|
||
Document: | ||
Type: AWS::SSM::Document | ||
Properties: | ||
Content: !Sub | | ||
{ | ||
"schemaVersion": "1.2", | ||
"description": "Backup logs to S3", | ||
"parameters": {}, | ||
"runtimeConfig": { | ||
"aws:runShellScript": { | ||
"properties": [ | ||
{ | ||
"id": "0.aws:runShellScript", | ||
"runCommand": [ | ||
"", | ||
"ASGNAME='${AutoScalingGroup}'", | ||
"LIFECYCLEHOOKNAME='${LifecycleHook}'", | ||
"BACKUPDIRECTORY='/var/log'", | ||
"S3BUCKET='${Bucket}'", | ||
"SNSTARGET='${Topic}'", | ||
"INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id)", | ||
"REGION=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)", | ||
"REGION=${!REGION::-1}", | ||
"HOOKRESULT='CONTINUE'", | ||
"MESSAGE=''", | ||
"", | ||
"tar -cf /tmp/${!INSTANCEID}.tar $BACKUPDIRECTORY &> /tmp/backup", | ||
"if [ $? -ne 0 ]", | ||
"then", | ||
" MESSAGE=$(cat /tmp/backup)", | ||
"else", | ||
" aws s3 cp /tmp/${!INSTANCEID}.tar s3://${!S3BUCKET}/${!INSTANCEID}/ &> /tmp/backup", | ||
" MESSAGE=$(cat /tmp/backup)", | ||
"fi", | ||
"", | ||
"aws sns publish --subject 'ASG Backup' --message \"$MESSAGE\" --target-arn ${!SNSTARGET} --region ${!REGION}", | ||
"aws autoscaling complete-lifecycle-action --lifecycle-hook-name ${!LIFECYCLEHOOKNAME} --auto-scaling-group-name ${!ASGNAME} --lifecycle-action-result ${!HOOKRESULT} --instance-id ${!INSTANCEID} --region ${!REGION}" | ||
] | ||
} | ||
] | ||
} | ||
} | ||
} | ||
Function: | ||
Type: AWS::Lambda::Function | ||
Properties: | ||
Code: lambda_backup.py | ||
Handler: lambda_backup.lambda_handler | ||
Role: !GetAtt LambdaRole.Arn | ||
Runtime: python2.7 | ||
Environment: | ||
Variables: | ||
DOCUMENT_NAME: !Ref Document | ||
|
||
Permission: | ||
Type: AWS::Lambda::Permission | ||
Properties: | ||
Action: "lambda:InvokeFunction" | ||
FunctionName: !GetAtt Function.Arn | ||
Principal: events.amazonaws.com | ||
|
||
Rule: | ||
Type: AWS::Events::Rule | ||
Properties: | ||
EventPattern: !Sub | | ||
{ | ||
"source": [ "aws.autoscaling" ], | ||
"detail": { | ||
"LifecycleTransition": ["autoscaling:EC2_INSTANCE_TERMINATING"] | ||
} | ||
} | ||
Targets: | ||
- Arn: !GetAtt Function.Arn | ||
Id: target |