Packer HCL2 Dynamic Blocks: Building Multi‑Region AMIs Without Copy‑Paste Drift
Use Packer HCL2 dynamic blocks to generate parallel multi‑region AMI builders from a single source definition, and add variable validation to catch bad AMI IDs before any cloud API call.
27 Sept 2025, 22:50 UTC

The problem: duplicated builder blocks across regions
Teams that ship the same Ubuntu base image to five AWS regions often end up with five nearly identical source "amazon-ebs" blocks. A change to the AMI name, instance type, or user‑data script means editing every block — and inevitably one region gets left behind.
Thesis: dynamic blocks turn a map of regions into parallel builders at parse time
Packer 1.7 introduced HCL2 and, with it, dynamic blocks that expand a for_each loop into concrete source blocks before any builder runs. The result is a single source definition that yields N builders, each named ubuntu-2204., while keeping provisioners and post‑processors shared.
Dynamic blocks in practice
Define a variable that holds region‑specific settings — AMI IDs, subnet IDs, tags — then iterate over it:
variable "regions" {
type = map(object({
ami_id = string
subnet_id = string
tags = map(string)
}))
default = {
us-east-1 = { ami_id = "ami-0abc123", subnet_id = "subnet-111", tags = { Env = "prod" } }
eu-west-1 = { ami_id = "ami-0def456", subnet_id = "subnet-222", tags = { Env = "prod" } }
}
}
source "amazon-ebs" "ubuntu-2204" {
dynamic "source" {
for_each = var.regions
content {
region = source.key
source_ami = source.value.ami_id
subnet_id = source.value.subnet_id
instance_type = "t3.micro"
tags = merge(source.value.tags, { Name = "ubuntu-2204-${source.key}" })
}
}
# shared provisioners go here
provisioner "shell" {
inline = ["sudo apt-get update && sudo apt-get install -y nginx"]
}
}
build {
sources = ["source.amazon-ebs.ubuntu-2204"]
post-processor "manifest" { output = "manifest.json" }
}
Running packer inspect on this template lists two builders: amazon-ebs.ubuntu-2204.us-east-1 and amazon-ebs.ubuntu-2204.eu-west-1. A single packer build invocation launches both in parallel.
Variable validation catches mis‑configured AMI IDs before any API call
Since Packer 1.8 you can attach a validation block to any input variable. The check runs during packer validate (or build) and fails fast if a required field is empty or malformed:
variable "regions" {
# …type definition…
validation {
condition = alltrue([for r in var.regions : length(r.ami_id) > 0])
error_message = "Every region must provide a non‑empty ami_id"
}
}
If a CI pipeline accidentally passes an empty map, packer validate -var-file=prod.pkrvars.hcl exits with a clear error instead of spinning up instances.
Trade‑off: expansion happens at configuration load time
Dynamic blocks cannot reference resources that are created later in the same build (no dependency graph). The for_each map must be fully known from input variables or locals — you cannot, for example, query the AWS API for the latest AMI inside the loop. If you need data‑driven counts, compute the map in a preceding step (e.g., a Terraform data source) and pass it as a variable.
Actionable next step
- Upgrade your CI runners to Packer ≥1.7.0 (
packer version). - Convert any remaining JSON templates with
packer convert, then hand‑tune the generateddynamicblocks for readability. - Add a
validationblock for each critical variable and runpacker validate -var-file=env.pkrvars.hclin your pipeline before the build stage. - Verify the generated builder names with
packer inspectto confirm the expected region count.
With these two HCL2 features — dynamic blocks and variable validation — you eliminate copy‑paste drift, catch configuration errors at parse time, and keep a single source of truth for multi‑region image pipelines.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.