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

  1. One playbook per image type — separate web, database, and base image playbooks
  2. Clean up at the end — remove caches, logs, and temp files to reduce image size
  3. Use roles — reuse the same Ansible roles in Packer builds and runtime configuration
  4. Pin versions — specify exact package versions for reproducible builds
  5. Run in CI/CD — build images automatically on playbook changes
  6. Weekly rebuilds — catch security patches with scheduled builds
  7. Tag images with metadata — include build date, git commit, Packer version
  8. Test images after build — add a validation provisioner or post-processor

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.