Image Baking with Packer: Immutable VMs Without the Boilerplate

Philip Rehberger Oct 29, 2026 7 min read

Build golden images that survive provider changes. Covers multi-cloud and CI integration.

Container images get most of the immutable-infrastructure attention these days, but VM images still matter. Bake an OS image with everything pre-installed, run identical instances from it, and you avoid the configuration drift that plagues script-based provisioning. Packer is the tool that makes this practical without writing custom AMI build pipelines.

This post is what Packer actually does, the patterns that hold up in production, and when image-based deployment is the right choice.

The Pitch

The traditional VM provisioning workflow: launch an empty Ubuntu instance, run cloud-init or Ansible to install everything, start the application. Each launch reruns the configuration. Each instance is slightly different by the end.

Packer's workflow: build an image once with everything installed. Each launch is a copy of that image. Instances are identical.

Benefits:

  • Fast boot. No configuration time at launch; the instance is ready when it boots.
  • Reproducible. All instances from the same image are byte-identical.
  • Auditable. The image is a known artifact you can scan and verify.
  • Drift-free. Configuration cannot drift; every instance is the same.

The cost: image build time, image storage, and the discipline of treating images as artifacts.

Packer Basics

A Packer template (HCL format):

packer {
  required_plugins {
    amazon = {
      version = ">= 1.0"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

source "amazon-ebs" "ubuntu" {
  ami_name      = "myapp-${var.app_version}-${formatdate("YYYYMMDDhhmmss", timestamp())}"
  instance_type = "t3.medium"
  region        = "us-east-1"
  source_ami_filter {
    filters = {
      name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
    }
    most_recent = true
    owners      = ["099720109477"] # Canonical
  }
  ssh_username = "ubuntu"
}

build {
  sources = ["source.amazon-ebs.ubuntu"]

  provisioner "shell" {
    scripts = [
      "scripts/install-deps.sh",
      "scripts/install-app.sh",
      "scripts/configure-monitoring.sh",
    ]
  }
}

packer build template.pkr.hcl launches a temporary instance, runs the provisioner scripts, snapshots the volume, and registers an AMI. The AMI is now available to launch from.

Multi-Cloud Builds

Packer supports many builders. You can build the same image for multiple clouds in one run:

source "amazon-ebs" "ubuntu" { ... }
source "googlecompute" "ubuntu" { ... }
source "azure-arm" "ubuntu" { ... }

build {
  sources = [
    "source.amazon-ebs.ubuntu",
    "source.googlecompute.ubuntu",
    "source.azure-arm.ubuntu",
  ]
  provisioner "shell" {
    scripts = ["scripts/install.sh"]
  }
}

The provisioner runs identically on all three. Each cloud's image is registered separately.

This is the multi-cloud parity story for VM images. Reliable across clouds, complex to maintain in practice.

What to Build Into the Image

The right amount of "baking" is a judgment call.

Always in the image:

  • Operating system (you started from one)
  • Language runtime (Node, Python, Ruby, JDK)
  • Application dependencies (system packages, libraries)
  • Monitoring agents (CloudWatch Agent, Datadog Agent, etc.)
  • SSH keys for ops access (if not handled separately)
  • Security baseline (CIS hardening, fail2ban, etc.)

Sometimes in the image:

  • Application code (if releases are frequent, this churns the image often)
  • Configuration files (if they vary per environment, hard to bake)

Never in the image:

  • Secrets (use the cloud's secrets manager at runtime)
  • Per-environment configuration (use cloud-init or environment variables at launch)
  • Anything that changes more often than you want to rebuild the image

A common pattern: a "base image" with OS + dependencies + agents, separate from an "application image" that adds the application code. Base images change rarely (security updates, dependency updates); app images change with every deploy.

Image Layering Strategy

Three common layering strategies:

Single image. Everything baked into one image. Simple, can be slow to build (full rebuild for any change).

Base + app. A base image with infrastructure concerns (OS, agents, runtime); an app image built on top with the actual application. Faster app rebuilds.

Base + runtime + app. Three layers. Useful for highly differentiated applications that share a runtime.

For most teams: base + app is the right level of complexity. Single image is too coarse for frequent app changes; three layers is over-engineered for most.

Build Pipeline Integration

Packer should run in CI. Manual packer build from a laptop produces images nobody can reproduce later.

A typical CI workflow:

# GitHub Actions
- uses: hashicorp/setup-packer@main
- run: packer init .
- run: packer validate .
- run: packer build -only="amazon-ebs.ubuntu" template.pkr.hcl

The CI build produces an image tagged with the commit SHA and a timestamp. Auto Scaling Groups, Launch Templates, and other consumers reference the image by AMI ID.

For frequent rebuilds: cache provisioning steps that do not change. A Docker-based proxy that caches apt-get install results, for example, can dramatically speed up rebuilds.

Testing Images

A built image that does not boot is a wasted build. Test the image before promoting it:

Inside the build. Packer's goss integration runs assertions inline:

provisioner "goss" {
  test_file = "goss-tests.yaml"
}
# goss-tests.yaml
service:
  myapp:
    enabled: true
    running: true
package:
  curl:
    installed: true

If goss assertions fail, the build fails. The image is not produced.

After the build. Launch an instance from the new AMI, run smoke tests, then promote (tag, register in ASG, etc.) if successful.

For production-bound images: both. Inline tests catch quick issues; live tests catch boot-time problems.

Image Lifecycle

Images accumulate. A regular build cadence produces dozens or hundreds per month. Without cleanup, costs grow.

Retention patterns:

  • Keep the last N versions of each image type
  • Keep production-deployed versions indefinitely (need them for rollback)
  • Delete dev/staging-only images after 30 days

AWS has AMI lifecycle policies; you can also script cleanup based on tags.

Image Hardening

For production: don't ship images without hardening. The minimum:

  • Disable root SSH login
  • Apply CIS benchmarks or similar baseline
  • Install only necessary packages
  • Disable services you do not use
  • Apply current security patches
  • Run a vulnerability scanner against the image

Tools like Lynis (open source), CIS-CAT (commercial), or AWS Inspector run against the built image and produce a hardening report.

A baked-in hardening process is one of Packer's biggest wins. Each image meets the security baseline before it ever runs.

When VM Images Are Right

Packer fits when:

  • You run VMs (not just containers)
  • Boot time matters (slow scaling means slow incident response)
  • The deployment target supports immutable images (auto-scaling groups, instance templates)
  • You can tolerate the image build time in your deploy cycle

It fits less well when:

  • All workloads run in containers (use container images)
  • You need to update at runtime (a hot patch on a long-lived VM)
  • The build cycle is too slow for your deploy cadence

For most modern workloads: containers handle the immutable infrastructure story. VM images are for the workloads that do not fit containers — full-VM databases, GPU-heavy workloads with kernel dependencies, certain compliance-restricted environments.

Packer vs Cloud-Native Image Building

Cloud providers have their own image-building services:

  • AWS Image Builder. AMI-focused. Integrates with AWS native services.
  • GCP Cloud Build for VM images. Similar.
  • Azure Image Builder. Similar.

These are good for single-cloud deployments. Packer is the right call for multi-cloud or when you want a tool that does not lock you in.

For most teams on a single cloud: the cloud-native tool is simpler. For multi-cloud or for teams that prefer HashiCorp tooling: Packer.

A Practical Setup

For a typical team using EC2:

  1. Packer template in version control, with provisioner scripts.
  2. CI pipeline runs Packer on push to main, producing tagged AMIs.
  3. goss tests inline in the Packer build to catch issues early.
  4. Auto Scaling Group references the latest AMI by tag.
  5. Lifecycle policy keeps the last 30 days of images.

Initial setup is a couple of days. Maintenance is light — Packer template updates as the system evolves.

When Packer Has Aged Out

Packer was the dominant image builder for years. Two challenges in 2026:

  • Containers replaced many VM workloads. What used to be a Packer-built AMI is now a Docker image.
  • Cloud-native services have matured. AWS Image Builder is competitive for AMI-only workflows.

Packer remains the right tool when:

  • You build images for multiple clouds
  • You have an existing investment in Packer
  • You prefer HashiCorp tooling

For new single-cloud deployments: evaluate the cloud's native tool. Often simpler.


Working with VM-based infrastructure and considering image-based deployment? We help teams set up Packer pipelines that fit their build cadence and operational model. scopeforged.com

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.