Skip to main content

AMI pipeline with Packer

A golden image is built once, in a pipeline, and used everywhere. Instances do not get patched after launch — they get replaced by instances built from a newer image.

Objective​

A repeatable process for creating Amazon Machine Images with Packer, covering credentials, template structure, hardening, automated testing, publication, and version control.

Template​

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

variable "aws_region" { default = "us-east-1" }
variable "instance_type" { default = "t3.micro" }
variable "app_version" { default = "1.0.0" }

source "amazon-ebs" "base" {
region = var.aws_region
instance_type = var.instance_type
ssh_username = "ec2-user"
ami_name = "app-base-${var.app_version}-{{timestamp}}"

source_ami_filter {
filters = {
name = "al2023-ami-*-x86_64"
virtualization-type = "hvm"
root-device-type = "ebs"
}
owners = ["amazon"]
most_recent = true
}

associate_public_ip_address = false
subnet_filter {
filters = { "tag:Purpose" = "packer-build" }
most_free = true
random = false
}

tags = {
Name = "app-base-${var.app_version}"
ManagedBy = "packer"
Environment = "build"
AppVersion = var.app_version
}
}

build {
sources = ["source.amazon-ebs.base"]

provisioner "shell" {
script = "scripts/hardening.sh"
}

provisioner "ansible" {
playbook_file = "./ansible/setup.yml"
}

provisioner "shell" {
inline = ["sudo /usr/local/bin/verify-image.sh"]
}

post-processor "manifest" {
output = "manifest.json"
strip_path = true
}
}

Hardening​

#!/usr/bin/env bash
set -euo pipefail

# Patches
sudo dnf update -y --security

# Remove unnecessary services
sudo systemctl disable --now postfix || true
sudo systemctl disable --now rpcbind || true

# SSH hardening
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config

# Audit rules
sudo tee /etc/audit/rules.d/99-baseline.rules >/dev/null <<'EOF'
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/sudoers -p wa -k privilege
EOF

# Ensure IMDSv2 only
sudo sed -i 's/^#\?HttpTokens.*/HttpTokens = required/' /etc/cloud/cloud.cfg

# Clean build artefacts
sudo dnf clean all
sudo rm -rf /tmp/* /var/tmp/*

Hardening is versioned with the image. A change to the hardening script produces a new image; it is never applied to running instances.

Pipeline​

name: ami-build

on:
push:
branches: [main]
paths: ['packer/**', 'ansible/**', 'scripts/**']
workflow_dispatch:

permissions:
id-token: write
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.PACKER_BUILD_ROLE }}
aws-region: us-east-1

- name: Install Packer
uses: hashicorp/setup-packer@main
with:
version: latest

- run: packer init ./packer-template.pkr.hcl
- run: packer validate ./packer-template.pkr.hcl
- run: packer build -on-error=cleanup ./packer-template.pkr.hcl

- name: Extract AMI id
id: ami
run: |
AMI_ID=$(jq -r '.builds[-1].artifact_id' manifest.json | cut -d: -f2)
echo "ami_id=$AMI_ID" >> "$GITHUB_OUTPUT"

- name: Tag the image
run: |
aws ec2 create-tags \
--resources "${{ steps.ami.outputs.ami_id }}" \
--tags Key=AppVersion,Value=${{ vars.APP_VERSION }} \
Key=GitCommit,Value=${GITHUB_SHA} \
Key=ManagedBy,Value=packer

- name: Publish to Parameter Store
run: |
aws ssm put-parameter \
--name "/${{ vars.ENVIRONMENT }}/${{ vars.PROJECT }}/ami-id" \
--value "${{ steps.ami.outputs.ami_id }}" \
--type String --overwrite

Terraform and the launch template read the AMI id from Parameter Store. The AMI id is never typed into a configuration file.

Quality gates​

  • Packer template validates.
  • Base image is current and patched.
  • Hardening script applied.
  • CIS benchmark scan passes the agreed threshold.
  • No critical vulnerabilities in the image scan.
  • Image boots and the application health check passes in a test launch.
  • Build manifest and SBOM retained as artefacts.

Versioning and retention​

AttributeRule
Name<project>-<role>-<version>-<timestamp>
ImmutabilityImages are never modified after publication
RetentionLast 6 production images, last 30 days of build images
DeregistrationAutomated after retention expires; snapshots removed with it
RollbackPrevious AMI id remains in Parameter Store history

Operational rules​

  1. Instances are never patched in place for OS packages — they are replaced via instance refresh from a newer image.
  2. A critical CVE triggers an out-of-band image build and rollout, following the normal pipeline rather than a manual patch.
  3. The build account is separate from workload accounts; images are shared to the accounts that need them, or copied through the pipeline.
  4. Build logs are retained with the manifest for audit.