Template Structure
A modern HCL2 Packer template has three top-level blocks:
packer {}— Declares required plugins and the minimum Packer version.source "builder_type" "name" {}— Defines a builder (the "where").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:
- Validate the template syntax and required variables.
- Launch a temporary EC2 instance from the
source_ami_filterresult. - Wait for SSH to become available on the instance.
- Run provisioners in the order they appear in the
buildblock. - Stop the instance.
- Create a snapshot of the EBS volume.
- Register the snapshot as a new AMI with the specified name and tags.
- Terminate the temporary instance.
- 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.
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.