AWS SSM automation to build Golden AMI

I know you are getting the first question in your mind, what do you mean by Golden AMI? A golden AMI is an AMI that contains the latest security patches, software, configuration, and software agents that you need to install for logging, security maintenance, and performance monitoring. We will use this preconfigure Golden AMI to launch an EC2 instance. Below are the steps to do this automation:

  • Launch an instance from Source AMI mentioned in SSM Parameter Store
  • Executes SSM Run Command that applies the vendor updates to the instance
  • Stops the instance
  • Creates a new AMI
  • Encrypt the AMI
  • Tag the AMI
  • Update the parameter store using Lambda
  • Delete unencrypted AMI
  • Terminates the original instance

Pre-Requisites

SSM Parameter Store:

  • Parameter Name: /GoldenAMI/Linux/RedHat-7/source (This parameter is used to store plane Source AMIid)
  • Parameter Name: /GoldenAMI/Linux/RedHat-7/latest (This parameter is to store latest AMIid after performing SSM automation steps)

RoleName: lambda-ssm-role: This role is required to execute Lambda function

  • Permissions: Managed Policies AWSLambdaExecute and AmazonSSMFullAccess

RoleName: ManagedInstanceRole: - An EC2 Role to allow SSM to start instances, create images etc,

  • Permissions: Managed Policies AmazonEC2RoleforSSM

RoleName: AutomationServiceRole: - An EC2 Role to Allow SSM to run documents and allow it to assume ManagedInstanceRole we just created.

Note: I have been created a pre-requisite with the Cloudformation template. Here is the attached code.

AWSTemplateFormatVersion: '2010-09-09'
Description: AWS CloudFormation template for GoldenAmi Automation with SSM


## This is a Lambda SSM role creation
Resources:
  LambdaSSMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            -  lambda.amazonaws.com
          Action: sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AWSLambdaExecute
      - arn:aws:iam::aws:policy/AmazonSSMFullAccess
      - arn:aws:iam::aws:policy/AmazonS3FullAccess
      Path: "/"
      
## An EC2 Role to allow SSM to start instances, create images etc      
  ManagedInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ssm.amazonaws.com
            - ec2.amazonaws.com
          Action: sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
      - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
      Path: "/"


  ManagedInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: "/"
      Roles:
      - !Ref ManagedInstanceRole
      InstanceProfileName: ManagedInstanceProfile 


#An EC2 Role to Allow SSM to run documents and allow it to assume ManagedInstanceRole we just created      
  AutomationServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ssm.amazonaws.com
            - ec2.amazonaws.com
          Action: sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole
      - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
      Path: "/"
      RoleName: AutomationServiceRole
      Policies:
      - PolicyName: passrole
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - iam:PassRole
            Resource:
            - !GetAtt ManagedInstanceRole.Arn


#Create Parameter for Golden AMI            
  GoldenAmiParameterSource:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /GoldenAMI/Linux/RedHat-7/source
      Type: String
      Value: ami-039a49e70ea773ffc
      Description: SSM Parameter to store AMI value.
      Tags:
        Environment: DEV


  GoldenAmiParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /GoldenAMI/Linux/RedHat-7/latest
      Type: String
      Value: to-be-updated
      Description: SSM Parameter to store AMI value.
      Tags:
        Environment: DEV         

Create Lambda to update SSM Parameter Store

This function will help us to automatically update the parameter store with the latest AMI when the Automation Document successfully creates a new image.

  1. Lambda Function Name: Choose it as Automation-UpdateSsmParam. If you change here, update the Automation Document also with the same name. Runtime should be Python 2.7
  2. Choose the lambda-ssm-role you created earlier.
  3. The code for lambda function is provided in this file Automation-UpdateSsmParam.py

Note: Here is the attached Lambda python code to update the SSM parameter.

from __future__ import print_function


import json
import boto3


print('Loading function')




#Updates an SSM parameter
#Expects parameterName, parameterValue
def lambda_handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))


    # get SSM client
    client = boto3.client('ssm')


    #confirm  parameter exists before updating it
    response = client.describe_parameters(
       Filters=[
          {
           'Key': 'Name',
           'Values': [ event['parameterName'] ]
          },
        ]
    )


    if not response['Parameters']:
        print('No such parameter')
        return 'SSM parameter not found.'


    #if parameter has a Descrition field, update it PLUS the Value
    if 'Description' in response['Parameters'][0]:
        description = response['Parameters'][0]['Description']
        
        response = client.put_parameter(
          Name=event['parameterName'],
          Value=event['parameterValue'],
          Description=description,
          Type='String',
          Overwrite=True
        )
    
    #otherwise just update Value
    else:
        response = client.put_parameter(
          Name=event['parameterName'],
          Value=event['parameterValue'],
          Type='String',
          Overwrite=True
        )
        
    reponseString = 'Updated parameter %s with value %s.' % (event['parameterName'], event['parameterValue'])
        
    return reponseString

Create custom Automation Document

  1. Create Document, Give a Name, like Bake-GoldenAMI-Linux
  2. Add the contents of the Bake-GoldenAMI-Linux.json file in the field.
No alt text provided for this image
  1. After successful creation of the document, you should be able to view, modify versions,
No alt text provided for this image

Note: Here is the attached Bake-GoldenAMI-Linux.json code

description: >-
  Create a Golden AMI with Linux distribution packages(ClamAV) and Amazon
  software(SSM & Inspector). For details,see
schemaVersion: '0.3'
assumeRole: '{{AutomationAssumeRole}}'
outputs:
  - createImage.ImageId
parameters:
  SourceAmiId:
    type: String
    description: (Required) The source Amazon Machine Image ID.
    default: '{{ssm:/GoldenAMI/Linux/RedHat-7/source}}'
  InstanceIamRole:
    type: String
    description: >-
      (Required) The name of the role that enables Systems Manager (SSM) to
      manage the instance.
    default: ManagedInstanceProfile
  AutomationAssumeRole:
    type: String
    description: >-
      (Required) The ARN of the role that allows Automation to perform the
      actions on your behalf.
    default: 'arn:aws:iam::{{global:ACCOUNT_ID}}:role/AutomationServiceRole'
  SubnetId:
    type: String
    description: (Required) The subnet that the created instance will be placed into.
    default: ''
  TargetAmiName:
    type: String
    description: >-
      (Optional) The name of the new AMI that will be created. Default is a
      system-generated string including the source AMI id, and the creation time
      and date.
    default: 'GoldenAMI-RH-7_on_{{global:DATE_TIME}}'
  InstanceType:
    type: String
    description: >-
      (Optional) Type of instance to launch as the workspace host. Instance
      types vary by region. Default is t2.micro.
    default: t2.micro
  PreUpdateScript:
    type: String
    description: >-
      (Optional) URL of a script to run before updates are applied. Default
      ("none") is to not run a script.
    default: none
  PostUpdateScript:
    type: String
    description: >-
      (Optional) URL of a script to run after package updates are applied.
      Default ("none") is to not run a script.
    default: ''
  IncludePackages:
    type: String
    description: >-
      (Optional) Only update these named packages. By default ("all"), all
      available updates are applied.
    default: all
  ExcludePackages:
    type: String
    description: >-
      (Optional) Names of packages to hold back from updates, under all
      conditions. By default ("none"), no package is excluded.
    default: none
  lambdaFunctionName:
    type: String
    description: >-
      (Required) The name of the lambda function. Default ('none') is to not run
      a script.
    default: Automation-UpdateSsmParam
mainSteps:
  - name: launchInstance
    action: 'aws:runInstances'
    maxAttempts: 3
    timeoutSeconds: 1200
    onFailure: Abort
    inputs:
      ImageId: '{{SourceAmiId}}'
      InstanceType: '{{InstanceType}}'
      SubnetId: '{{ SubnetId }}'
      UserData: >-
        IyEvYmluL2Jhc2gNCg0KZnVuY3Rpb24gZ2V0X2NvbnRlbnRzKCkgew0KICAgIGlmIFsgLXggIiQod2hpY2ggY3VybCkiIF07IHRoZW4NCiAgICAgICAgY3VybCAtcyAtZiAiJDEiDQogICAgZWxpZiBbIC14ICIkKHdoaWNoIHdnZXQpIiBdOyB0aGVuDQogICAgICAgIHdnZXQgIiQxIiAtTyAtDQogICAgZWxzZQ0KICAgICAgICBkaWUgIk5vIGRvd25sb2FkIHV0aWxpdHkgKGN1cmwsIHdnZXQpIg0KICAgIGZpDQp9DQoNCnJlYWRvbmx5IElERU5USVRZX1VSTD0iaHR0cDovLzE2OS4yNTQuMTY5LjI1NC8yMDE2LTA2LTMwL2R5bmFtaWMvaW5zdGFuY2UtaWRlbnRpdHkvZG9jdW1lbnQvIg0KcmVhZG9ubHkgVFJVRV9SRUdJT049JChnZXRfY29udGVudHMgIiRJREVOVElUWV9VUkwiIHwgYXdrIC1GXCIgJy9yZWdpb24vIHsgcHJpbnQgJDQgfScpDQpyZWFkb25seSBERUZBVUxUX1JFR0lPTj0idXMtZWFzdC0xIg0KcmVhZG9ubHkgUkVHSU9OPSIke1RSVUVfUkVHSU9OOi0kREVGQVVMVF9SRUdJT059Ig0KDQpyZWFkb25seSBTQ1JJUFRfTkFNRT0iYXdzLWluc3RhbGwtc3NtLWFnZW50Ig0KIFNDUklQVF9VUkw9Imh0dHBzOi8vYXdzLXNzbS1kb3dubG9hZHMtJFJFR0lPTi5zMy5hbWF6b25hd3MuY29tL3NjcmlwdHMvJFNDUklQVF9OQU1FIg0KDQppZiBbICIkUkVHSU9OIiA9ICJjbi1ub3J0aC0xIiBdOyB0aGVuDQogIFNDUklQVF9VUkw9Imh0dHBzOi8vYXdzLXNzbS1kb3dubG9hZHMtJFJFR0lPTi5zMy5jbi1ub3J0aC0xLmFtYXpvbmF3cy5jb20uY24vc2NyaXB0cy8kU0NSSVBUX05BTUUiDQpmaQ0KDQppZiBbICIkUkVHSU9OIiA9ICJ1cy1nb3Ytd2VzdC0xIiBdOyB0aGVuDQogIFNDUklQVF9VUkw9Imh0dHBzOi8vYXdzLXNzbS1kb3dubG9hZHMtJFJFR0lPTi5zMy11cy1nb3Ytd2VzdC0xLmFtYXpvbmF3cy5jb20vc2NyaXB0cy8kU0NSSVBUX05BTUUiDQpmaQ0KDQpjZCAvdG1wDQpGSUxFX1NJWkU9MA0KTUFYX1JFVFJZX0NPVU5UPTMNClJFVFJZX0NPVU5UPTANCg0Kd2hpbGUgWyAkUkVUUllfQ09VTlQgLWx0ICRNQVhfUkVUUllfQ09VTlQgXSA7IGRvDQogIGVjaG8gQVdTLVVwZGF0ZUxpbnV4QW1pOiBEb3dubG9hZGluZyBzY3JpcHQgZnJvbSAkU0NSSVBUX1VSTA0KICBnZXRfY29udGVudHMgIiRTQ1JJUFRfVVJMIiA+ICIkU0NSSVBUX05BTUUiDQogIEZJTEVfU0laRT0kKGR1IC1rIC90bXAvJFNDUklQVF9OQU1FIHwgY3V0IC1mMSkNCiAgZWNobyBBV1MtVXBkYXRlTGludXhBbWk6IEZpbmlzaGVkIGRvd25sb2FkaW5nIHNjcmlwdCwgc2l6ZTogJEZJTEVfU0laRQ0KICBpZiBbICRGSUxFX1NJWkUgLWd0IDAgXTsgdGhlbg0KICAgIGJyZWFrDQogIGVsc2UNCiAgICBpZiBbWyAkUkVUUllfQ09VTlQgLWx0IE1BWF9SRVRSWV9DT1VOVCBdXTsgdGhlbg0KICAgICAgUkVUUllfQ09VTlQ9JCgoUkVUUllfQ09VTlQrMSkpOw0KICAgICAgZWNobyBBV1MtVXBkYXRlTGludXhBbWk6IEZpbGVTaXplIGlzIDAsIHJldHJ5Q291bnQ6ICRSRVRSWV9DT1VOVA0KICAgIGZpDQogIGZpIA0KZG9uZQ0KDQppZiBbICRGSUxFX1NJWkUgLWd0IDAgXTsgdGhlbg0KICBjaG1vZCAreCAiJFNDUklQVF9OQU1FIg0KICBlY2hvIEFXUy1VcGRhdGVMaW51eEFtaTogUnVubmluZyBVcGRhdGVTU01BZ2VudCBzY3JpcHQgbm93IC4uLi4NCiAgLi8iJFNDUklQVF9OQU1FIiAtLXJlZ2lvbiAiJFJFR0lPTiINCmVsc2UNCiAgZWNobyBBV1MtVXBkYXRlTGludXhBbWk6IFVuYWJsZSB0byBkb3dubG9hZCBzY3JpcHQsIHF1aXR0aW5nIC4uLi4NCmZp
      MinInstanceCount: 1
      MaxInstanceCount: 1
      IamInstanceProfileName: '{{InstanceIamRole}}'
  - name: updateOSSoftware
    action: 'aws:runCommand'
    maxAttempts: 3
    timeoutSeconds: 3600
    onFailure: Abort
    inputs:
      DocumentName: AWS-RunShellScript
      InstanceIds:
        - '{{launchInstance.InstanceIds}}'
      Parameters:
        commands:
          - set -e
          - '[ -x "$(which wget)" ] && get_contents=''wget $1 -O -'''
          - '[ -x "$(which curl)" ] && get_contents=''curl -s -f $1'''
          - >-
            eval $get_contents
            https://aws-ssm-downloads-{{global:REGION}}.s3.amazonaws.com/scripts/aws-update-linux-instance
            > /tmp/aws-update-linux-instance
          - chmod +x /tmp/aws-update-linux-instance
          - >-
            /tmp/aws-update-linux-instance --pre-update-script
            '{{PreUpdateScript}}' --post-update-script '{{PostUpdateScript}}'
            --include-packages '{{IncludePackages}}' --exclude-packages
            '{{ExcludePackages}}' 2>&1 | tee /tmp/aws-update-linux-instance.log
  - name: installCustomizations
    action: 'aws:runCommand'
    maxAttempts: 3
    timeoutSeconds: 600
    onFailure: Abort
    inputs:
      DocumentName: AWS-RunShellScript
      InstanceIds:
        - '{{launchInstance.InstanceIds}}'
      Parameters:
        commands:
          - >-
            curl -O
            http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
          - rpm -ivh epel-release-latest-7.noarch.rpm
          - yum -y install httpd
          - systemctl enable httpd
          - systemctl restart httpd
          - sudo yum --enablerepo=epel install -y clamav
          - yum-config-manager --disable epel
          - cat /etc/motd >> /var/www/html/index.html
          - echo 'Welcome' >> /var/www/html/index.html
          - cat > /etc/motd <<- EOF
          - '           __  __ _                 _                        _   _             '
          - '     /\   |  \/  (_)     /\        | |                      | | (_)            '
          - '    /  \  | \  / |_     /  \  _   _| |_ ___  _ __ ___   __ _| |_ _  ___  _ __  '
          - '   / /\ \ | |\/| | |   / /\ \| | | | __/ _ \| ''_ ` _ \ / _` | __| |/ _ \| ''_ \ '
          - '  / ____ \| |  | | |  / ____ \ |_| | || (_) | | | | | | (_| | |_| | (_) | | | |'
          - ' /_/    \_\_|  |_|_| /_/    \_\__,_|\__\___/|_| |_| |_|\__,_|\__|_|\___/|_| |_|'
          - '   '
          - '   '
          - EOF
  - name: installInspectorAgent
    action: 'aws:runCommand'
    maxAttempts: 3
    timeoutSeconds: 600
    onFailure: Abort
    inputs:
      DocumentName: AmazonInspector-ManageAWSAgent
      InstanceIds:
        - '{{launchInstance.InstanceIds}}'
      Parameters:
        Operation: Install
  - name: installUnifiedCloudWatchAgent
    action: 'aws:runCommand'
    maxAttempts: 3
    timeoutSeconds: 1200
    onFailure: Abort
    inputs:
      DocumentName: AWS-ConfigureAWSPackage
      InstanceIds:
        - '{{launchInstance.InstanceIds}}'
      Parameters:
        name: AmazonCloudWatchAgent
        action: Install
  - name: stopInstance
    action: 'aws:changeInstanceState'
    maxAttempts: 3
    timeoutSeconds: 1200
    onFailure: Abort
    inputs:
      InstanceIds:
        - '{{launchInstance.InstanceIds}}'
      DesiredState: stopped
  - name: createImage
    action: 'aws:createImage'
    maxAttempts: 3
    onFailure: Abort
    inputs:
      InstanceId: '{{launchInstance.InstanceIds}}'
      ImageName: '{{TargetAmiName}}'
      NoReboot: true
      ImageDescription: >-
        AMI Generated by EC2 Automation on {{global:DATE_TIME}} from
        {{SourceAmiId}}
  - name: createEncryptedCopy
    action: 'aws:copyImage'
    maxAttempts: 3
    onFailure: Abort
    inputs:
      SourceImageId: '{{createImage.ImageId}}'
      SourceRegion: '{{global:REGION}}'
      ImageName: 'Encrypted-{{TargetAmiName}}'
      ImageDescription: >-
        Encrypted GoldenAMI by SSM Automation on {{global:DATE_TIME}} from
        source AMI {{createImage.ImageId}}
      Encrypted: true
  - name: createTagsForEncryptedImage
    action: 'aws:createTags'
    maxAttempts: 1
    onFailure: Continue
    inputs:
      ResourceType: EC2
      ResourceIds:
        - '{{createEncryptedCopy.ImageId}}'
      Tags:
        - Key: Automation-Id
          Value: '{{automation:EXECUTION_ID}}'
        - Key: Owner
          Value: Mystique
        - Key: SourceAMI
          Value: '{{SourceAmiId}}'
        - Key: Amazon-Inspector
          Value: 'true'
        - Key: Amazon-SSM
          Value: 'true'
        - Key: Encrypted
          Value: 'true'
  - name: updateSsmParam
    action: 'aws:invokeLambdaFunction'
    timeoutSeconds: 1200
    maxAttempts: 1
    onFailure: Abort
    inputs:
      FunctionName: Automation-UpdateSsmParam
      Payload: >-
        {"parameterName":"/GoldenAMI/Linux/RedHat-7/latest",
        "parameterValue":"{{createEncryptedCopy.ImageId}}"}
  - name: terminateInstance
    action: 'aws:changeInstanceState'
    maxAttempts: 3
    onFailure: Continue
    inputs:
      InstanceIds:
        - '{{launchInstance.InstanceIds}}'
      DesiredState: terminated
  - name: deleteUnEcryptedImage
    action: 'aws:deleteImage'
    maxAttempts: 3
    timeoutSeconds: 180
    onFailure: Abort
    inputs:
      ImageId: '{{createImage.ImageId}}'

After completing above setup follow the below steps to test that automation successfully working or not.

  1. Go to AWS services and choose Systems Manager Services, Automations.
  2. Choose Execution automation and pick the one we just created. The easiest way is to search by "Owned by Me".
  3. Then finally click on the execute automation button and check the output.


Trupti Tawde

Executive Assistant |Co-ordinator|MIS|Salesforce | Sales Operation|AR-Credit Control

5y

Very nice

Like
Reply

To view or add a comment, sign in

More articles by Rajesh Gunjal

Others also viewed

Explore content categories