Ansible + Packer — Build Machine Images Automatically

Introduction

HashiCorp Packer automates machine image creation for multiple platforms (AWS AMIs, Docker images, VMware templates, Vagrant boxes). The Ansible provisioner lets you use existing Ansible playbooks to configure images during the build — no need to rewrite configuration as shell scripts or Dockerfiles.

Install Packer

# Install on Ubuntu/Debian
wget -O- 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

# Verify
packer version

Basic Packer Template with Ansible

# aws-ami.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"
    }
  }
}

source "amazon-ebs" "ubuntu" {
  ami_name      = "my-app-{{timestamp}}"
  instance_type = "t3.medium"
  region        = "us-east-1"

  source_ami_filter {
    filters = {
      name                = "ubuntu/images/hvm-ssd/ubuntu-noble-24.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    owners      = ["099720109477"]
    most_recent = true
  }

  ssh_username = "ubuntu"

  tags = {
    Name    = "My App Image"
    Builder = "packer"
    OS      = "Ubuntu 24.04"
  }
}

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

  # Run Ansible playbook
  provisioner "ansible" {
    playbook_file = "ansible/site.yml"
    extra_arguments = [
      "-e", "env=production",
      "--scp-extra-args", "'-O'"
    ]
    ansible_env_vars = [
      "ANSIBLE_HOST_KEY_CHECKING=False"
    ]
  }
}

Ansible Playbook for Image Configuration

# ansible/site.yml
---
- name: Configure machine image
  hosts: all
  become: true
  vars:
    app_user: myapp
    app_dir: /opt/myapp

  tasks:
    - name: Update all packages
      ansible.builtin.apt:
        upgrade: dist
        update_cache: true

    - name: Install required packages
      ansible.builtin.apt:
        name:
          - nginx
          - python3
          - python3-pip
          - certbot
          - fail2ban
          - unattended-upgrades
        state: present

    - name: Create application user
      ansible.builtin.user:
        name: "{{ app_user }}"
        system: true
        shell: /usr/sbin/nologin
        home: "{{ app_dir }}"

    - name: Deploy nginx configuration
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
        mode: "0644"

    - 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 apt cache (reduce image size)
      ansible.builtin.apt:
        autoclean: true
        autoremove: true

    - name: Clear temporary files
      ansible.builtin.file:
        path: "{{ item }}"
        state: absent
      loop:
        - /tmp/*
        - /var/tmp/*
        - /var/cache/apt/archives/*.deb

Docker Image with Ansible

# docker.pkr.hcl
source "docker" "ubuntu" {
  image  = "ubuntu:24.04"
  commit = true
  changes = [
    "EXPOSE 80 443",
    "CMD [\"/usr/sbin/nginx\", \"-g\", \"daemon off;\"]"
  ]
}

build {
  sources = ["source.docker.ubuntu"]

  provisioner "ansible" {
    playbook_file = "ansible/docker-setup.yml"
    extra_arguments = [
      "--connection=docker",
      "-e", "ansible_host={{.Host}}"
    ]
  }

  post-processor "docker-tag" {
    repository = "myapp"
    tags       = ["latest", "1.0.0"]
  }
}

Build Commands

# Initialize plugins
packer init aws-ami.pkr.hcl

# Validate template
packer validate aws-ami.pkr.hcl

# Build the image
packer build aws-ami.pkr.hcl

# Build with variables
packer build -var "region=eu-west-1" aws-ami.pkr.hcl

# Debug mode
PACKER_LOG=1 packer build aws-ami.pkr.hcl

Troubleshooting

# Ansible not found by Packer
which ansible  # ensure ansible is in PATH

# SSH connection issues
packer build -on-error=ask aws-ami.pkr.hcl

# SCP errors with newer OpenSSH
extra_arguments = ["--scp-extra-args", "'-O'"]

Conclusion

Packer + Ansible = immutable infrastructure done right. Packer handles the image lifecycle (build, test, publish) while Ansible handles configuration. Reuse your existing Ansible roles and playbooks without rewriting them as shell scripts.