Skip to content

Packer

Chapter 42: Packer - Machine Image Building

Section titled “Chapter 42: Packer - Machine Image Building”

Automated Machine Image Creation with Packer

Section titled “Automated Machine Image Creation with Packer”

Packer is an open-source tool by HashiCorp for creating identical machine images for multiple platforms from a single source configuration.

Packer Overview
+------------------------------------------------------------------+
| |
| +------------------------+ |
| | Packer | |
| +------------------------+ |
| | |
| +---------------------+---------------------+ |
| | | | | |
| v v v v |
| +----------+ +----------+ +----------+ +----------+ |
| | Builders | | Provisio | | Post- | | Templates| |
| | | | -ners | | Processors| | | |
| | - AWS | | - Shell | | - Upload | | - JSON | |
| | - Azure | | - Ansible| | - Scan | | - HCL2 | |
| | - GCP | | - Puppet | | - Tag | | - Var | |
| | - Docker | | - Chef | | - Test | | - Packer| |
| +----------+ +----------+ +----------+ +----------+ |
| |
+------------------------------------------------------------------+
FeatureDescription
BuildersCreate images for different platforms
ProvisionersConfigure the image
Post-ProcessorsProcess images after creation
TemplatesDefine image configuration

Packer Workflow
+------------------------------------------------------------------+
| |
| +----------+ +----------+ +----------+ +----------+ |
| | Template | -> | Builder | -> | Provision| -> | Post- | |
| | (JSON/ | | (Create | | -er | | Processor| |
| | HCL2) | | Instance| | (Config) | | (Export) | |
| +----------+ +----------+ +----------+ +----------+ |
| | | | | |
| v v v v |
| +----------------------------------------------------------+ |
| | State Management | |
| +----------------------------------------------------------+ |
| |
| Output: Machine Image (AMI, VHD, Docker Image, etc.) |
| |
+------------------------------------------------------------------+
AWS Builders
+------------------------------------------------------------------+
| |
| +------------------+ +------------------+ +------------------+ |
| | AMI Builder | | EC2 Image Builder| | EBS Builder | |
| | | | | | | |
| | - Source AMI | | - Pipeline | | - Volume-based | |
| | - Instance Store | | - Components | | - Snapshot | |
| | - Launch | | - Distribution | | - Registration | |
| +------------------+ +------------------+ +------------------+ |
| |
| +------------------+ +------------------+ |
| | Instance Builder | | Chroot Builder | |
| | | | | |
| | - EC2 Instance | | - No running | |
| | - SSH/WinRM | | instance | |
| | - Full support | | - Fast build | |
| +------------------+ +------------------+ |
| |
+------------------------------------------------------------------+

packer.pkr.hcl
# Required plugins
packer {
required_plugins {
amazon = {
version = ">= 1.0.0"
source = "github.com/hashicorp/amazon"
}
}
}
# Variables
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "ami_name" {
type = string
default = "web-server-{{timestamp}}"
}
# Data sources
data "amazon-ami" "ubuntu" {
filters = {
name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
# Source definition
source "amazon-ebs" "ubuntu" {
region = var.aws_region
source_ami = data.amazon-ami.ubuntu.id
instance_type = "t3.medium"
ssh_username = "ubuntu"
ami_name = var.ami_name
ami_description = "Web server AMI built with Packer"
tags = {
Name = "web-server"
BuiltBy = "Packer"
}
}
# Build definition
build {
name = "web-server"
sources = [
"source.amazon-ebs.ubuntu"
]
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
"sudo systemctl enable nginx"
]
}
provisioner "file" {
source = "./configs/nginx.conf"
destination = "/tmp/nginx.conf"
}
provisioner "shell" {
inline = [
"sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf",
"sudo systemctl restart nginx"
]
}
post-processor "manifest" {
output = "manifest.json"
}
}
{
"variables": {
"aws_region": "us-east-1",
"ami_name": "web-server-{{timestamp}}"
},
"builders": [
{
"type": "amazon-ebs",
"region": "{{user `aws_region`}}",
"source_ami_filter": {
"filters": {
"name": "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*",
"root-device-type": "ebs",
"virtualization-type": "hvm"
},
"most_recent": true,
"owners": ["099720109477"]
},
"instance_type": "t3.medium",
"ssh_username": "ubuntu",
"ami_name": "{{user `ami_name`}}",
"tags": {
"Name": "web-server",
"BuiltBy": "Packer"
}
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"sudo apt-get update",
"sudo apt-get install -y nginx"
]
}
],
"post-processors": [
{
"type": "manifest",
"output": "manifest.json"
}
]
}

Packer Provisioners
+------------------------------------------------------------------+
| |
| +------------------+ +------------------+ +------------------+ |
| | Shell | | Ansible | | Chef | |
| | | | | | | |
| | - inline | | - playbook | | - cookbook | |
| | - script | | - galaxy | | - role | |
| | - execute_command| | - inventory | | - attributes | |
| +------------------+ +------------------+ +------------------+ |
| |
| +------------------+ +------------------+ +------------------+ |
| | Puppet | | File | | PowerShell | |
| | | | | | | |
| | - manifest | | - upload files | | - scripts | |
| | - module | | - directories | | - inline | |
| | - facter | | - permissions | | - elevated | |
| +------------------+ +------------------+ +------------------+ |
| |
+------------------------------------------------------------------+
# Inline commands
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx curl wget",
"sudo systemctl enable nginx"
]
}
# Script file
provisioner "shell" {
script = "./scripts/setup.sh"
}
# Script with arguments
provisioner "shell" {
script = "./scripts/configure.sh"
execute_command = "sudo -S -E bash -s '{{ .Path }}'"
environment_vars = [
"APP_ENV=production",
"DB_HOST=${var.db_host}"
]
}
# Expect disconnect (for reboot)
provisioner "shell" {
inline = ["sudo reboot"]
expect_disconnect = true
}
# Pause before next provisioner
provisioner "shell" {
inline = ["echo 'Waiting for reboot...'"]
pause_before = "30s"
start_retry_timeout = "10m"
}
# Local Ansible
provisioner "ansible" {
playbook_file = "./ansible/site.yml"
extra_arguments = [
"--extra-vars", "app_env=production"
]
}
# Ansible Galaxy
provisioner "ansible" {
playbook_file = "./ansible/site.yml"
galaxy_file = "./ansible/requirements.yml"
galaxy_command = "ansible-galaxy install -r {{.GalaxyFile}}"
}
# Remote Ansible (Ansible installed on instance)
provisioner "ansible-remote" {
playbook_file = "./ansible/site.yml"
ansible_ssh_user = "ubuntu"
}
# Upload single file
provisioner "file" {
source = "./configs/app.conf"
destination = "/tmp/app.conf"
}
# Upload directory
provisioner "file" {
source = "./configs/"
destination = "/tmp/configs/"
}
# Upload with direction (for Windows)
provisioner "file" {
source = "./configs/app.conf"
destination = "C:/Temp/app.conf"
direction = "upload"
}

source "amazon-ebs" "web-server" {
# Region and source AMI
region = "us-east-1"
source_ami = "ami-0abcdef1234567890"
# Instance configuration
instance_type = "t3.medium"
spot_instance_types = ["t3.medium", "t3.large"]
spot_price = "0.05"
# Network configuration
vpc_id = "vpc-12345678"
subnet_id = "subnet-12345678"
security_group_ids = ["sg-12345678"]
# SSH configuration
ssh_username = "ubuntu"
ssh_keypair_name = "my-keypair"
ssh_private_key_file = "./keys/my-keypair.pem"
# AMI configuration
ami_name = "web-server-{{timestamp}}"
ami_description = "Web server AMI"
ami_users = ["123456789012"] # Share with accounts
ami_groups = ["all"] # Make public (or specific groups)
# AMI tags
tags = {
Name = "web-server"
Environment = "production"
BuiltBy = "Packer"
Project = "web-app"
}
# Snapshot tags
snapshot_tags = {
Name = "web-server-snapshot"
}
# Launch template
launch_template = {
id = "lt-1234567890abcdef"
version = "$Latest"
}
}
# Windows AMI Builder
source "amazon-ebs" "windows" {
region = "us-east-1"
source_ami = "ami-0abcdef1234567890"
instance_type = "t3.medium"
# Windows-specific
user_data_file = "./scripts/windows-userdata.ps1"
communicator = "winrm"
winrm_username = "Administrator"
winrm_password = "PackerPassword123!"
winrm_use_ssl = true
winrm_insecure = true
# AMI configuration
ami_name = "windows-server-{{timestamp}}"
}
source "amazon-ebs" "spot-instance" {
region = "us-east-1"
source_ami = "ami-0abcdef1234567890"
# Use spot instances
spot_price = "auto"
spot_instance_types = [
"t3.medium",
"t3.large",
"m5.medium"
]
# Fallback to on-demand if spot unavailable
spot_price_auto_product = "Linux/Unix (Amazon VPC)"
instance_type = "t3.medium" # Fallback
ssh_username = "ubuntu"
ami_name = "spot-built-ami-{{timestamp}}"
}

Post-Processors
+------------------------------------------------------------------+
| |
| +------------------+ +------------------+ +------------------+ |
| | Manifest | | Amazon Import | | Vagrant | |
| | | | | | | |
| | - Output JSON | | - Import to EC2 | | - Vagrant box | |
| | - Image IDs | | - VM Import | | - VirtualBox | |
| | - Metadata | | - Conversion | | - AWS provider | |
| +------------------+ +------------------+ +------------------+ |
| |
| +------------------+ +------------------+ +------------------+ |
| | Docker | | Artifactory | | Checksum | |
| | | | | | | |
| | - Docker image | | - Upload | | - SHA256 | |
| | - Push to repo | | - Repository | | - MD5 | |
| | - Tag | | - Authentication | | - Verification | |
| +------------------+ +------------------+ +------------------+ |
| |
+------------------------------------------------------------------+
build {
post-processor "manifest" {
output = "manifest.json"
strip_path = true
custom_data = {
git_commit = "${var.git_commit}"
build_number = "${var.build_number}"
}
}
}
# Build Docker image from AMI
build {
sources = ["source.amazon-ebs.ubuntu"]
post-processor "docker-import" {
repository = "my-app"
tag = "latest"
}
post-processor "docker-push" {
login = true
login_username = var.docker_username
login_password = var.docker_password
login_server = "docker.io"
}
}

source "amazon-ebs" "base" {
region = "us-east-1"
source_ami = "ami-0abcdef1234567890"
instance_type = "t3.medium"
ssh_username = "ubuntu"
ami_name = "base-ami-{{timestamp}}"
# Copy to multiple regions
ami_regions = [
"us-west-1",
"us-west-2",
"eu-west-1",
"eu-central-1",
"ap-southeast-1"
]
# Region-specific AMI names
ami_region_kms_key_ids = {
us-west-1 = "alias/aws/ebs"
us-west-2 = "alias/aws/ebs"
eu-west-1 = "alias/aws/ebs"
eu-central-1 = "alias/aws/ebs"
}
}
# Build in multiple regions simultaneously
variable "regions" {
type = list(string)
default = ["us-east-1", "eu-west-1", "ap-southeast-1"]
}
build {
name = "multi-region"
dynamic "sources" {
for_each = var.regions
iterator = region
content {
source = "source.amazon-ebs.base"
# Override region-specific settings
region = region.value
ami_name = "app-${region.value}-{{timestamp}}"
}
}
provisioner "shell" {
inline = ["sudo apt-get update && sudo apt-get install -y nginx"]
}
}

Golden Image Pipeline
+------------------------------------------------------------------+
| |
| Stage 1: Base Image |
| +----------------------------------------------------------+ |
| | - OS updates and patches | |
| | - Common packages | |
| | - Security hardening | |
| | - Base configuration | |
| +----------------------------------------------------------+ |
| | |
| v |
| Stage 2: Application Image |
| +----------------------------------------------------------+ |
| | - Application runtime | |
| | - Application dependencies | |
| | - Application configuration | |
| | - Application-specific hardening | |
| +----------------------------------------------------------+ |
| | |
| v |
| Stage 3: Deployment Image |
| +----------------------------------------------------------+ |
| | - Environment-specific config | |
| | - Secrets and credentials | |
| | - Monitoring agents | |
| | - Final validation | |
| +----------------------------------------------------------+ |
| |
+------------------------------------------------------------------+
# Base image template
source "amazon-ebs" "base" {
region = "us-east-1"
source_ami = data.amazon-ami.ubuntu.id
instance_type = "t3.medium"
ssh_username = "ubuntu"
ami_name = "base-image-{{timestamp}}"
ami_users = var.account_ids
}
build {
name = "base-image"
sources = ["source.amazon-ebs.base"]
# System updates
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get upgrade -y",
"sudo apt-get install -y curl wget jq unzip"
]
}
# Security hardening
provisioner "shell" {
script = "./scripts/harden.sh"
}
# Install common tools
provisioner "shell" {
script = "./scripts/install-tools.sh"
}
# Output manifest
post-processor "manifest" {
output = "base-manifest.json"
}
}
# Application image (uses base image)
variable "base_ami" {
type = string
}
source "amazon-ebs" "app" {
region = "us-east-1"
source_ami = var.base_ami
instance_type = "t3.medium"
ssh_username = "ubuntu"
ami_name = "app-image-{{timestamp}}"
}
build {
name = "app-image"
sources = ["source.amazon-ebs.app"]
# Install application
provisioner "ansible" {
playbook_file = "./ansible/app.yml"
}
# Configure application
provisioner "file" {
source = "./configs/"
destination = "/opt/app/configs/"
}
post-processor "manifest" {
output = "app-manifest.json"
}
}

.github/workflows/packer.yml
name: Build AMI with Packer
on:
push:
branches: [main]
paths:
- 'packer/**'
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Packer
uses: hashicorp/setup-packer@main
with:
version: "1.9.0"
- name: Validate Template
run: |
cd packer
packer init .
packer validate .
build:
needs: validate
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Packer
uses: hashicorp/setup-packer@main
with:
version: "1.9.0"
- name: Build AMI
run: |
cd packer
packer init .
packer build -machine-readable .
env:
PKR_VAR_ami_name: "app-${{ github.sha }}"
- name: Upload manifest
uses: actions/upload-artifact@v3
with:
name: packer-manifest
path: packer/manifest.json
// Jenkinsfile
pipeline {
agent any
environment {
AWS_REGION = 'us-east-1'
PACKER_VERSION = '1.9.0'
}
stages {
stage('Validate') {
steps {
sh '''
packer init packer/
packer validate packer/
'''
}
}
stage('Build AMI') {
steps {
withAWS(credentials: 'aws-credentials', region: "${AWS_REGION}") {
sh '''
cd packer
packer build \
-var "ami_name=app-${BUILD_NUMBER}" \
-var "git_commit=${GIT_COMMIT}" \
.
'''
}
}
post {
success {
archiveArtifacts artifacts: 'packer/manifest.json', fingerprint: true
}
}
}
stage('Update Terraform') {
steps {
script {
def manifest = readJSON file: 'packer/manifest.json'
def amiId = manifest.builds[0].artifact_id
sh """
cd terraform
terraform apply -auto-approve \
-var "ami_id=${amiId}"
"""
}
}
}
}
post {
always {
cleanWs()
}
}
}

Packer Security Best Practices
+------------------------------------------------------------------+
| |
| 1. Credential Management |
| +--------------------------------------------------------+ |
| | - Use IAM roles for EC2 instances | |
| | - Never hardcode credentials | |
| | - Use temporary credentials (STS) | |
| | - Rotate keys regularly | |
| +--------------------------------------------------------+ |
| |
| 2. Image Security |
| +--------------------------------------------------------+ |
| | - Start from trusted source AMIs | |
| | - Apply latest security patches | |
| | - Remove sensitive data before AMI creation | |
| | - Use encrypted AMIs | |
| +--------------------------------------------------------+ |
| |
| 3. Network Security |
| +--------------------------------------------------------+ |
| | | Use isolated VPC for building | |
| | | Restrict security group rules | |
| | | Use VPC endpoints for AWS services | |
| +--------------------------------------------------------+ |
| |
+------------------------------------------------------------------+
# Use spot instances for cost savings
source "amazon-ebs" "optimized" {
# ... other config ...
# Spot instances
spot_price = "auto"
spot_instance_types = ["t3.medium", "t3.large", "m5.medium"]
# Faster EBS volumes
volume_type = "gp3"
volume_size = 20
encrypted = true
# Parallel provisioning
skip_create_ami = false
}
# Parallel builds
build {
sources = [
"source.amazon-ebs.region1",
"source.amazon-ebs.region2",
"source.amazon-ebs.region3"
]
# Run provisioners in parallel
provisioner "shell" {
inline = ["echo 'Parallel provisioning'"]
}
}
packer/
+------------------------------------------------------------------+
| |
| packer.pkr.hcl # Main template |
| variables.pkr.hcl # Variable definitions |
| sources.pkr.hcl # Source definitions |
| builds.pkr.hcl # Build definitions |
| |
| scripts/ # Shell scripts |
| +-- setup.sh |
| +-- harden.sh |
| +-- install-tools.sh |
| |
| configs/ # Configuration files |
| +-- nginx.conf |
| +-- app.conf |
| |
| ansible/ # Ansible playbooks |
| +-- site.yml |
| +-- roles/ |
| +-- web-server/ |
| |
| manifests/ # Output manifests |
| +-- base-manifest.json |
| +-- app-manifest.json |
| |
+------------------------------------------------------------------+

Packer Troubleshooting
+------------------------------------------------------------------+
| |
| Issue: SSH Connection Timeout |
| +--------------------------------------------------------+ |
| | Solutions: | |
| | - Check security group allows SSH (port 22) | |
| | - Verify ssh_username matches AMI | |
| | - Increase ssh_timeout value | |
| | - Check subnet route table | |
| +--------------------------------------------------------+ |
| |
| Issue: AMI Creation Failed |
| +--------------------------------------------------------+ |
| | Solutions: | |
| | - Check IAM permissions | |
| | - Verify AMI name is unique | |
| | - Check EBS volume limits | |
| | - Review instance logs | |
| +--------------------------------------------------------+ |
| |
| Issue: Provisioner Failures |
| +--------------------------------------------------------+ |
| | Solutions: | |
| | - Enable debug logging: PACKER_LOG=1 | |
| | - Check script exit codes | |
| | - Verify file paths exist | |
| | - Test provisioners manually | |
| +--------------------------------------------------------+ |
| |
+------------------------------------------------------------------+
Terminal window
# Enable debug logging
PACKER_LOG=1 packer build template.pkr.hcl
# Save log to file
PACKER_LOG=1 PACKER_LOG_PATH=packer.log packer build template.pkr.hcl
# Debug mode (keeps instance running on failure)
packer build -debug template.pkr.hcl
# On-error behavior
packer build -on-error=ask template.pkr.hcl
# Options: cleanup, abort, ask, run-cleanup-provisioner

TopicKey Points
BuildersUse appropriate builder for use case (EBS, instance, chroot)
ProvisionersShell for simple, Ansible for complex configurations
SecurityUse IAM roles, encrypt AMIs, isolate build environment
CI/CDIntegrate with pipelines for automated image building
Multi-RegionUse ami_regions for distribution, parallel builds for speed
Golden ImagesBuild layered images for maintainability


Next Chapter: Chapter 43 - Ansible on AWS