AWS DevOps Blog

How to Create an AMI Builder with AWS CodeBuild and HashiCorp Packer

Written by AWS Solutions Architects Jason Barto and Heitor Lessa

It’s an operational and security best practice to create and maintain custom Amazon Machine Images. Because it’s also a best practice to maintain infrastructure as code, it makes sense to use automated tooling to script the creation and configuration of AMIs that are used to quickly launch Amazon EC2 instances.

In this first of two posts, we’ll use AWS CodeBuild to programmatically create AMIs for use in our environment. As a part of the AMI generation, we will apply OS patches, configure a banner statement, and install some common software, forming a solid base for future Amazon EC2-based deployments.

Requirements

You will need Git and a text editor.

Technologies

AWS CodeBuild is a fully managed build service that enables customers to compile source code, run tests, and produce software packages that are ready to deploy. It is elastic, scalable, and provides curated build environments for programming languages such as Java, Ruby, Python, Go, and Node.js. With AWS CodeBuild, you don’t need to provision, manage, and scale your own build servers. For more information, see the AWS CodeBuild User Guide.

HashiCorp Packer is an open source utility designed to automate the creation of identical virtual machine images for multiple platforms from a single source configuration. For more information, see the HashiCorp Packer documentation.

Getting Started

We need to host the AWS CodeBuild and HashiCorp Packer project somewhere. AWS CodeBuild can use Amazon S3, AWS CodeCommit, or GitHub as source code repositories. For this tutorial, sign in to the AWS Management Console and create an AWS CodeCommit repository. If you have not used AWS CodeCommit before, we recommend that you start with the Git with AWS CodeCommit Tutorial.

Create an AWS CodeBuild Project in the Console

Create an AWS CodeBuild project. It will be used later to build our HashiCorp Packer template.

  1. From the AWS Management Console, open the AWS CodeBuild console.
  2. If you have no AWS CodeBuild projects, choose Get Started or, on the Build Projects page, choose Create project.
  3. On the Create project page, type a name for your AWS CodeBuild project (for example, AMI_Builder).
  4. For Source provider, choose AWS CodeCommit. From the Repository drop-down list, select the repository you created in the Getting Started step.

AWS CodeBuild uses containers to execute the project’s build instructions and build your project. You can specify custom container images hosted in Amazon EC2 Container Registry or DockerHub, but this tutorial uses the default managed Ubuntu container.

  1. For Environment Image, select Use an image managed by AWS CodeBuild. For Operating system, choose Ubuntu. For Runtime, choose Base. For Version, choose aws/codebuild/ubuntu-base:14.04.

The next section of the page tells AWS CodeBuild where to find commands that will be executed as a part of the build. For this project, the build commands are stored in the buildspec.yml file in your repository.

  1. For Build specification, accept the default.

CodeBuild project configuration

The buildspec.yml file will use HashiCorp Packer to execute a template and generate an Amazon EC2 AMI. There is no binary output or build result that will be used as an artifact.

  1. For Artifacts type, choose No artifacts.

In the Service Role section the service role you select here will provide the AWS CodeBuild container with permissions to your AWS account. HashiCorp Packer needs permissions to create a temporary EC2 instance and an AMI, delete an EC2 instance, and perform other EC2-related actions. .

For the purposes of this post, allow AWS CodeBuild to create a service role for you. You will update it later with the permissions required by HashiCorp Packer.

  1. For Service Role choose the default of Create a service role in your account.
  2. Choose Continue.
  3. Review the settings on the next page, and then choose Save.

Update Service Role Permissions

You have now created an AWS CodeBuild project that is connected to an AWS CodeCommit repository and is ready to build our source code. Now we need to grant AWS CodeBuild the additional permissions it will need to create, delete, and image an EC2 instance.

  1. Open the IAM console and click the AWS CodeBuild service role, created in the last section, to display its summary page.
  2. On the summary page, under Permissions, expand Inline policies, and click the link to create a policy.
  3. Choose Custom Policy, and then choose Select.
  4. Copy and paste the IAM policy from the HashiCorp Packer documentation into the text area. Type a name for the policy (for example, codebuild-AMI_Builder-ec2-permissions).
  5. Choose Validate Policy, and then choose Apply Policy to link the policy with the service role.

CodeBuild project permissions

AWS CodeBuild should now have the permissions required to create AMIs.

Initial Commit

The next step is to create the HashiCorp Packer template and build specification. Check out a copy of the Git repository and create two files:

  • The HashiCorp Packer template, amazon-linux_packer-template.json.
  • The AWS CodeBuild configuration file, buildspec.yml.

Create a HashiCorp Packer Template

A HashiCorp Packer template is a JSON-formatted document that provides HashiCorp Packer with information about how to build a machine image. A HashiCorp Packer template can be composed of many keys. In this post, the focus is on the builders and provisioners keys. For more information, see Templates on the HashiCorp Packer website.

Using a text editor, create a HashiCorp Packer template named amazon-linux_packer-template.json that contains three sections: variables, builders, and provisioners.

The variables section defines two variables, aws_region and aws_ami_name. These variables can be reused later in the template. They are defined by using an environment variable {{env `AWS_REGION`}} and an internal HashiCorp Packer function {{isotime \"02Jan2006\"}} . For more information about HashiCorp Packer functions, see Template Engine on the HashiCorp Packer website.

The builders section configures the amazon-ebs builder to deploy an instance into the same region in which AWS CodeBuild is running, using a t2.micro instance, and to connect to the instance with a username of ec2-user. The source_ami_filter is being used to find the latest version of Amazon Linux available for the target region. The selected source AMI will be the base for your custom AMI. After the EC2 instance has been provisioned, amazon-ebs will create an AMI using the value of the aws_ami_name variable as the AMI name.

{
    "variables": {
        "aws_region": "{{env `AWS_REGION`}}",
        "aws_ami_name": "amazon-linux_{{isotime \"02Jan2006\"}}"
    },

    "builders": [{
        "type": "amazon-ebs",
        "region": "{{user `aws_region`}}",
        "instance_type": "t2.micro",
        "ssh_username": "ec2-user",
        "ami_name": "{{user `aws_ami_name`}}",
        "ami_description": "Customized Amazon Linux",
        "associate_public_ip_address": "true",
        "source_ami_filter": {
            "filters": {
                "virtualization-type": "hvm",
                "name": "amzn-ami*-ebs",
                "root-device-type": "ebs"
            },
            "owners": ["137112412989", "591542846629", "801119661308", "102837901569", "013907871322", "206029621532", "286198878708", "443319210888"],
            "most_recent": true
        }
    }],

    "provisioners": [
        {
            "type": "shell",
            "inline": [
                "sudo yum update -y",
				"sudo /usr/sbin/update-motd --disable",
                "echo 'No unauthorized access permitted' | sudo tee /etc/motd",
                "sudo rm /etc/issue",
                "sudo ln -s /etc/motd /etc/issue",
                "sudo yum install -y elinks screen"
            ]
        }
    ]
}

The provisioners section provides shell commands that Packer will use to configure the virtual machine after it has been created by the builders, but before it is captured as a machine image. The provisioners section updates the package management system and cleans up temporary keys before exiting. For more information, see Provisioners on the HashiCorp Packer website.

AWS CodeBuild and the containers it executes operate outside of a customer’s VPC. Any actions performed as a part of your build project will not have access to private IP addresses in your VPC. As a result a public IP address must be assigned to the EC2 instance so that HashiCorp Packer can remotely connect to it and configure the system. If the instance is not going to be launched in your default VPC, you must provide HashiCorp Packer with the VPC and a public-facing subnet. You will also need to be able to use SSH to connect to the EC2 instance from the internet.

To protect against malicious behavior and prevent unnecessary access, during the short life of the EC2 instance, HashiCorp Packer will use a newly created key pair and security group to deploy the EC2 instance. They will be deleted after the AMI has been created.

The source_ami_filter is an alternative to source_ami, which states which AMI should be used to create the EC2 instance. source_ami_filter will query the AMIs available in your region and select the one that best matches the filter criteria. In this example, source_ami_filter is configured to find the most recent AMI that matches the expression amzn-ami*-ebs. The filter has been restricted to match AMIs owned by a limited set of AWS accounts. The owners listed are accounts owned and managed by AWS.

Create a Build Specification

AWS CodeBuild uses the buildspec.yml file to execute user-defined commands at various phases of the build process. This file tells AWS CodeBuild how to use HashiCorp Packer to execute the template.

The buildspec.yml must contain the following:

---
version: 0.2

phases:
  pre_build:
    commands:
      - echo "Installing HashiCorp Packer..."
      - curl -qL -o packer.zip https://releases.hashicorp.com/packer/0.12.3/packer_0.12.3_linux_amd64.zip && unzip packer.zip
      - echo "Installing jq..."
      - curl -qL -o jq https://stedolan.github.io/jq/download/linux64/jq && chmod +x ./jq
      - echo "Validating amazon-linux_packer-template.json"
      - ./packer validate amazon-linux_packer-template.json
  build:
    commands:
      ### HashiCorp Packer cannot currently obtain the AWS CodeBuild-assigned role and its credentials
      ### Manually capture and configure the AWS CLI to provide HashiCorp Packer with AWS credentials
      ### More info here: https://github.com/mitchellh/packer/issues/4279
      - echo "Configuring AWS credentials"
      - curl -qL -o aws_credentials.json http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI > aws_credentials.json
      - aws configure set region $AWS_REGION
      - aws configure set aws_access_key_id `./jq -r '.AccessKeyId' aws_credentials.json`
      - aws configure set aws_secret_access_key `./jq -r '.SecretAccessKey' aws_credentials.json`
      - aws configure set aws_session_token `./jq -r '.Token' aws_credentials.json`
      - echo "Building HashiCorp Packer template, amazon-linux_packer-template.json"
      - ./packer build amazon-linux_packer-template.json
  post_build:
    commands:
      - echo "HashiCorp Packer build completed on `date`"

During pre_build, this build file configures the AWS CodeBuild container with the tools it will need to execute the Packer template. The first tool is HashiCorp Packer, which is available for download from the HashiCorp website. The second tool is the jq utitility, used to parse JSON files. The last command in this phase validates the HashiCorp Packer template to ensure there are no syntax errors.

In the build phase, the build file configures the build container’s AWS credentials using the metadata URL. This metadata will provide the AWS credentials provided by the IAM role assigned to the AWS CodeBuild project earlier to HashiCorp Packer so that it can create EC2 resources on your behalf. As a final step the HashiCorp Packer template is executed, resulting in the building of an EC2 AMI.

Execute the AWS CodeBuild Project

Commit the new files to your repository and push them to AWS CodeCommit. You are now ready to have AWS CodeBuild create the AMI.

  1. From the AWS Management Console, navigate to the AWS CodeBuild console.
  2. In the list of build projects, choose the project you created, and then choose Start build.
  3. In Start new build, choose which branch and revision of your AWS CodeCommit repository should be used to build your AMI.
  4. From the Branch drop-down list, choose master. This should populate the Source version field with the latest commit you pushed to the repository.
  5. Choose Start build. AWS CodeBuild will execute the buildspec.yml file in your repository.

CodeBuild project start

You will be taken to a page that provides status and results for each phase of the build. Succeeded should be displayed for every stage. If an error occurred, you can review the build logs at the bottom of the page for any error messages.

CodeBuild project status

During the build, HashiCorp Packer will generate a temporary key pair, launch an EC2 instance, remotely connect to the instance using the generated key pair, and then provision the machine, before converting the instance to an AMI and tearing down everything that was created to build the AMI. After these steps are complete, the build log should contain output that shows an AMI with an ID of something like ami-1a2b3cde was created.

This AMI can now be used to create EC2 instances on demand or as part of an Auto Scaling group to elastically scale your infrastructure.

Next Steps

We hope you found the information in this first post helpful and will use it as a starting point for your own infrastructure as code pipeline. In this post, we created a portion of your infrastructure as code in the form of a HashiCorp Packer template. We used AWS CodeBuild to create an AMI based on the template. Your next steps could include:

  • Building more than one HashiCorp Packer template. Multiple HashiCorp Packer templates can be used to build custom AMIs based on different source AMIs, perhaps one for Ubuntu and another for Microsoft Windows.
  • Adding AWS CodePipeline to monitor your AWS CodeCommit repository. When a new version is committed, AWS CodePipeline will call AWS CodeBuild and re-create your AMIs automatically.
  • Using CloudWatch Events to implement automated compliance. You can use an AWS Lambda function to verify that all EC2 instances launched in your account are using one of your preconfigured custom AMIs.

For next steps and taking advantage of other more advanced features and services please see part 2 in this two-part series. The post uses AWS services such as AWS CodePipeline, AWS CloudFormation, and Amazon Cloudwatch Events to continuously release new AMIs from end to end.