Free — no signup required

Writing a Packer Template

2 min read

Template Structure

A modern HCL2 Packer template has three top-level blocks:

  1. packer {} — Declares required plugins and the minimum Packer version.
  2. source "builder_type" "name" {} — Defines a builder (the "where").
  3. build {} — Ties sources and provisioners together (the "what").

A template that builds an AWS AMI with Nginx installed follows the same three-block shape: the packer block declares the Amazon plugin it needs; the source block finds the latest official Ubuntu 22.04 AMI to build from, names the resulting image (with a timestamp to keep names unique), and tags it for lifecycle management; the build block ties that source to a sequence of provisioners — first uploading a pre-written nginx config file, then a shell provisioner that waits for cloud-init to finish before installing and enabling nginx — and finishes with a post-processor that writes a manifest file recording the new AMI's ID for downstream pipelines to consume.

The Build Lifecycle in Detail

When a Packer build runs, Packer executes these steps in order:

  1. Validate the template syntax and required variables.
  2. Launch a temporary EC2 instance from the source_ami_filter result.
  3. Wait for SSH to become available on the instance.
  4. Run provisioners in the order they appear in the build block.
  5. Stop the instance.
  6. Create a snapshot of the EBS volume.
  7. Register the snapshot as a new AMI with the specified name and tags.
  8. Terminate the temporary instance.
  9. Run post-processors (write the manifest file).

The sleep 30 you sometimes see in older templates is a workaround for cloud-init not finishing before apt-get runs. The modern approach is cloud-init status --wait, which blocks until cloud-init completes properly.

Interview Tip

A common interview question is: "How do you pass the AMI ID built by Packer into your Terraform code?" The naive answer is to hardcode it. The production answer has two parts: (1) Use Packer's manifest post-processor to write the AMI ID to a JSON file, then parse it in your CI/CD pipeline. (2) Better yet, use a Terraform data "aws_ami" source that queries AWS for the latest AMI matching specific tags (like ManagedBy = "Packer" and Name = "nginx-golden-image"). This decouples the Packer and Terraform pipelines entirely.

Key Point: The cloud-init status --wait pattern is a subtle but important production detail. On AWS, when an instance boots, cloud-init runs in the background to configure networking, mount volumes, and set up the default user. If your provisioner shell script runs apt-get before cloud-init finishes, you can get lock conflicts on /var/lib/dpkg/lock that cause intermittent, hard-to-debug build failures.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free