Introduction
Packer builds identical machine images for multiple platforms from a single configuration. Ansible is its most popular provisioner — your existing playbooks and roles configure the image during the build. The result: golden images (AMIs, Azure images, VMware templates, Docker images) that boot fully configured in seconds, with no configuration drift.
How It Works
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Packer HCL │────►│ Launch │────►│ Ansible │
│ Template │ │ Temp VM/ │ │ Provisioner │
│ │ │ Container │ │ runs │
└──────────────┘ └─────────────┘ └──────┬───────┘
│
┌──────▼───────┐
│ Snapshot / │
│ Export Image │
│ (AMI, VMDK, │
│ QCOW2, OVA) │
└──────────────┘
Prerequisites
# Install Packer
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install packer
# Install Ansible
pip install ansible
# Verify
packer version
ansible --version
AWS AMI with Ansible Provisioner
Packer Template
# aws-ubuntu.pkr.hcl
packer {
required_plugins {
amazon = {
version = ">= 1.3.0"
source = "github.com/hashicorp/amazon"
}
ansible = {
version = ">= 1.1.0"
source = "github.com/hashicorp/ansible"
}
}
}
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "ami_name" {
type = string
default = "ubuntu-web-{{timestamp}}"
}
source "amazon-ebs" "ubuntu" {
region = var.aws_region
instance_type = "t3.medium"
ami_name = var.ami_name
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["099720109477"] # Canonical
most_recent = true
}
ssh_username = "ubuntu"
tags = {
Name = var.ami_name
Builder = "Packer"
Provisioner = "Ansible"
OS = "Ubuntu 24.04"
}
}
build {
sources = ["source.amazon-ebs.ubuntu"]
provisioner "ansible" {
playbook_file = "playbooks/webserver.yml"
user = "ubuntu"
extra_arguments = [
"--extra-vars", "env=production",
"--scp-extra-args", "'-O'"
]
ansible_env_vars = [
"ANSIBLE_HOST_KEY_CHECKING=False"
]
}
}
Ansible Playbook
# playbooks/webserver.yml
---
- name: Configure web server golden image
hosts: all
become: true
tasks:
- name: Update all packages
ansible.builtin.apt:
upgrade: dist
update_cache: true
- name: Install web server packages
ansible.builtin.apt:
name:
- nginx
- certbot
- python3-certbot-nginx
- fail2ban
- unattended-upgrades
- node-exporter
state: present
- name: Configure nginx
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: '0644'
- name: Enable services
ansible.builtin.service:
name: "{{ item }}"
enabled: true
loop:
- nginx
- fail2ban
- node-exporter
- name: Configure unattended upgrades
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
- name: Harden SSH
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- name: Clean up for image
ansible.builtin.shell: |
apt-get clean
rm -rf /var/lib/apt/lists/*
rm -rf /tmp/*
rm -rf /var/tmp/*
truncate -s 0 /var/log/*.log
history -c
changed_when: false
Build
packer init aws-ubuntu.pkr.hcl
packer validate aws-ubuntu.pkr.hcl
packer build aws-ubuntu.pkr.hcl
Azure Image with Ansible
# azure-ubuntu.pkr.hcl
source "azure-arm" "ubuntu" {
subscription_id = var.azure_subscription_id
client_id = var.azure_client_id
client_secret = var.azure_client_secret
tenant_id = var.azure_tenant_id
managed_image_resource_group_name = "packer-images"
managed_image_name = "ubuntu-web-{{timestamp}}"
os_type = "Linux"
image_publisher = "Canonical"
image_offer = "ubuntu-24_04-lts"
image_sku = "server"
location = "eastus"
vm_size = "Standard_B2s"
}
build {
sources = ["source.azure-arm.ubuntu"]
provisioner "ansible" {
playbook_file = "playbooks/webserver.yml"
user = "packer"
}
provisioner "shell" {
inline = [
"sudo /usr/sbin/waagent -force -deprovision+user",
"sync"
]
}
}
VMware Template with Ansible
# vmware-ubuntu.pkr.hcl
source "vsphere-iso" "ubuntu" {
vcenter_server = var.vcenter_server
username = var.vcenter_username
password = var.vcenter_password
datacenter = "DC1"
cluster = "Production"
datastore = "SSD-Datastore"
folder = "Templates"
vm_name = "ubuntu-2404-template"
CPUs = 2
RAM = 4096
disk_controller_type = ["pvscsi"]
storage {
disk_size = 40960
disk_thin_provisioned = true
}
network_adapters {
network = "VM Network"
network_card = "vmxnet3"
}
iso_paths = ["[SSD-Datastore] ISO/ubuntu-24.04-live-server-amd64.iso"]
boot_command = ["<wait>e<wait>...<enter>"]
ssh_username = "packer"
ssh_password = "packer"
convert_to_template = true
}
build {
sources = ["source.vsphere-iso.ubuntu"]
provisioner "ansible" {
playbook_file = "playbooks/base-image.yml"
user = "packer"
}
}
Docker Image with Ansible
# docker-app.pkr.hcl
source "docker" "ubuntu" {
image = "ubuntu:24.04"
commit = true
changes = [
"EXPOSE 8080",
"CMD [\"/opt/app/start.sh\"]"
]
}
build {
sources = ["source.docker.ubuntu"]
provisioner "ansible" {
playbook_file = "playbooks/app-container.yml"
extra_arguments = [
"--connection", "docker",
"--extra-vars", "ansible_host=default"
]
}
post-processor "docker-tag" {
repository = "myregistry.example.com/myapp"
tags = ["latest", "1.0.0"]
}
}
Project Structure
packer-images/
├── aws-ubuntu.pkr.hcl
├── azure-ubuntu.pkr.hcl
├── vmware-ubuntu.pkr.hcl
├── variables.pkr.hcl
├── playbooks/
│ ├── webserver.yml
│ ├── database.yml
│ ├── base-image.yml
│ └── app-container.yml
├── roles/
│ ├── common/
│ ├── hardening/
│ └── monitoring/
├── templates/
│ └── nginx.conf.j2
└── .github/
└── workflows/
└── build-images.yml
CI/CD: GitHub Actions
# .github/workflows/build-images.yml
name: Build Golden Images
on:
push:
branches: [main]
paths:
- 'playbooks/**'
- '*.pkr.hcl'
schedule:
- cron: '0 2 * * 1' # Weekly Monday 2 AM
jobs:
build-aws:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Packer
uses: hashicorp/setup-packer@main
- name: Install Ansible
run: pip install ansible
- name: Packer Init
run: packer init aws-ubuntu.pkr.hcl
- name: Packer Build
run: packer build aws-ubuntu.pkr.hcl
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Using Ansible Roles in Packer
# Reference roles from a requirements file
provisioner "ansible" {
playbook_file = "playbooks/webserver.yml"
roles_path = "roles/"
galaxy_file = "requirements.yml"
galaxy_force_install = true
}
# requirements.yml
roles:
- name: geerlingguy.docker
- name: geerlingguy.nginx
collections:
- name: community.general
- name: ansible.posix
Best Practices
- One playbook per image type — separate web, database, and base image playbooks
- Clean up at the end — remove caches, logs, and temp files to reduce image size
- Use roles — reuse the same Ansible roles in Packer builds and runtime configuration
- Pin versions — specify exact package versions for reproducible builds
- Run in CI/CD — build images automatically on playbook changes
- Weekly rebuilds — catch security patches with scheduled builds
- Tag images with metadata — include build date, git commit, Packer version
- Test images after build — add a validation provisioner or post-processor
Related Articles
Conclusion
Packer with the Ansible provisioner builds golden images using your existing playbooks and roles — the same automation that configures running hosts also creates immutable images for AWS, Azure, VMware, Proxmox, and Docker. Run packer build in CI/CD on a weekly schedule to keep images patched, and your fleet boots fully configured in seconds with zero drift.