Skip to content

Packer — Primer

What Packer does

Packer builds identical machine images for multiple platforms from a single source configuration. You define what the image should contain once. Packer produces an AMI, a GCP image, a Vagrant box, a Docker image — whatever you need — from that single definition.

It is not a configuration management tool. It does not run on live servers. It runs once, produces an artifact (the image), and exits.

Name origin: Packer was created by Mitchell Hashimoto (HashiCorp co-founder) in 2013. The name describes its function — it "packs" software and configuration into a machine image. It was one of HashiCorp's earliest tools, preceding Terraform (2014) and Consul (2014).

Fun fact: Before Packer, teams typically built images by booting a VM, manually installing software, and snapshotting — a non-reproducible process called "golden image by hand." Packer made image building scriptable, version-controlled, and CI-pipeline-friendly.

Why it matters

  • Golden images. Every server starts from a known, tested baseline.
  • Immutable infrastructure. Deploy new images instead of patching running servers.
  • Consistency. Dev, staging, and production boot from the same image. Drift disappears.
  • Speed. Launching a pre-baked image is faster than bootstrapping from a bare OS at boot.

Core concepts

Concept Role
Template HCL2 file(s) defining the entire build
Builder Plugin that creates the image for a specific platform (AWS, GCP, Docker, etc.)
Provisioner Runs inside the build to install/configure software (shell, Ansible, file copy)
Post-processor Acts on the finished artifact (generate manifest, push Docker image, create Vagrant box)

HCL2 template structure

Two main block types:

Source blocks — define the builder and its platform-specific config (AMI base, instance type, region).

Timeline: Packer originally used JSON templates (.json). HCL2 support was added in Packer 1.5 (2020) and became the recommended format. JSON templates still work but lack variables, locals, and functions. If you see a Packer template in JSON, it is legacy — migrate to HCL2 with packer hcl2_upgrade.

source "amazon-ebs" "ubuntu" {
  ami_name      = "app-{{timestamp}}"
  instance_type = "t3.micro"
  region        = "us-east-1"
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    owners      = ["099720109477"] # Canonical
    most_recent = true
  }
  ssh_username = "ubuntu"
}

Build blocks — reference sources and attach provisioners/post-processors.

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

  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx"
    ]
  }

  post-processor "manifest" {
    output = "manifest.json"
  }
}

Builders

Builder Produces Notes
amazon-ebs AMI Most common AWS builder. Launches instance, provisions, snapshots.
azure-arm Managed Image / Shared Image Gallery Needs service principal or managed identity.
googlecompute GCP Image Uses service account JSON or application default credentials.
docker Docker image Useful when you need provisioners Dockerfile can't express.
virtualbox-iso OVF/OVA Boots from ISO, good for local dev images.
qemu QCOW2 KVM/libvirt images. Headless builds.
proxmox-iso Proxmox template Homelab favorite. Integrates with Proxmox API.

Provisioners

Provisioner What it does
shell Runs inline commands or a script file. The workhorse.
ansible Runs an Ansible playbook against the build instance over SSH.
file Copies files/directories from host into the image.
powershell Shell equivalent for Windows builds.

Provisioners run in order. If one fails, the build fails.

Gotcha: Shell provisioners run in a non-interactive, non-login shell by default. Your .bashrc and .profile are NOT sourced. Environment variables you expect from login shells will be missing. Use inline_shebang or explicit source /etc/profile if you need login environment. Also, apt-get must use -y — there is no terminal to accept prompts.

Remember: Packer provisioner order: "FAP"File (copy configs in), Ansible (configure the system), Post-processor (output the artifact). In practice, you typically run shell provisioners to install packages, then Ansible for configuration, then file provisioners for last-mile config.

Post-processors

Post-processor What it does
manifest Writes build metadata (artifact ID, builder, timestamp) to JSON.
docker-push Pushes the built Docker image to a registry.
vagrant Packages the artifact as a .box file.

Variables and locals

Define variables in the template:

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

locals {
  ami_name = "app-${formatdate("YYYYMMDD-hhmm", timestamp())}"
}

Set values via: - -var 'aws_region=us-west-2' on the command line - -var-file=prod.pkrvars.hcl for a file of values - PKR_VAR_aws_region environment variable

Packer + Ansible

provisioner "ansible" {
  playbook_file = "./ansible/site.yml"
  extra_arguments = [
    "--extra-vars", "env=production",
    "--vault-password-file", "/path/to/vault-pass"
  ]
}

Packer creates a temporary SSH key, passes connection details to Ansible. Ansible runs against the build instance exactly like any other target.

Packer + Terraform

Standard pattern: 1. Packer builds the AMI, writes the AMI ID to manifest.json. 2. Terraform reads the AMI ID (or uses aws_ami data source with filters). 3. Terraform launches instances from the golden image.

Packer owns the image. Terraform owns the infrastructure. Clean boundary.

Image pipeline

code change -> CI triggers Packer build -> image created
  -> automated tests (boot, smoke, compliance)
  -> promote to production account/project
  -> Terraform deploys new instances from promoted image

Build vs runtime configuration

Bake into the image (Packer): - OS packages, agents, base software - Hardening, CIS benchmarks - Static config that never changes between environments

Configure at boot (cloud-init, user-data): - Secrets, credentials, tokens - Environment-specific settings (hostnames, endpoints) - Service registration, cluster join

Rule of thumb: if it changes between environments, it does not belong in the image.

Remember: The bake-vs-boot decision mnemonic: "BOSS" — Binaries bake, OS config bake, Secrets boot, Settings (env-specific) boot. If it is the same across all environments, bake it. If it varies, inject it at boot via cloud-init or user-data.

Packer vs Docker: Packer builds VM images (AMIs, QCOW2). Docker builds container images. Use Packer when you need a full OS with kernel, boot process, and system services. Use Docker when you need a single application process. Many teams use both: Packer for the base AMI (OS + agents + hardening), Docker for application packaging on top.

Key commands

Command What it does
packer init . Downloads required plugins defined in required_plugins blocks.
packer fmt . Formats HCL2 files to canonical style.
packer validate . Checks template syntax and config without building.
packer build . Runs the build. The main event.
packer build -only='amazon-ebs.ubuntu' . Build a single source when multiple are defined.
packer build -var-file=prod.pkrvars.hcl . Build with a specific variable file.

Always run validate before build. Make fmt part of your CI lint step.

Debug clue: When a Packer build fails mid-provisioner, the build instance is usually terminated, making it hard to debug. Use packer build -on-error=ask . — this pauses on failure and lets you SSH into the instance to inspect state. For CI, use -on-error=abort (the default) to fail fast and clean up.

Interview tip: "How do you ensure your AMIs are secure and up to date?" Strong answer: Packer pipeline builds weekly from the latest base AMI, runs CIS benchmark hardening via Ansible, validates with InSpec/goss tests, and promotes through dev -> staging -> prod accounts. Old AMIs are deregistered after 90 days.