Build AWS AMI with HashiCorp Packer using GitHub Actions

Build AWS AMI with HashiCorp Packer using GitHub Actions

Subscribe to my newsletter and never miss my upcoming articles

Security best practices recommend the latest base images (e.g. AWS AMI ) for spinning VM on the cloud. Up to date software patches reduce the risk of any security breach. This emphasizes a strong need to set up an Image Factory to automate the image creation process with the latest software versions.

Also, every organization has a standard list of software to be installed for a given image. Only the essential software must be part of the list to reduce the blast surface from a security standpoint. This list of software can either be installed once the VM is created or when the VM is in the process of creation. However, both these techniques are time-consuming and hence not recommended.

It would be preferred to bake this software list in the base image itself. HashiCorp Packer is an open-source tool that specializes in building automated machine images for multiple platforms from a single source configuration.

HashiCorp Packer Installation

Refer to HashiCorp documentation for Packer installation based on your hardware OS.

GitHub Actions

For this demo, we will use GitHub Actions to create CI/CD pipeline to automate this workflow and eventually push the baked image (AMI) in AWS. It is a platform to automate tasks within the software development lifecycle. It's an event-driven framework, which means we can carry series of commands for a given event or can be scheduled for one-off or repetitive tasks. (e.g. Execute a Test Suite on Pull Request creation, Adding labels to issues, Lint checks, etc.)

Actions are defined in YAML files, which allows pipeline workflow to be triggered using any GitHub events like on creation of Pull Requests, on code commits, and much more.

Prerequisites

  • AWS User with Programmatic access
    • AWS Access Key ID
    • AWS Secret Access Key
  • AWS IAM Privileges to create EC2 Instance (create, modify and delete EC2 instances). Refer documentation for the full list of IAM permissions required to run the amazon-ebs builder.

In this post, we will bake Open JDK (Java 8) in our Ubuntu base Image and push it into AWS. Packer configurations can be written in HCL (.pkr.hcl file extension) and JSON (.pkr.json) formats. We will use the HCL language for this demo.

Packer Flow.png

Reference GitHub repository - pkr-aws-ubuntu-java

Code Time

Let us start writing Packer configuration. (I am using a Linux machine for this demo)

Packer Configuration

Create Project folder pkr-aws-ubuntu-java

mkdir pkr-aws-ubuntu-java && cd $_

Create a file named aws-demo.pkr.hcl

touch aws-demo.pkr.hcl

Open your favorite IDE (e.g. VSCode). Copy the below code in aws-demo.pkr.hcl file.

packer {
  required_plugins {
    amazon = {
      version = ">= 0.0.2"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

The packer {} block contains Packer settings, including a required Packer version. The required_plugins block in the Packer block, specifies the plugin required by the template to build your image. The plugin block contains a version and source attribute.

Source block

The source block configures a specific builder plugin, which is then invoked by the build block. Source blocks use builders and communicators to define virtualization type, image launch type, etc.

Copy the following code to aws-demo.pkr.hcl file.

variable "ami_prefix" {
  type    = string
  default = "packer-aws-ubuntu-java"
}

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

source "amazon-ebs" "ubuntu_java" {
  ami_name      = "${var.ami_prefix}-${local.timestamp}"
  instance_type = "t2.micro"
  region        = "us-east-1"
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"
}

Variable ami_prefix is used to define the AMI image. Local variable timestamp helps ensure uniqueness to AMI name.

The amazon-ebs builder launches the source AMI, runs provisioners within this instance, then repackages it into an EBS-backed AMI. This builder configuration launches a t2.micro AMI in the us-east-1 region using an ubuntu:xenial AMI as the base image.

It creates the AMI named packer-aws-ubuntu-java+timestamp. AMI names must be unique else it will throw an error.

It also uses the SSH communicator - by specifying the ssh_username attribute. Packer is then able to SSH into EC2 instance using a temporary keypair and security group to provision your instances.

Build Block

The build block defines what Packer should do with the EC2 instance after it launches.

build {
  name    = "packer-ubuntu"
  sources = [
    "source.amazon-ebs.ubuntu_java"
  ]

  provisioner "shell" {

    inline = [
      "echo Install Open JDK 8 - START",
      "sleep 10",
      "sudo apt-get update",
      "sudo apt-get install -y openjdk-8-jdk",
      "echo Install Open JDK 8 - SUCCESS",
    ]
  }
}

provisioner block helps automate modifications to your base image. It leverages shell scripts, file uploads, and integrations with modern configuration management tools such as Ansible, Chef, etc.

The above provisioner defines a shell provisioner and installs Open JDK 8 in the base image.

The final file aws-demo.pkr.hcl should look as below.

packer {
  required_plugins {
    amazon = {
      version = ">= 0.0.2"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

variable "ami_prefix" {
  type    = string
  default = "packer-aws-ubuntu-java"
}

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

source "amazon-ebs" "ubuntu_java" {
  ami_name      = "${var.ami_prefix}-${local.timestamp}"
  instance_type = "t2.micro"
  region        = "us-east-1"
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"
}

build {
  name    = "packer-ubuntu"
  sources = [
    "source.amazon-ebs.ubuntu_java"
  ]

  provisioner "shell" {

    inline = [
      "echo Install Open JDK 8 - START",
      "sleep 10",
      "sudo apt-get update",
      "sudo apt-get install -y openjdk-8-jdk",
      "echo Install Open JDK 8 - SUCCESS",
    ]
  }
}

GitHub Actions

Create a new file in the .github/workflows directory named github-actions-packer.yml

Folder Structure.png

We will schedule this workflow to run in the wee hours - let's say 04:00 Hrs in the morning.

name - The name of your workflow. GitHub displays the names of your workflows on your repository's actions page - "AWS AMI using Packer Config"

name: AWS AMI using Packer Config

on - (Required) The name of the GitHub event that triggers the workflow. We have configured to trigger the workflow on schedule.

on:
  schedule:
    # * is a special character in YAML so you have to quote this string
    - cron:  '0 4 * * *'

jobs - A workflow run is made up of one or more jobs. These jobs can run in parallel or sequentially. Each job executes in a runner environment specified by runs-on.

job name - The name of the job displayed on GitHub.

runs-on - (Required) Determines the type of machine to run the job on. The machine can be either a GitHub-hosted runner or a self-hosted runner. Available GitHub-hosted runner types are: windows-latest / windows-2019 / windows-2016 / ubuntu-latest / ubuntu-20.04 etc.

jobs:
  packer:
    runs-on: ubuntu-latest
    name: packer

steps - Sequence of tasks called steps within a Job. They can execute commands, set up tasks, or run actions in your repository, a public repository, or action published in a Docker registry.

The first step is to check out the source code in the runner environment.

Checkout V2- This action checks out your repository under $GITHUB_WORKSPACE, so your workflow can access it.

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

To ensure access to the AWS Cloud environment we need to configure AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the runner environment. The values for these variables will be configured as GitHub Secrets in the below section.

Configure AWS Credentials - This action configures AWS credential and region environment variables for use in other GitHub Actions.

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          # aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} 
          # if you have/need it
          aws-region: us-east-1

Init initializes the Packer configuration used in the GitHub action workflow.

      # Initialize Packer templates
      - name: Initialize Packer Template
        uses: hashicorp/packer-github-actions@master
        with:
          command: init

Validate checks whether the configuration has been properly written. It will throw an error otherwise.

      # validate templates
      - name: Validate Template
        uses: hashicorp/packer-github-actions@master
        with:
          command: validate
          arguments: -syntax-only
          target: aws-demo.pkr.hcl

Build executes the Packer configuration.

      # build artifact
      - name: Build Artifact
        uses: hashicorp/packer-github-actions@master
        with:
          command: build
          arguments: "-color=false -on-error=abort"
          target: aws-demo.pkr.hcl
        env:
          PACKER_LOG: 1

The complete file github-actions-packer.yml will look as below.

---

name: AWS AMI using Packer Config

on:
  schedule:
    # * is a special character in YAML so you have to quote this string
    - cron:  '0 4 * * *'

jobs:
  packer:
    runs-on: ubuntu-latest
    name: packer

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          # aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} 
          # if you have/need it
          aws-region: us-east-1

      # Initialize Packer templates
      - name: Initialize Packer Template
        uses: hashicorp/packer-github-actions@master
        with:
          command: init

      # validate templates
      - name: Validate Template
        uses: hashicorp/packer-github-actions@master
        with:
          command: validate
          arguments: -syntax-only
          target: aws-demo.pkr.hcl

      # build artifact
      - name: Build Artifact
        uses: hashicorp/packer-github-actions@master
        with:
          command: build
          arguments: "-color=false -on-error=abort"
          target: aws-demo.pkr.hcl
        env:
          PACKER_LOG: 1

The source code is ready and can be pushed to the GitHub repository. As configured, the workflow will be triggered at 04:00 hrs in the morning.

Packer Actions 1.png

Packer Actions 2.png

Packer Actions 3.png

Packer Actions 4.png

 
Share this