├── sns-subscription.yml ├── event.yml ├── README.md ├── main.yml ├── roles.yml ├── lambda.yml └── ssm.yml /sns-subscription.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Subscribes previously created Lambda function to the official Linux AMI SNS topic. RUN ONLY IN us-east-1 REGION" 3 | Parameters: 4 | SSMLambdaARN: 5 | Type: "String" 6 | NewAMITopicARN: 7 | Type: "String" 8 | Default: "arn:aws:sns:us-east-1:137112412989:amazon-linux-ami-updates" 9 | Resources: 10 | AMITopicSubscription: 11 | Type: "AWS::SNS::Subscription" 12 | Properties: 13 | Endpoint: !Ref "SSMLambdaARN" 14 | Protocol: lambda 15 | TopicArn: !Ref "NewAMITopicARN" -------------------------------------------------------------------------------- /event.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Creates SNS topic for SSM service notifications and subsribe to it by email" 3 | Parameters: 4 | NotificationEmail: 5 | Description: "Notification email address for SSM updates" 6 | AllowedPattern: "^.+@.+$" 7 | Type: "String" 8 | MinLength: 5 9 | Resources: 10 | SSMTopic: 11 | Type: "AWS::SNS::Topic" 12 | Properties: 13 | Subscription: 14 | - 15 | Endpoint: !Ref "NotificationEmail" 16 | Protocol: "email-json" 17 | SSMEventRule: 18 | Type: "AWS::Events::Rule" 19 | Properties: 20 | Description: "Send SSM state change notifications to SNS" 21 | EventPattern: 22 | source: 23 | - "aws.ssm" 24 | detail-type: 25 | - "EC2 Automation Execution Status-change Notification" 26 | State: "ENABLED" 27 | Targets: 28 | - Arn: !Ref "SSMTopic" 29 | Id: "SNSTopic" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssm-ami-automation 2 | Automated AMI creation using SSM 3 | 4 | Recently AWS announced the induction of SNS topic to keep the community informed about Amazon AMI releases. Following solution integrates this feature with Amazon EC2 Systems Manager to provide Amazon native solution to build AMIs. 5 | 6 | Workflow 7 | --------- 8 | 9 | ![ssm automation](http://bigm-and-pie.com/wp-content/uploads/2017/04/ssm.png) 10 | 11 | 1. The lambda function is subscribed to the official SNS topic 12 | `arn:aws:sns:us-east-1:137112412989:amazon-linux-ami-updates` 13 | 2. The SSM automation document is triggered with the latest AMI when a new SNS notification is received 14 | 3. Automation document launches a new EC2 instance and runs required configuration commands. 15 | 4. Once the configuration process has completed, SSM stops the instance and creates a new AMI. When the AMI is ready, automation terminates the stopped instance. 16 | 5. CloudWatch Event gets notified when the SSM has finished or failed the execution. It sends a message to the SNS topic which is subscribed by the notification email. 17 | 18 | Creating stack 19 | ------------- 20 | 21 | 1. Upload all cloudformation templates except `main.yml` and `sns-subscription.yml` to the s3 bucket 22 | 2. Run `main.yml` in the desired region and provide s3 bucket name where cloudformations were uploaded 23 | 3. Run `sns-subscription` in the us-east-1 region and specify lambda ARN from `SSMLambdaARN` output created in step 2 24 | -------------------------------------------------------------------------------- /main.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "AWS CloudFormation template to automate AMI patching process" 3 | Outputs: 4 | SSMLambdaARN: 5 | Value: !GetAtt "SSMLambda.Outputs.SSMLambdaARN" 6 | Parameters: 7 | SubnetId: 8 | Description: "VPC Subnet ID" 9 | Type: "AWS::EC2::Subnet::Id" 10 | NewAMITopicARN: 11 | Description: "New AMI SNS topic ARN" 12 | Type: "String" 13 | Default: "arn:aws:sns:us-east-1:137112412989:amazon-linux-ami-updates" 14 | NotificationEmail: 15 | Description: "Notification email address for SSM updates" 16 | AllowedPattern: "^.+@.+$" 17 | Type: "String" 18 | MinLength: 5 19 | S3BucketName: 20 | Type: "String" 21 | MinLength: 3 22 | Resources: 23 | SSMRoles: 24 | Type: "AWS::CloudFormation::Stack" 25 | Properties: 26 | TemplateURL: !Sub "https://s3.amazonaws.com/${S3BucketName}/roles.yml" 27 | SSMDocument: 28 | Type: "AWS::CloudFormation::Stack" 29 | Properties: 30 | TemplateURL: !Sub "https://s3.amazonaws.com/${S3BucketName}/ssm.yml" 31 | Parameters: 32 | SubnetId: !Ref "SubnetId" 33 | EC2InstanceProfile: !GetAtt "SSMRoles.Outputs.EC2InstanceProfile" 34 | SSMRoleARN: !GetAtt "SSMRoles.Outputs.SSMRoleARN" 35 | SSMLambda: 36 | Type: "AWS::CloudFormation::Stack" 37 | Properties: 38 | TemplateURL: !Sub "https://s3.amazonaws.com/${S3BucketName}/lambda.yml" 39 | Parameters: 40 | NewAMITopicARN: !Ref "NewAMITopicARN" 41 | LambdaRoleARN: !GetAtt "SSMRoles.Outputs.LambdaRoleARN" 42 | SSMRoleARN: !GetAtt "SSMRoles.Outputs.SSMRoleARN" 43 | SSMAutomationDocument: !GetAtt "SSMDocument.Outputs.SSMAutomationDocument" 44 | SSMEvent: 45 | Type: "AWS::CloudFormation::Stack" 46 | Properties: 47 | TemplateURL: !Sub "https://s3.amazonaws.com/${S3BucketName}/event.yml" 48 | Parameters: 49 | NotificationEmail: !Ref "NotificationEmail" -------------------------------------------------------------------------------- /roles.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Creates IAM roles requiredSSMLambdaARN for SSM" 3 | Outputs: 4 | EC2InstanceProfile: 5 | Value: !Ref "EC2InstanceProfile" 6 | SSMRoleARN: 7 | Value: !GetAtt "SSMRole.Arn" 8 | LambdaRoleARN: 9 | Value: !GetAtt "LambdaRole.Arn" 10 | Resources: 11 | EC2InstanceRole: 12 | Type: "AWS::IAM::Role" 13 | Properties: 14 | AssumeRolePolicyDocument: 15 | Version: "2012-10-17" 16 | Statement: 17 | - Effect: "Allow" 18 | Principal: 19 | Service: 20 | - "ssm.amazonaws.com" 21 | - "ec2.amazonaws.com" 22 | Action: "sts:AssumeRole" 23 | ManagedPolicyArns: 24 | - "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM" 25 | Path: "/" 26 | EC2InstanceProfile: 27 | Type: "AWS::IAM::InstanceProfile" 28 | Properties: 29 | Path: "/" 30 | Roles: 31 | - !Ref "EC2InstanceRole" 32 | SSMRole: 33 | Type: "AWS::IAM::Role" 34 | Properties: 35 | AssumeRolePolicyDocument: 36 | Version: "2012-10-17" 37 | Statement: 38 | - Effect: "Allow" 39 | Principal: 40 | Service: 41 | - "ssm.amazonaws.com" 42 | - "ec2.amazonaws.com" 43 | Action: "sts:AssumeRole" 44 | ManagedPolicyArns: 45 | - "arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole" 46 | Path: "/" 47 | Policies: 48 | - PolicyName: "passrole" 49 | PolicyDocument: 50 | Version: "2012-10-17" 51 | Statement: 52 | - Effect: "Allow" 53 | Action: 54 | - "iam:PassRole" 55 | Resource: 56 | - !GetAtt "EC2InstanceRole.Arn" 57 | LambdaRole: 58 | Type: "AWS::IAM::Role" 59 | Properties: 60 | AssumeRolePolicyDocument: 61 | Version: "2012-10-17" 62 | Statement: 63 | - Effect: "Allow" 64 | Principal: 65 | Service: 66 | - "lambda.amazonaws.com" 67 | Action: "sts:AssumeRole" 68 | ManagedPolicyArns: 69 | - "arn:aws:iam::aws:policy/AWSLambdaExecute" 70 | - "arn:aws:iam::aws:policy/AmazonSSMFullAccess" 71 | - "arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole" 72 | Path: "/" 73 | Policies: 74 | - PolicyName: "passrole" 75 | PolicyDocument: 76 | Version: "2012-10-17" 77 | Statement: 78 | - Effect: "Allow" 79 | Action: 80 | - "iam:PassRole" 81 | Resource: 82 | - !GetAtt "SSMRole.Arn" -------------------------------------------------------------------------------- /lambda.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "Creates lambda function to trigger SSM automation and allows it to be invoked by SNS" 3 | Parameters: 4 | NewAMITopicARN: 5 | Description: "New AMI SNS topic ARN" 6 | Type: "String" 7 | Default: "arn:aws:sns:us-east-1:137112412989:amazon-linux-ami-updates" 8 | LambdaRoleARN: 9 | Description: "Lambda role ARN" 10 | Type: "String" 11 | SSMRoleARN: 12 | Description: "SSM role ARN" 13 | Type: "String" 14 | SSMAutomationDocument: 15 | Description: "Automation document name" 16 | Type: "String" 17 | Outputs: 18 | SSMLambdaARN: 19 | Value: !GetAtt "SSMLambda.Arn" 20 | Resources: 21 | SSMLambda: 22 | Type: "AWS::Lambda::Function" 23 | Properties: 24 | Handler: "index.lambda_handler" 25 | Runtime: "python2.7" 26 | Role: !Ref "LambdaRoleARN" 27 | Timeout: 15 28 | Environment: 29 | Variables: 30 | DocumentName: !Ref "SSMAutomationDocument" 31 | AutomationAssumeRoleARN: !Ref "SSMRoleARN" 32 | Code: 33 | ZipFile: | 34 | import os 35 | import boto3 36 | import logging 37 | import operator 38 | from datetime import datetime 39 | def lambda_handler(event, context): 40 | logger = logging.getLogger(__name__) 41 | logger.addHandler(logging.StreamHandler()) 42 | logger.setLevel(logging.INFO) 43 | ec2_client = boto3.client('ec2') 44 | ssm_client = boto3.client('ssm') 45 | 46 | response = ec2_client.describe_images(Owners=['amazon'], Filters=[{'Name': 'name', 'Values': ['amzn-ami-hvm-????.??.??.*-x86_64-gp2']}]) 47 | images = [{'ImageId':image['ImageId'], 'CreationDate': datetime.strptime(image['CreationDate'], '%Y-%m-%dT%H:%M:%S.%fZ')} for image in response['Images']] 48 | images = sorted(images, reverse=True, key=operator.itemgetter('CreationDate')) 49 | ami_id = images[0]['ImageId'] 50 | logger.info("ami-id: {}".format(ami_id)) 51 | response = ssm_client.start_automation_execution(DocumentName=os.environ['DocumentName'], 52 | Parameters={'SourceAmiId': [ami_id,], 'AutomationAssumeRoleARN': [os.environ['AutomationAssumeRoleARN'],]} 53 | ) 54 | logger.info(response) 55 | SSMLambdaInvokePermission: 56 | Type: "AWS::Lambda::Permission" 57 | Properties: 58 | FunctionName: !GetAtt "SSMLambda.Arn" 59 | Action: "lambda:InvokeFunction" 60 | Principal: "sns.amazonaws.com" 61 | SourceArn: !Ref "NewAMITopicARN" -------------------------------------------------------------------------------- /ssm.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: "AWS CloudFormation template to automate AMI patching process" 3 | Parameters: 4 | SubnetId: 5 | Description: "VPC Subnet ID" 6 | Type: "AWS::EC2::Subnet::Id" 7 | EC2InstanceProfile: 8 | Description: "EC2 instance profile name" 9 | Type: "String" 10 | SSMRoleARN: 11 | Description: "SSM service role ARN" 12 | Type: "String" 13 | Outputs: 14 | SSMAutomationDocument: 15 | Value: !Ref "SSMAutomationDocument" 16 | Resources: 17 | SSMAutomationDocument: 18 | Type: "AWS::SSM::Document" 19 | Properties: 20 | DocumentType: "Automation" 21 | Content: 22 | schemaVersion: "0.3" 23 | description: "Update a Linux AMI." 24 | assumeRole: "{{AutomationAssumeRoleARN}}" 25 | parameters: 26 | SourceAmiId: 27 | type: "String" 28 | description: "The source Amazon Machine Image ID." 29 | SubnetId: 30 | type: "String" 31 | description: "Subnet id" 32 | default: !Ref "SubnetId" 33 | InstanceProfileName: 34 | type: "String" 35 | description: "EC2 IAM profile that is allowed to perform RunCommand." 36 | default: !Ref "EC2InstanceProfile" 37 | AutomationAssumeRoleARN: 38 | type: "String" 39 | description: "Role under which to execute this automation." 40 | default: !Ref "SSMRoleARN" 41 | TargetAmiName: 42 | type: "String" 43 | description: "The name of the new AMI that will be created." 44 | default: ami-{{SourceAmiId}}-{{global:DATE_TIME}} 45 | InstanceType: 46 | type: "String" 47 | description: "Type of instance to launch as the workspace host." 48 | default: "t2.micro" 49 | mainSteps: 50 | - name: "launchInstance" 51 | action: "aws:runInstances" 52 | maxAttempts: 3 53 | timeoutSeconds: 1200 54 | onFailure: "Abort" 55 | inputs: 56 | SubnetId: "{{SubnetId}}" 57 | ImageId: "{{SourceAmiId}}" 58 | InstanceType: "{{InstanceType}}" 59 | UserData: IyEvYmluL2Jhc2gNCmNkIC90bXANCmN1cmwgaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL2VjMi1kb3dubG9hZHMtd2luZG93cy9TU01BZ2VudC9sYXRlc3QvbGludXhfYW1kNjQvYW1hem9uLXNzbS1hZ2VudC5ycG0gLW8gYW1hem9uLXNzbS1hZ2VudC5ycG0NCnl1bSBpbnN0YWxsIC15IGFtYXpvbi1zc20tYWdlbnQucnBt 60 | MinInstanceCount: 1 61 | MaxInstanceCount: 1 62 | IamInstanceProfileName: "{{InstanceProfileName}}" 63 | - name: "installRequiredPackages" 64 | action: "aws:runCommand" 65 | maxAttempts: 3 66 | timeoutSeconds: 1200 67 | onFailure: "Abort" 68 | inputs: 69 | DocumentName: "AWS-RunShellScript" 70 | InstanceIds: 71 | - "{{launchInstance.InstanceIds}}" 72 | Parameters: 73 | commands: 74 | - set -e 75 | - sudo yum update -y 76 | - sudo yum install -y awslogs 77 | - sudo yum -y install collectd 78 | - name: "stopInstance" 79 | action: "aws:changeInstanceState" 80 | maxAttempts: 3 81 | timeoutSeconds: 1200 82 | onFailure: "Abort" 83 | inputs: 84 | InstanceIds: 85 | - "{{launchInstance.InstanceIds}}" 86 | DesiredState: stopped 87 | - name: "createImage" 88 | action: "aws:createImage" 89 | maxAttempts: 3 90 | onFailure: "Abort" 91 | inputs: 92 | InstanceId: "{{launchInstance.InstanceIds}}" 93 | ImageName: "{{TargetAmiName}}" 94 | NoReboot: true 95 | ImageDescription: "AMI Generated from {{SourceAmiId}}" 96 | - name: "terminateInstance" 97 | action: "aws:changeInstanceState" 98 | maxAttempts: 3 99 | onFailure: "Continue" 100 | inputs: 101 | InstanceIds: 102 | - "{{launchInstance.InstanceIds}}" 103 | DesiredState: "terminated" 104 | outputs: 105 | - createImage.ImageId --------------------------------------------------------------------------------