Why Your Packer Build Broke After 1.7 and How required_plugins Fixes It
Packer 1.7 split builders and provisioners into separately versioned plugins. If your template predates the split, you are probably seeing 'unknown builder' errors. Here is how required_plugins and packer init restore reproducible builds.
15 Feb 2026, 16:39 UTC

The Upgrade That Broke Your Template
You upgraded Packer, ran packer build, and hit an error like Unknown builder type: amazon-ebs or Unknown provisioner type: shell. The template worked yesterday. Nothing in your source blocks changed. The problem is not Packer core; it is that Packer 1.7 moved builders, provisioners, and post-processors out of the main binary into separately versioned plugins. A template written before that split does not tell Packer which plugins it needs, so a fresh install has no idea how to resolve them.
The fix is not to downgrade. It is to declare your dependencies explicitly in a required_plugins block and run packer init before the first build. That single change turns an implicit, machine-local dependency into a portable declaration.
What Actually Changed in Packer 1.7
Before 1.7, builders and provisioners shipped inside the Packer binary. If you had Packer, you had the Amazon builder and the shell provisioner. Starting with 1.7, HashiCorp decoupled these components. The core binary now orchestrates builds, but the actual builders and provisioners are plugins that Packer downloads on demand.
This is a normal dependency-management pattern, but it has a sharp edge: a template that does not declare its plugins will fail on any machine that does not already have them cached. The error messages are terse—unknown builder or unknown provisioner—and they do not mention plugins, which makes the diagnosis confusing if you are not expecting it.
Declaring Dependencies with required_plugins
HCL2 templates (the modern .pkr.hcl format) can express plugin dependencies in a required_plugins block. You place it at the top level of your template, alongside your source and build blocks. Each entry maps a logical plugin name to a registry source and an optional version constraint.
Here is a minimal worked example for an Amazon EC2 image:
packer {
required_plugins {
amazon = {
version = ">= 1.2.0, < 2.0.0"
source = "github.com/hashicorp/amazon"
}
}
}
source "amazon-ebs" "example" {
ami_name = "example-{{timestamp}}"
instance_type = "t3.micro"
region = "us-east-1"
ssh_username = "admin"
}
build {
sources = ["source.amazon-ebs.example"]
provisioner "shell" {
inline = ["echo 'hello'"]
}
}
Run packer init . in the directory containing this template. Packer reads the required_plugins block, contacts the registry, and downloads the plugins into a local cache—by default ~/.packer.d/plugins. After initialization, packer validate . should succeed, confirming the template parses and all declared dependencies are satisfied.
If you typo the source path—say github.com/hashicorp/amazn—packer init fails with a registry lookup error, not a template syntax error. The source string must match the registry exactly.
Version Constraints Are Your Only Reproducibility Mechanism
packer init does not generate a lock file. It downloads plugins into a cache directory, but the next machine that runs packer init will resolve versions independently based on the constraints in your template. This means your version string is the sole reproducibility guarantee.
If you omit the constraint entirely, Packer resolves the newest compatible release. That is convenient for a quick experiment, but it means a CI run three weeks from now might pull a different plugin version than the one you tested locally, potentially changing behavior without any change to your template.
The constraint syntax follows HashiCorp's format, not npm or Maven. A caret range like ^1.2.0 or a tilde range like ~> 1.2 will not behave as developers coming from Node.js or Ruby expect. Use explicit comma-separated bounds such as ">= 1.2.0, < 2.0.0".
One useful consequence of the plugin split: because builder, provisioner, and post-processor plugins are independent, you can upgrade a single cloud plugin to pick up an API fix without upgrading Packer core or unrelated components. You adjust one constraint, run packer init, and rebuild.
The Trade-off: Local Cache Masks Portability Problems
The biggest limitation is that a template without required_plugins may still build locally if the plugin is already cached. You will not see the failure until the same template runs on a clean CI agent. That delayed failure is exactly what the required_plugins block prevents.
There is also a format boundary: legacy JSON templates cannot express required_plugins. If you are still using .json templates, you must convert them to HCL2 before adopting the plugin-based workflow. There is no JSON equivalent for declaring plugin dependencies.
How to Verify the Fix
To confirm your template is portable, do not rely on your local cache. In a clean checkout, run:
packer init .
Then run:
packer validate .
If both succeed, the template declares its dependencies correctly. To see what Packer actually resolved, inspect the plugin cache:
ls ~/.packer.d/plugins
If you want to observe the failure mode that a missing required_plugins block produces on a fresh machine, remove the cache directory and run packer build without packer init. You should see the unknown builder or unknown provisioner error—useful for understanding what your CI agents experience.
Bottom Line
Packer 1.7 did not break your template; it exposed an implicit dependency that was previously hidden inside the binary. Add a required_plugins block, pin your versions with explicit ranges, and make packer init the first step in every pipeline. It is a small change that removes an entire class of "works on my machine" failures.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.