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
| Attribute | Rule |
|---|---|
| Name | <project>-<role>-<version>-<timestamp> |
| Immutability | Images are never modified after publication |
| Retention | Last 6 production images, last 30 days of build images |
| Deregistration | Automated after retention expires; snapshots removed with it |
| Rollback | Previous AMI id remains in Parameter Store history |
Operational rules
- Instances are never patched in place for OS packages — they are replaced via instance refresh from a newer image.
- A critical CVE triggers an out-of-band image build and rollout, following the normal pipeline rather than a manual patch.
- The build account is separate from workload accounts; images are shared to the accounts that need them, or copied through the pipeline.
- Build logs are retained with the manifest for audit.