Packer for Mutable and Immutable Infrastructure
#packer#iac#immutable-infrastructure#ami#devops#hashicorp
Packer is an open-source tool from HashiCorp that builds machine images (AMIs, Azure VM images, Vagrant boxes, etc.) from a single definition. You describe how to create the image once; Packer runs the build and outputs an image you can deploy many times. It fits both mutable and immutable infrastructure patterns, and is especially powerful for immutable: bake a new image, deploy it, and replace instances instead of patching in place.
Mutable vs immutable infrastructure
| Aspect | Mutable | Immutable |
|---|---|---|
| Definition | Servers (or VMs) are updated in place: SSH in, patch, reconfigure, restart. | Servers are not changed after deployment. To update, you build a new image, deploy new instances, and retire the old ones. |
| State | Accumulates over time (config drift, manual changes). | Image is the source of truth; instances are replaced from that image. |
| Updates | Patch, config management (Ansible, Chef), or re-run scripts on existing machines. | Build new image (e.g. with Packer), deploy new instances (e.g. with Terraform), terminate old ones. |
| Rollback | Often complex (revert config, hope nothing else changed). | Deploy previous image version again. |
| Consistency | Can drift between environments. | Same image ID = same contents everywhere. |
Packer’s role: In a mutable world, Packer can produce a golden image (base OS + common software) that you then configure further at deploy time or with config management. In an immutable world, Packer bakes the full app and config into the image; you deploy that image and do not modify the running instance—you replace it with a new image when you need to change something.
How Packer fits
- Mutable: Packer builds a base image (e.g. Amazon Linux + security updates + agents). You use it as the starting point; after launch you might still run Ansible or ad‑hoc changes (mutable).
- Immutable: Packer builds a complete image (OS + app + config). Terraform (or similar) launches instances from that image; you never SSH in to change them. To update, you build a new image, deploy new instances, and destroy old ones.
+------------------ PACKER (build time) -----------------+
| Template (.pkr.hcl) |
| +-----------+ +-------------+ +------------------+ |
| | Builder | | Provisioner | | Post-processor | |
| | (start |->| (install |->| (optional: | |
| | machine) | | software) | | copy, tag, etc) | |
| +-----------+ +-------------+ +------------------+ |
+--------------------------------------------------------+
| |
v v
Temporary VM Output: image
(discarded) (AMI, Azure image, etc.)
+------------------ TERRAFORM / CLOUD (deploy time) -------+
| Launch instances from the image (immutable: no patch) |
+----------------------------------------------------------+
How Packer works
- Template — You write a Packer template (HCL2:
.pkr.hcl) that defines one or more builds. - Builder — For each build, a builder starts a temporary machine (e.g. EC2 instance, Azure VM). Packer uses this machine only to create the image; it is shut down and removed after the build.
- Provisioners — While the machine is running, provisioners run: shell scripts, Ansible, Chef, or file copies. They install the OS updates, app, and config so the image is ready to use.
- Post-processors — After the image is created, post-processors can copy it to other regions, add tags, or produce artifacts (e.g. Vagrant box).
- Output — The builder produces an artifact: an image ID (e.g.
ami-xxx, Azure image resource ID). You then use that ID in Terraform or the cloud console to launch instances.
Packer does not manage the lifecycle of instances; it only builds images. You combine it with Terraform (or CloudFormation, etc.) to deploy and scale those images.
Example: immutable Amazon Linux 2 AMI
Below is a minimal HCL2 template that builds an AMI with the app and config baked in. After packer build, you get an AMI ID to use in Terraform.
# packer.pkr.hcl
packer {
required_plugins {
amazon = {
source = "github.com/hashicorp/amazon"
version = "~> 1.0"
}
}
}
variable "aws_region" {
type = string
default = "ap-southeast-1"
}
variable "ami_name_prefix" {
type = string
default = "my-app"
}
source "amazon-ebs" "app" {
ami_name = "${var.ami_name_prefix}-{{timestamp}}"
instance_type = "t3.micro"
region = var.aws_region
source_ami_filter {
filters = {
name = "amzn2-ami-hvm-*-x86_64-gp2"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["amazon"]
most_recent = true
}
ssh_username = "ec2-user"
}
build {
sources = ["source.amazon-ebs.app"]
provisioner "shell" {
inline = [
"sudo yum update -y",
"sudo yum install -y nginx",
"sudo systemctl enable nginx"
]
}
provisioner "file" {
source = "app.conf"
destination = "/tmp/app.conf"
}
provisioner "shell" {
inline = ["sudo mv /tmp/app.conf /etc/nginx/conf.d/"]
}
}
Run the build:
packer init .
packer build packer.pkr.hcl
Output will include the new AMI ID. In Terraform you reference it (e.g. via a data source or variable) and launch EC2 instances from it; you do not modify those instances after launch (immutable).
Example: Azure (immutable image)
# azure.pkr.hcl
packer {
required_plugins {
azure = {
source = "github.com/hashicorp/azure"
version = "~> 2.0"
}
}
}
source "azure-arm" "app" {
os_type = "Linux"
image_publisher = "Canonical"
image_offer = "0001-com-ubuntu-server-jammy"
image_sku = "22_04-lts"
azure_tags = {
env = "prod"
app = "my-app"
}
location = "Southeast Asia"
vm_size = "Standard_B2s"
}
build {
sources = ["source.azure-arm.app"]
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx"
]
}
}
Mutable use case: golden base image
For a mutable setup, use Packer to build a golden base image (hardened OS, updates, agents). Deploy instances from it, then use Ansible or manual steps to configure apps and config after boot:
- Packer: Base image = OS + updates + CloudWatch/SSM agent, etc.
- Terraform: Launch instances from that AMI.
- Ansible / scripts: Run after launch to install app and config (mutable).
You still get consistent starting points and faster boot, but the server is changed after creation.
Best practices
| Practice | Description |
|---|---|
| Version images | Use {{timestamp}} or a Git SHA in the image name so you can trace and roll back. |
| Minimal and documented | Only install what the image needs; document what is in the image (e.g. in a README or tag). |
| Secrets | Do not bake secrets into images. Use instance metadata, env vars at launch, or a secrets manager at runtime. |
| Pipeline | Run Packer in CI (e.g. on merge to main). Pass the output image ID to Terraform (e.g. via variable or pipeline artifact). |
| Immutable | Prefer immutable: build new image for every change, deploy new instances, terminate old. Use Terraform to manage the rollout. |
Summary
- Mutable infrastructure: Update servers in place; Packer can supply a golden base image that you configure further after launch.
- Immutable infrastructure: Do not change servers after deploy; build a new image (Packer), deploy new instances (Terraform), retire old ones. Packer is central to this pattern.
- Packer builds images via a template (builders + provisioners + optional post-processors), outputs an artifact (e.g. AMI ID), and you use that in Terraform or your cloud provider to run instances. Combining Packer with Terraform gives you repeatable, versioned images and infrastructure.
Comments