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'"]
Related Articles
- Ansible + Terraform
- Ansible AWS Automation
- Ansible VMware Templates
- Ansible Docker Container Management
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.