Pin Packer Plugins to Stop Image Build Drift
A Packer template that worked yesterday can fail today because of plugin drift. Declare plugin sources and version constraints with required_plugins, then enforce packer init and packer validate in CI.
04 Jul 2026, 10:58 UTC

A Packer template that built an AMI successfully on Monday can fail on Tuesday with no changes to your provisioner scripts. The cause is often not your code but plugin drift: the builder or provisioner plugin resolved a newer version, changed behavior, or picked up a developer-local binary that never existed in CI. The fix is to stop treating plugins as an ambient dependency and start declaring them inside the template itself.
The thesis is simple: make plugin resolution explicit with required_plugins, and make packer init plus packer validate a required CI gate before any packer build step. This turns "it worked on my machine" into a deterministic, reviewable contract.
Why plugin drift breaks image builds
Packer templates are code, but the plugins they invoke are often treated as an afterthought. In an HCL2 template, a source block like amazon-ebs or docker depends on a plugin binary. If that binary is not pinned, three things can change without warning:
- A newer plugin release alters default flags or output formatting.
- A developer's local plugin directory shadows the version CI expects.
- A cloud provider deprecates a source AMI, and the newer plugin handles the transition differently than the older one.
The result is an image that differs from the last build, or a pipeline that suddenly fails with an opaque error. Because the template file did not change, the diff review process misses the root cause entirely.
Declare plugins with required_plugins
HCL2 templates support a top-level packer block. Inside it, required_plugins maps a logical name to a source and a version constraint. The source tells Packer where to fetch the plugin; the version constraint tells it which releases are acceptable.
When you run packer init, Packer reads this block, resolves the constraint, downloads the plugin if needed, and records the selection. A subsequent packer validate then checks the template against that resolved plugin set. If the constraint cannot be satisfied—because the source is unreachable or the version range excludes all available releases—packer init fails immediately.
Worked example: pinning the Amazon plugin
Here is a minimal HCL2 template that declares its plugin dependency. Place this at the top of your .pkr.hcl file:
packer {
required_plugins {
amazon = {
source = "github.com/hashicorp/amazon"
version = ">= 1.2.0, < 2.0.0"
}
}
}
source "amazon-ebs" "ubuntu" {
# region, source_ami_filter, instance_type, ssh_username, ami_name
}
build {
name = "ubuntu-example"
sources = ["amazon-ebs.ubuntu"]
}
In CI, run these commands from the repository root in a clean workspace:
# Check formatting first; fails if the template is not canonical HCL
packer fmt -check .
# Resolve and install only the declared plugins
packer init .
# Validate the template against the resolved plugins
packer validate .
Expected checks: packer fmt -check should exit zero. packer init should complete without network errors if the plugin source is reachable, and packer validate should report that the configuration is valid. If packer init cannot satisfy the constraint, the pipeline stops before any cloud resources are created.
After validation passes, you can build a specific source with packer build -only=amazon-ebs.ubuntu .. Record the artifact ID and build metadata as pipeline outputs so you can trace which image came from which commit.
CI pipeline design
A clean environment is essential. If your CI runner reuses a workspace that already contains Packer plugins, packer init may skip resolution and use stale binaries. Use a fresh checkout or container for every run. Cache only the plugin installation directory—not the entire workspace—and key that cache on the template's plugin constraints. That way, changing a version constraint invalidates the cache and forces a fresh resolution.
Do not commit developer-local plugin state. The template and its required_plugins block are the source of truth. Everything else is derived.
Trade-offs and limitations
Version constraints pin the plugin release, not every downstream behavior. A pinned amazon plugin does not prevent AWS from deprecating a source AMI, changing an API, or altering default instance behavior. It also does not freeze the packages your provisioner installs. Constraints reduce drift; they do not eliminate it.
Choosing a range is a balance. A loose constraint like >= 1.0 still allows drift and may pull a breaking change. A very tight constraint like = 1.2.3 blocks security fixes and forces manual updates. A middle ground—such as >= 1.2.0, < 2.0.0—allows patch and minor updates while blocking major rewrites, but you must review those updates deliberately.
Air-gapped or regulated environments need special handling. If the plugin source is unreachable, packer init will fail. Pre-populate the plugin path or host an internal mirror, and verify that the mirror serves the exact versions your constraints allow.
Legacy JSON templates do not support required_plugins. If you still maintain JSON templates, either migrate them to HCL2 or manage plugin installation outside the template with explicit, versioned download steps.
Actionable next steps
Start with one template. Add a packer block with required_plugins, run packer init and packer validate locally, then wire those same commands into CI as a required gate. Confirm the installed version with packer plugins installed and compare artifact IDs across two clean builds. If the IDs and metadata match, your plugin resolution is deterministic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.