How to Design HashiCorp Packer Machine Image Automation
A Senior+ Guide to Immutable Infrastructure and Golden Image Pipelines
1. Introduction: Packer at Scale
In modern cloud-native infrastructure, the concept of immutable infrastructure has fundamentally changed how organizations deploy and manage their server fleets. Instead of SSH-ing into running machines and making ad-hoc configuration changes, teams now build pre-configured machine images that contain everything needed to run an application — operating system, runtime, dependencies, configuration, and application code. This approach eliminates configuration drift, improves reproducibility, and dramatically simplifies rollback procedures. HashiCorp Packer stands at the center of this transformation, providing a powerful, extensible tool for automating machine image creation across multiple platforms from a single configuration.
Packer was first released by Mitchell Hashimoto in 2013 and has since become the de facto standard for machine image automation in the industry. Unlike manual image creation processes that rely on clicking through cloud provider consoles or running brittle shell scripts on top of base images, Packer introduces a declarative, idempotent, and version-controlled approach to image building. With Packer, you define what your image should contain in HCL2 (HashiCorp Configuration Language version 2) or legacy JSON, and Packer handles the orchestration of creating temporary build instances, provisioning them, snapshotting the results, and cleaning up after itself.
The Scale Challenge
At enterprise scale, the challenges of machine image management multiply rapidly. A mid-size organization might need to maintain golden images for dozens of operating system variants, each tailored for different workload types — web servers, API gateways, batch processing workers, database nodes, machine learning training instances, and more. Each of these images must be built regularly to incorporate security patches, updated dependencies, and configuration changes. The images must be tested, scanned for vulnerabilities, signed for provenance, distributed across multiple cloud regions, and versioned for audit trails.
Consider a multinational corporation running workloads across AWS, Azure, and on-premises VMware infrastructure. They might need to produce 50 or more distinct image variants from shared base configurations. Without automation, this quickly becomes an unmanageable operational burden. Packer solves this by allowing you to parameterize everything — the base image, the software packages to install, the security hardening steps, the cloud-specific metadata — and produce consistent, reproducible outputs across any target platform.
Key Benefits of Packer
- Multi-Platform Support: A single Packer configuration can target Amazon EBS, Azure Managed Images, Google Compute Images, Docker containers, VMware vSphere templates, QEMU/KVM, and many more platforms simultaneously.
- Idempotent Builds: Running the same Packer configuration twice produces functionally identical images, ensuring consistency across development, staging, and production environments.
- Parallelism: Packer can build images for multiple platforms in parallel, reducing total build time from hours to minutes.
- Ecosystem Integration: Packer integrates with Ansible, Chef, Puppet, Shell scripts, PowerShell, and custom provisioners, allowing teams to leverage existing configuration management investments.
- CI/CD Native: Packer commands can be invoked from any CI/CD system, enabling fully automated image pipelines triggered by source code changes, scheduled intervals, or security advisory events.
- HCL2 Configuration: The modern HCL2 syntax supports variables, locals, conditionals, functions, and modular composition, making large-scale configurations maintainable and DRY.
The Role of Golden Images
A golden image is a pre-configured, hardened, and tested machine image that serves as the approved baseline for deploying new instances. Unlike Configuration Management approaches where servers are configured at boot time (pet infrastructure), golden images bake the configuration into the image itself (cattle infrastructure). This means every instance launched from the same image is functionally identical from the moment it boots, eliminating the race conditions and timing issues that plague runtime configuration approaches.
Packer makes golden image creation a first-class, automated process. Rather than manually maintaining a "master VM" and periodically snapshotting it, Packer builds images from scratch every time, ensuring they always reflect the latest approved configuration. This practice is central to achieving true immutable infrastructure, where servers are never modified after deployment — if changes are needed, a new image is built and instances are replaced rather than patched.
Who Should Read This Guide
This guide is designed for senior engineers, platform engineers, and infrastructure architects who want to build robust, production-grade machine image automation systems using Packer. We go beyond basic Packer usage to cover advanced topics like multi-stage builds, security hardening pipelines, cross-cloud image distribution, compliance automation, and CI/CD integration patterns. Whether you are replacing a manual image process or building a new image factory from scratch, this guide provides the architectural patterns and practical configurations needed to succeed at scale.
2. Core Architecture — Builders, Provisioners, Post-Processors
Packer's architecture revolves around three fundamental components that work together in a pipeline: Builders, Provisioners, and Post-Processors. Understanding how these components interact is essential for designing effective image automation systems, as each plays a distinct and critical role in the image creation process. The separation of concerns among these three components is what gives Packer its flexibility and power.
Builders: The Infrastructure Orchestrators
Builders are responsible for creating the base machine and managing the lifecycle of the temporary build infrastructure. When you run a Packer build, the builder is the component that communicates with your target platform's API to create a virtual machine, container, or compute instance. For cloud providers, this typically involves launching an EC2 instance, an Azure VM, or a GCP instance from a specified base image (source image). For local hypervisors like VMware or QEMU, the builder creates a new virtual machine from an ISO image or existing template.
The builder's responsibilities extend beyond just creating the instance. It must also configure networking, attach storage volumes, set up SSH or WinRM access for provisioners, wait for the instance to be ready, and handle cleanup after the build completes. Most importantly, once provisioning is done, the builder is responsible for creating the final image artifact — whether that is an AMI, an Azure Managed Image, a GCP image, a VHD file, or a Docker container snapshot.
Each builder has its own set of configuration options specific to its platform. For example, the Amazon EBS builder supports options like ami_regions for cross-region AMI replication, ami_users for sharing AMIs with specific AWS accounts, ami_block_device_mappings for configuring EBS volumes, and run_volume_tags for tagging the temporary build instance. Understanding these platform-specific options is critical for building images that meet your organization's requirements.
Provisioners: The Configuration Engines
Provisioners are the components that actually configure the machine once it has been created by the builder. They run scripts, install software, copy files, and execute configuration management tools inside the temporary build instance. Packer supports multiple provisioner types that can be chained together in sequence, allowing you to use different tools for different aspects of image configuration.
The most common provisioner types include Shell provisioners for executing bash or sh scripts, PowerShell provisioners for Windows images, Ansible provisioners for leveraging Ansible playbooks, and file provisioners for copying files into the image. You can also use Chef, Puppet, Salt, and custom provisioner plugins for more specialized needs. The key design principle is that provisioners should be idempotent — running the same provisioner multiple times on the same image should produce the same result without errors.
Provisioners execute in the order they are defined in the Packer configuration, and each one must complete successfully before the next one starts. If any provisioner fails, the entire build fails, and Packer cleans up any temporary resources. This sequential execution model simplifies debugging and ensures that dependencies between provisioning steps are respected.
Post-Processors: The Artifact Processors
Post-processors run after the builder has created the image and all provisioners have completed successfully. They are used to process, transform, or upload the final image artifact. Common use cases include generating manifest files that record metadata about the built image, compressing image files for local builds, uploading artifacts to remote storage, and publishing images to additional registries or marketplaces.
The Artifice post-processor is particularly useful in multi-stage build scenarios where you want to customize the artifacts that Packer considers as output. By default, Packer treats the builder's output (like an AMI or VHD) as the final artifact. The Artifice post-processor allows you to override this and specify custom files produced by provisioners as the new artifacts, which can then be processed by subsequent post-processors.
The Build Pipeline Flow
The overall flow of a Packer build follows a strict pipeline: the builder creates infrastructure, provisioners configure it, and post-processors process the output. This linear pipeline ensures predictability and makes it easy to reason about what happens at each stage. Failures at any stage trigger cleanup of all temporary resources, ensuring that failed builds do not leave behind orphaned infrastructure that could incur costs or create security risks.
Component Interaction Matrix
| Component | Execution Phase | Responsibility | Failure Behavior | Examples |
|---|---|---|---|---|
| Builder | Phase 1 — Infrastructure | Create VM, configure access, snapshot image | Clean up temporary VM, fail build | amazon-ebs, azure-arm, googlecompute, docker |
| Provisioner | Phase 2 — Configuration | Install software, configure OS, copy files | Abort build, clean up temporary VM | shell, powershell, ansible, chef-solo |
| Post-Processor | Phase 3 — Artifact Processing | Transform, compress, upload, publish | Fail build (image already created) | manifest, compress, artifice, docker-push |
Multi-Source and Multi-Build Architecture
In HCL2, Packer supports a more sophisticated architecture with the concepts of Sources and Builds. A Source defines a builder's configuration independently, while a Build references one or more Sources and adds provisioners and post-processors on top. This separation allows you to define a single source and reuse it across multiple builds with different provisioning steps. This is a powerful pattern for enterprise environments where you want to create multiple image variants from the same base without duplicating builder configuration.
3. HCL2 Configuration — Variables, Locals, Sources, Builds
HashiCorp Configuration Language version 2 (HCL2) is the modern, recommended syntax for Packer configurations. It replaces the older JSON-based format with a more expressive, human-readable syntax that supports variables, locals, conditionals, loops, functions, and modular composition. HCL2 is the same configuration language used by Terraform, so teams already familiar with Terraform will find Packer's HCL2 syntax immediately approachable. Mastering HCL2 is essential for building maintainable, scalable image automation systems.
Variables and Input Types
Variables in Packer allow you to parameterize your configurations, making them reusable across different environments and contexts. You can define variables in .pkrvars.hcl files, pass them via the -var flag, or set them as environment variables. Packer supports string, number, bool, list, map, and object variable types, giving you fine-grained control over input validation and documentation.
HCL2
variable "aws_region" {
type = string
default = "us-east-1"
description = "AWS region for building the image"
}
variable "instance_type" {
type = string
default = "t3.medium"
description = "EC2 instance type for the build instance"
}
variable "source_ami_filter" {
type = object({
owners = list(string)
most_recent = bool
filters = map(string)
})
default = {
owners = ["099720109477"]
most_recent = true
filters = {
name = "ubuntu/images/hvm-ssd-gp3-*-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
}
}
variable "packages" {
type = list(string)
default = ["nginx", "curl", "git", "unzip", "fail2ban"]
}
variable "environment" {
type = string
default = "production"
description = "Target environment for the image"
validation {
condition = contains(["development", "staging", "production"], var.environment)
error_message = "Environment must be development, staging, or production."
}
}
Locals and Computed Values
Locals allow you to compute derived values from variables and other expressions. They are evaluated once during the planning phase and can be used throughout your configuration to keep things DRY and readable. Locals are particularly useful for constructing resource names, computing tags, and building complex filter expressions.
HCL2
locals {
timestamp = formatdate("YYYYMMDD-hhmm", timestamp())
image_name = "${var.environment}-ubuntu-2404-${local.timestamp}"
common_tags = {
PackerManaged = "true"
Environment = var.environment
BuildDate = local.timestamp
BaseImage = "ubuntu-24.04"
ManagedBy = "packer"
Team = "platform-engineering"
}
ami_name = "packer-${local.image_name}"
ssh_timeout = var.instance_type == "t3.micro" ? "20m" : "10m"
}
source "amazon-ebs" "ubuntu" {
ami_name = local.ami_name
instance_type = var.instance_type
region = var.aws_region
source_ami_filter {
filters = var.source_ami_filter.filters
owners = var.source_ami_filter.owners
most_recent = var.source_ami_filter.most_recent
}
ssh_username = "ubuntu"
ssh_timeout = local.ssh_timeout
tags = local.common_tags
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_size = 30
volume_type = "gp3"
delete_on_termination = true
}
}
Functions and Expressions
HCL2 provides a rich set of built-in functions that you can use in your configurations. These include string manipulation functions like format(), join(), and replace(); collection functions like length(), merge(), and flatten(); type conversion functions like tomap() and tolist(); and encoding/decoding functions like base64encode() and jsonencode(). These functions allow you to compute complex values without resorting to external tools.
| Function Category | Functions | Use Case |
|---|---|---|
| String | format(), join(), replace(), substr() | Constructing names, paths, metadata strings |
| Collection | length(), merge(), flatten(), distinct() | Combining and transforming lists and maps |
| Encoding | base64encode(), jsonencode(), yamlencode() | Generating cloud-init configs and metadata |
| Date/Time | timestamp(), formatdate(), timeadd() | Build timestamps, version strings |
| Type | tomap(), tolist(), tonumber() | Type coercion for variable compatibility |
| File | file(), fileexists(), templatefile() | Reading scripts, configs, and templates |
Sources and Builds — The HCL2 Composition Model
The separation of Sources and Builds is one of the most important HCL2 features for large-scale image automation. A Source defines the builder configuration — which platform, which base image, what instance type. A Build references one or more Sources and adds provisioners and post-processors. This means you can define a single source once and use it in multiple builds, each applying different provisioning steps.
HCL2
build {
name = "web-server"
sources = ["source.amazon-ebs.ubuntu"]
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx certbot python3-certbot-nginx",
"sudo systemctl enable nginx"
]
}
provisioner "file" {
source = "configs/nginx.conf"
destination = "/tmp/nginx.conf"
}
provisioner "shell" {
inline = [
"sudo mv /tmp/nginx.conf /etc/nginx/nginx.conf",
"sudo nginx -t"
]
}
post-processor "manifest" {
output = "manifests/web-server-manifest.json"
strip_path = true
}
}
build {
name = "api-server"
sources = ["source.amazon-ebs.ubuntu"]
provisioner "ansible" {
playbook_file = "ansible/api-server.yml"
extra_arguments = [
"--extra-vars", "app_version=1.2.3 environment=${var.environment}"
]
}
post-processor "manifest" {
output = "manifests/api-server-manifest.json"
strip_path = true
}
}
Packer Init and Required Plugins
Packer 1.7+ introduced the packer init command which automatically downloads and installs required plugins based on the packer {} block in your configuration. This eliminates the need to manually manage plugin binaries and ensures version consistency across different machines and CI/CD environments.
HCL2
packer {
required_plugins {
amazon = {
version = ">= 1.2.0"
source = "github.com/hashicorp/amazon"
}
azure = {
version = ">= 1.4.0"
source = "github.com/hashicorp/azure"
}
docker = {
version = ">= 1.1.0"
source = "github.com/hashicorp/docker"
}
ansible = {
version = ">= 1.1.0"
source = "github.com/hashicorp/ansible"
}
}
required_version = ">= 1.10.0"
}
4. Builders — Amazon EBS, Azure, GCP, Docker, VMware, QEMU
Builders are the foundation of Packer's image creation process. Each builder is a plugin responsible for interfacing with a specific platform's API to create temporary build infrastructure and produce image artifacts. Packer supports dozens of builders through its plugin ecosystem, but the most commonly used in enterprise environments are the Amazon EBS builder, Azure builder, Google Compute builder, Docker builder, VMware vSphere builder, and QEMU builder. Understanding the nuances of each builder is critical for designing effective multi-platform image pipelines.
Amazon EBS Builder
The Amazon EBS builder is the most widely used Packer builder in the industry. It creates AMIs (Amazon Machine Images) by launching a temporary EC2 instance from a source AMI, running provisioners against it, and then creating a new AMI from the resulting EBS snapshot. The builder supports a rich set of options for controlling the build process, including instance type, VPC configuration, security groups, IAM instance profiles, and EBS volume configuration.
HCL2
source "amazon-ebs" "hardened-ubuntu" {
ami_name = "hardened-ubuntu-2404-${formatdate("YYYYMMDD", timestamp())}"
instance_type = "m6i.large"
region = "us-east-1"
ami_description = "CIS-hardened Ubuntu 24.04 LTS"
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd-gp3-*-amd64-server-24.04*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["099720109477"]
most_recent = true
}
ssh_username = "ubuntu"
ssh_timeout = "15m"
ami_users = ["123456789012", "987654321098"]
ami_regions = ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"]
tags = {
Name = "hardened-ubuntu-2404"
OS = "Ubuntu 24.04 LTS"
CISBenchmark = "Level 2"
PackerManaged = "true"
}
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_size = 50
volume_type = "gp3"
encrypted = true
delete_on_termination = true
}
temporary_security_group_source_cidrs = ["10.0.0.0/16"]
iam_instance_profile { name = "packer-build-role" }
vpc_filter {
filters = { "tag:Environment" = "build", "tag:ManagedBy" = "packer" }
}
subnet_filter {
filters = { "tag:SubnetType" = "Private" }
most_free = true
}
}
Azure Builder
The Azure builder creates Azure Managed Images or Azure Compute Gallery images. It authenticates using service principals, managed identities, or Azure CLI credentials. The builder creates a temporary Azure VM, provisions it, and then captures it as a managed image or gallery image version.
HCL2
source "azure-arm" "windows-server" {
managed_image_name = "packer-windows-2022-${formatdate("YYYYMMDD", timestamp())}"
managed_image_resource_group_name = "packer-images-rg"
build_resource_group_name = "packer-build-rg"
subscription_id = var.azure_subscription_id
tenant_id = var.azure_tenant_id
client_id = var.azure_client_id
client_secret = var.azure_client_secret
os_type = "Windows"
image_publisher = "MicrosoftWindowsServer"
image_offer = "WindowsServer"
image_sku = "2022-Datacenter"
image_version = "latest"
location = "East US"
vm_size = "Standard_D4s_v3"
communicate_over_winrm = true
winrm_username = "packer"
winrm_timeout = "30m"
shared_image_gallery {
subscription = var.azure_subscription_id
resource_group = "packer-images-rg"
gallery_name = "packer_gallery"
image_name = "windows-server-2022"
image_version = "1.0.${packer.version}"
replication_regions = ["eastus", "westus2", "northeurope"]
}
os_disk_size_gb = 128
azure_tags = {
Environment = var.environment
ManagedBy = "packer"
}
}
Google Compute Builder
The Google Compute builder creates GCE images. It launches a temporary GCE instance from a source image, provisions it, and then creates a new image from the resulting disk. Authentication is handled via service account keys or application default credentials.
HCL2
source "googlecompute" "debian" {
project_id = var.gcp_project_id
zone = "us-central1-a"
machine_type = "e2-standard-4"
source_image_family = "debian-12"
source_image_project_id = ["debian-cloud"]
image_name = "custom-debian-12-${formatdate("YYYYMMDD", timestamp())}"
image_family = "custom-debian"
image_description = "Custom Debian 12 with application dependencies"
image_labels = { managed-by = "packer", environment = var.environment }
image_storage_locations = ["us", "eu"]
disk_size = 30
disk_type = "pd-balanced"
ssh_username = "packer"
metadata = { enable-oslogin = "false" }
network_project_id = var.gcp_network_project_id
subnetwork = "packer-build-subnet"
network = "packer-build-network"
scopes = [
"https://www.googleapis.com/auth/devstorage.read_write",
"https://www.googleapis.com/auth/cloud-platform"
]
}
Docker Builder
The Docker builder creates Docker images by running containers from existing images, provisioning them, and committing the result. It is particularly useful for creating base images, testing provisioning scripts locally, and building container images for development workflows.
HCL2
source "docker" "ubuntu" {
image = "ubuntu:24.04"
commit = true
changes = [
"EXPOSE 80", "EXPOSE 443",
"ENV APP_ENV=production",
"WORKDIR /app", "USER appuser"
]
}
build {
name = "app-container"
sources = ["docker.ubuntu"]
provisioner "shell" {
inline = [
"apt-get update",
"apt-get install -y python3 python3-pip nginx",
"useradd -m -s /bin/bash appuser"
]
}
provisioner "ansible" {
playbook_file = "ansible/container-setup.yml"
}
post-processor "docker-tag" {
repository = "registry.example.com/app"
tags = ["latest", "1.2.3"]
}
post-processor "docker-push" {
login = true
login_server = "registry.example.com"
login_username = var.docker_username
login_password = var.docker_password
}
}
VMware vSphere Builder
The VMware vSphere builder creates VM templates in vSphere environments. It is essential for on-premises and hybrid cloud scenarios where workloads run on VMware infrastructure.
HCL2
source "vsphere-iso" "ubuntu-vsphere" {
vcenter_server = var.vsphere_server
username = var.vsphere_user
password = var.vsphere_password
insecure_connection = false
cluster = "production-cluster"
datacenter = "dc-01"
datastore = "vsan-datastore"
folder = "Templates/Packer"
vm_name = "ubuntu-2404-template"
guest_os_type = "ubuntu64Guest"
CPUs = 4
RAM = 8192
disk_controller_type = ["pvscsi"]
storage {
disk_size = 51240
disk_thin_provisioned = true
}
network_adapters {
network = "VM Network"
network_card = "vmxnet3"
}
iso_paths = ["[datastore] ISOs/ubuntu-24.04-live-server-amd64.iso"]
cd_content = {
"meta-data" = ""
"user-data" = file("cloud-init/user-data")
}
cd_label = "cidata"
ssh_username = "ubuntu"
ssh_password = "packer"
ssh_timeout = "30m"
convert_to_template = true
}
Builder Comparison Matrix
| Builder | Target Platform | Output Artifact | Access Method | Key Use Case |
|---|---|---|---|---|
| amazon-ebs | AWS EC2 | AMI | SSH | Cloud-native workloads on AWS |
| azure-arm | Azure | Managed Image / Gallery | WinRM / SSH | Enterprise Azure deployments |
| googlecompute | GCP | GCE Image | SSH | Google Cloud workloads |
| docker | Docker | Docker Image | docker exec | Container base images, testing |
| vsphere-iso | VMware vSphere | VM Template | SSH / WinRM | On-premises VMware infrastructure |
| qemu | KVM / QEMU | QCOW2 / Raw Image | SSH | OpenStack, Proxmox, bare-metal |
5. Provisioners — Shell, PowerShell, Ansible, Chef, Puppet
Provisioners are the workhorses of image configuration in Packer. Once a builder has created a temporary build instance and established access (SSH for Linux, WinRM for Windows), provisioners execute within the instance to install software, copy files, configure system settings, and apply security hardening. The choice of provisioner type depends on your team's expertise, existing tooling investments, and the complexity of the configuration being applied.
Shell Provisioner
The Shell provisioner is the most commonly used provisioner type for Linux images. It executes shell scripts either inline or from external files. Despite its simplicity, the Shell provisioner is incredibly powerful when combined with well-structured scripts. For production use, it is recommended to use external script files rather than inline commands, as external scripts are easier to test, version, and maintain.
HCL2
provisioner "shell" {
script = "scripts/base-hardening.sh"
execute_command = "echo '${var.ssh_password}' | sudo -S sh -c '{{ .Path }}'"
environment_vars = [
"DEBIAN_FRONTEND=noninteractive",
"APP_VERSION=${var.app_version}",
"TARGET_ENV=${var.environment}"
]
timeout = "30m"
pause_before = "10s"
start_retry_timeout = "2m"
}
provisioner "shell" {
scripts = [
"scripts/update-packages.sh",
"scripts/install-dependencies.sh",
"scripts/configure-firewall.sh",
"scripts/cleanup.sh"
]
execute_command = "chmod +x {{ .Path }}; {{ .Path }}"
}
PowerShell Provisioner
The PowerShell provisioner is designed for Windows images and executes PowerShell scripts. It supports both inline scripts and external script files. Windows image building typically uses WinRM for communication instead of SSH, and the PowerShell provisioner handles this transparently.
HCL2
provisioner "powershell" {
inline = [
"Set-ExecutionPolicy Bypass -Scope Process -Force",
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12",
"Install-Module PSWindowsUpdate -Force -AllowClobber",
"Install-WindowsUpdate -AcceptEula -IgnoreReboot",
"Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True",
"Enable-PSRemoting -Force -SkipNetworkProfileCheck"
]
elevated_user = var.admin_username
elevated_password = var.admin_password
elevated = true
execution_policy = "unrestricted"
timeout = "45m"
}
provisioner "powershell" {
script = "scripts/windows-hardening.ps1"
environment_vars = [
"ENVIRONMENT=${var.environment}",
"DOMAIN_NAME=${var.domain_name}"
]
elevated = true
elevated_user = var.admin_username
elevated_password = var.admin_password
}
Ansible Provisioner
The Ansible provisioner allows you to run Ansible playbooks and roles against the build instance. This is a popular choice for teams already using Ansible for configuration management, as it allows them to reuse their existing playbooks and roles for both ephemeral image building and ongoing server configuration. The provisioner supports both local Ansible execution (running Ansible from the Packer host) and remote execution (installing Ansible on the target and running it there).
HCL2
provisioner "ansible" {
playbook_file = "ansible/playbooks/image-base.yml"
galaxy_file = "ansible/requirements.yml"
extra_arguments = [
"--extra-vars", "app_version=${var.app_version}",
"--extra-vars", "environment=${var.environment}",
"--tags", "security,monitoring",
"--skip-tags", "dev-only",
"-vvv"
]
ansible_env_vars = [
"ANSIBLE_HOST_KEY_CHECKING=False",
"ANSIBLE_FORCE_COLOR=true"
]
groups = ["packer", "production", "web-servers"]
timeout = "60m"
}
provisioner "ansible-local" {
playbook_file = "ansible/local-provision.yml"
playbook_dir = "ansible/playbooks"
extra_arguments = ["--become"]
staging_directory = "/tmp/packer-provisioner"
command = "ansible-playbook"
}
File Provisioner
The File provisioner copies files from the Packer host into the build instance. It supports both directories and individual files, and is commonly used for copying configuration files, certificates, scripts, and application code into the image.
HCL2
provisioner "file" {
source = "configs/"
destination = "/tmp/configs"
}
provisioner "file" {
content = templatefile("templates/cloud-init.yaml.tpl", {
hostname = "${var.environment}-web-01"
packages = var.packages
})
destination = "/tmp/cloud-init.yaml"
}
provisioner "file" {
source = "certs/"
destination = "/etc/ssl/certs/custom"
}
Chef and Puppet Provisioners
The Chef Solo and Puppet Masterless provisioners allow you to run Chef cookbooks or Puppet manifests against the build instance without requiring a running Chef Server or Puppet Master. These provisioners upload your cookbooks or modules to the instance and execute them locally. While less commonly used for new projects than Ansible or Shell, they remain valuable for organizations with established Chef or Puppet investments. The key consideration with Chef and Puppet provisioners is that they require the respective runtimes to be available, which adds to image build time but leverages existing configuration management expertise and codebases.
Provisioner Best Practices
- Idempotency: Ensure all provisioners can be run multiple times without producing errors. Use package managers that handle already-installed packages gracefully.
- Cleanup: Always clean up temporary files, package caches, and build artifacts at the end of provisioning to reduce image size.
- Error Handling: Use
set -ein shell scripts to fail fast on errors. Check return codes and provide meaningful error messages. - Logging: Add descriptive echo statements so Packer output provides clear visibility into what each step is doing.
- Timeout Configuration: Set appropriate timeouts for long-running provisioners like OS updates and large package installations.
- Validation: After making changes, validate them within the provisioner. For example, after installing nginx, run
nginx -tto verify the configuration.
| Provisioner | Best For | Complexity | Reusability | Learning Curve |
|---|---|---|---|---|
| Shell | Simple installs, OS-level config | Low | Medium | Low |
| PowerShell | Windows configuration | Low | Medium | Low |
| Ansible | Complex multi-service config | Medium | High | Medium |
| Chef Solo | Existing Chef shops | High | High | High |
| Puppet | Existing Puppet shops | High | High | High |
6. Post-Processors — Manifest, Compression, Artifice
Post-processors run after the builder has created the image and all provisioners have completed successfully. They are the final stage of the Packer pipeline and are used to process, transform, verify, or upload the resulting image artifacts. While they are sometimes overlooked, post-processors play a crucial role in enterprise image pipelines by generating audit metadata, compressing artifacts for efficient storage, and triggering downstream processes.
Manifest Post-Processor
The Manifest post-processor writes a JSON manifest file containing metadata about the built image. This metadata includes the image ID (AMI ID, image name, etc.), the builder type, the build time, and all custom tags applied to the image. Manifest files are essential for integrating Packer builds with downstream systems like Terraform, configuration management databases (CMDBs), and image registries. In a multi-platform build, the manifest provides a single source of truth for the artifact IDs produced by each builder.
HCL2
build {
name = "production-images"
sources = [
"source.amazon-ebs.ubuntu-web",
"source.amazon-ebs.ubuntu-api",
"source.azure-arm.windows-iis",
]
provisioner "shell" {
scripts = ["scripts/base-setup.sh", "scripts/hardening.sh"]
}
post-processor "manifest" {
output = "output/${build.name}-manifest.json"
strip_path = true
custom_data = {
git_commit = var.git_commit_sha
pipeline_id = var.pipeline_id
build_reason = var.build_reason
}
}
}
The manifest file output contains structured metadata that downstream tools like Terraform can consume to discover image IDs dynamically, without hardcoding AMI references. Each build entry records the builder type, source name, build timestamp, Packer run UUID, and all artifacts produced. The custom_data field allows you to inject CI/CD context like git commit SHAs and pipeline IDs for full traceability.
Compression Post-Processor
The Compression post-processor compresses image artifact files using gzip, bzip2, or zip compression. This is primarily used for local builders (QEMU, VMware) where the output is a file that needs to be stored or transferred. Compression can significantly reduce storage and transfer costs for large image files.
HCL2
source "qemu" "ubuntu" {
vm_name = "ubuntu-2404.qcow2"
format = "qcow2"
accelerator = "kvm"
ssh_username = "packer"
ssh_password = "packer"
shutdown_command = "sudo shutdown -P now"
disk_size = "30G"
iso_urls = ["ubuntu-24.04-live-server-amd64.iso"]
output_directory = "output/qemu"
}
build {
sources = ["qemu.ubuntu"]
provisioner "shell" {
scripts = ["scripts/setup.sh", "scripts/cleanup.sh"]
}
post-processor "compress" {
output = "output/qemu/ubuntu-2404-${build.name}.tar.gz"
compression_level = 9
keep_input_artifact = false
}
}
Artifice Post-Processor
The Artifice post-processor allows you to override the default artifacts produced by the builder. By default, Packer considers the builder's output as the final artifact. In some scenarios, you want to produce custom artifacts — for example, a binary compiled during provisioning, a configuration bundle, or a set of files assembled during the build. The Artifice post-processor lets you specify these custom files as the new artifacts, which can then be processed by subsequent post-processors. This is particularly useful in multi-stage builds where the provisioner creates output files that are more important than the base image itself.
Docker Tag and Push Post-Processors
When building Docker images, the docker-tag and docker-push post-processors allow you to tag images with version labels and push them to container registries. These post-processors handle authentication, tagging, and push operations, making it easy to integrate Docker image building into your Packer pipelines. You can tag images with multiple tags simultaneously, such as semantic versions, latest tags, and git commit-based tags.
Checksum and Notarize Post-Processors
For security-critical image pipelines, post-processors can generate checksums (SHA-256) of image artifacts and notarize them using external signing services. This provides cryptographic proof of image integrity and provenance, which is essential for compliance with standards like FedRAMP, SOC 2, and PCI DSS. The checksum post-processor writes a file containing the hash of each artifact, which can be verified before deployment to ensure no tampering occurred between build and deployment.
Custom Post-Processors
Packer supports custom post-processor plugins that can be written in Go using the Packer plugin SDK. Organizations with specific artifact processing needs — such as uploading to private image galleries, triggering webhook notifications, or updating CMDB records — can implement custom post-processors to integrate with their internal systems. The plugin SDK provides a well-defined interface for receiving artifact data and producing output artifacts, allowing the custom post-processor to seamlessly participate in the Packer pipeline.
| Post-Processor | Purpose | Input | Output | When to Use |
|---|---|---|---|---|
| manifest | Record build metadata | Builder artifacts | JSON manifest file | Always (for Terraform integration) |
| compress | Reduce artifact size | Image files | Compressed archive | Local builders (QEMU, VMware) |
| artifice | Override default artifacts | Provisioner outputs | Custom artifact list | Multi-stage builds |
| docker-tag | Tag Docker images | Docker images | Tagged images | Container image pipelines |
| docker-push | Push to registry | Tagged images | Published images | Registry distribution |
| checksum | Generate integrity hashes | Any artifacts | Checksum files | Security/compliance pipelines |
7. Multi-Platform Builds — Cross-Compilation and Parallel Builds
One of Packer's most powerful capabilities is the ability to build images for multiple platforms from a single configuration. This means you can define your image once and produce AMIs for AWS, Managed Images for Azure, images for GCP, Docker containers, and VMware templates — all in a single build invocation. Multi-platform builds reduce configuration duplication, ensure consistency across environments, and dramatically accelerate multi-cloud deployments.
Parallel Platform Builds
By default, Packer builds all sources in parallel when you invoke packer build. This means that an AWS AMI build, an Azure image build, and a Docker image build all run simultaneously, each creating their own temporary build instances. This parallelism significantly reduces total build time compared to sequential builds. The degree of parallelism can be controlled with the -parallel-builds flag, which is useful when you need to avoid exceeding cloud provider API rate limits or when your CI/CD runner has limited resources.
HCL2
source "amazon-ebs" "ubuntu-aws" {
ami_name = "app-${var.version}-aws"
instance_type = "t3.large"
region = "us-east-1"
source_ami_filter {
filters = { name = "ubuntu/images/hvm-ssd-gp3-*-amd64-server-*" }
owners = ["099720109477"]
most_recent = true
}
ssh_username = "ubuntu"
tags = { Platform = "aws", Version = var.version }
}
source "azure-arm" "ubuntu-azure" {
managed_image_name = "app-${var.version}-azure"
managed_image_resource_group_name = "images-rg"
subscription_id = var.azure_sub_id
tenant_id = var.azure_tenant_id
client_id = var.azure_client_id
client_secret = var.azure_client_secret
image_publisher = "Canonical"
image_offer = "0001-com-ubuntu-server-jammy"
image_sku = "22_04-lts-gen2"
location = "East US"
vm_size = "Standard_D4s_v3"
os_type = "Linux"
communicate_over_ssh = true
ssh_username = "azureuser"
os_disk_size_gb = 30
}
source "googlecompute" "ubuntu-gcp" {
project_id = var.gcp_project_id
zone = "us-central1-a"
machine_type = "e2-standard-4"
source_image_family = "ubuntu-2404-lts"
source_image_project_id = ["ubuntu-os-cloud"]
image_name = "app-${var.version}-gcp"
image_family = "app-images"
ssh_username = "packer"
disk_size = 30
}
source "docker" "ubuntu-docker" {
image = "ubuntu:24.04"
commit = true
changes = ["EXPOSE 8080", "WORKDIR /app"]
}
build {
name = "multi-platform"
sources = [
"source.amazon-ebs.ubuntu-aws",
"source.azure-arm.ubuntu-azure",
"source.googlecompute.ubuntu-gcp",
"source.docker.ubuntu-docker",
]
provisioner "shell" {
scripts = [
"scripts/install-base.sh",
"scripts/install-app.sh",
"scripts/configure.sh",
"scripts/cleanup.sh"
]
}
provisioner "file" {
source = "app/"
destination = "/opt/app"
}
post-processor "manifest" {
output = "output/${build.name}-manifest.json"
custom_data = { version = var.version, git_commit = var.git_commit }
}
}
Build Matrix Pattern
For scenarios where you need to produce multiple image variants with different configurations, Packer supports the build matrix pattern. You can define multiple build blocks that reference different sources with different variables, creating a matrix of platform-times-configuration combinations. This is useful for building images with different software stacks, different security profiles, or different resource allocations. The build matrix can be managed through variable files, where each file defines the configuration for a specific variant.
Cross-Compilation Considerations
When building multi-architecture images (amd64 and arm64), you need to consider how your application binaries will be compiled. Packer itself runs on the build host's architecture, but the provisioned instance runs on the target architecture. For cloud platforms, this means choosing the correct instance type (Graviton for AWS arm64, Ampere for Azure, Tau T2a for GCP) during the build. A common pattern is to use Docker's buildx feature for building multi-architecture container images, where Packer builds the base image and Docker buildx handles the cross-compilation.
| Target Architecture | AWS Instance Type | Azure VM Size | GCP Machine Type | Notes |
|---|---|---|---|---|
| x86_64 (amd64) | m6i.large | Standard_D4s_v3 | e2-standard-4 | Default for most workloads |
| ARM64 (aarch64) | m7g.large (Graviton3) | Standard_D4ps_v5 | t2a-standard-4 | Better price/performance |
| GPU (amd64) | g5.xlarge | Standard_NC4as_T4_v3 | a2-highgpu-1g | For ML/AI workloads |
Performance Optimization
Multi-platform builds can be time-consuming if not optimized. Key optimization strategies include using the fastest possible instance types for build VMs, leveraging package caching and mirrors, minimizing the number of provisioner steps, and using pre-compiled binaries where possible. The -parallel-builds flag should be set based on your CI/CD runner's capacity and cloud provider API rate limits. For large-scale operations, consider splitting builds across multiple CI/CD jobs to maximize throughput. Additionally, using Packer's only and except flags allows developers to build only specific platforms during development, reserving full multi-platform builds for CI/CD pipelines.
8. Machine Image Best Practices — Hardening, CIS, Minimal Images
Building production-grade machine images requires more than just installing software on a base OS. It demands a disciplined approach to security hardening, minimal footprint design, comprehensive testing, and ongoing maintenance. The best practices outlined in this section represent industry standards that senior engineers should follow when designing image automation systems for production environments.
Security Hardening
Security hardening is the process of reducing the attack surface of an image by disabling unnecessary services, removing unused software packages, configuring firewalls, enabling auditing, and applying security patches. A well-hardened image should comply with your organization's security policies and relevant compliance frameworks such as CIS Benchmarks, NIST 800-123, and DISA STIGs. The hardening process should be automated as part of your Packer build pipeline, ensuring that every image produced meets the same security baseline without manual intervention.
bash
#!/bin/bash
# scripts/hardening.sh - Ubuntu CIS Level 2 Hardening
set -euo pipefail
echo "=== CIS Level 2 Hardening - Ubuntu 24.04 ==="
# 1. Filesystem Configuration
echo "Configuring filesystem permissions..."
chmod 600 /etc/shadow
chmod 600 /etc/gshadow
chmod 644 /etc/passwd
chmod 644 /etc/group
chmod 700 /boot/grub
# 2. Network Hardening
echo "Applying network hardening..."
cat >> /etc/sysctl.conf << 'EOF'
net.ipv4.ip_forward = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOF
sysctl -p
# 3. SSH Hardening
echo "Hardening SSH..."
cat > /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
Protocol 2
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
X11Forwarding no
AllowTcpForwarding no
MaxAuthTries 3
LoginGraceTime 60
ClientAliveInterval 300
ClientAliveCountMax 2
PermitEmptyPasswords no
UsePAM yes
Banner /etc/issue.net
EOF
# 4. Remove unnecessary packages
echo "Removing unnecessary packages..."
apt-get remove -y telnet rsh-client rsh-server ypbind rsh talk telnetd 2>/dev/null || true
# 5. Enable auditd
echo "Configuring audit rules..."
cat > /etc/audit/rules.d/hardening.rules << 'EOF'
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-a always,exit -F arch=b64 -S execve -C uid!=euid -F euid=0 -k privilege_escalation
-w /var/log/auth.log -p wa -k auth_log
-w /var/log/syslog -p wa -k syslog
EOF
systemctl enable auditd
echo "=== Hardening Complete ==="
CIS Benchmark Compliance
The Center for Internet Security (CIS) publishes benchmark documents for most major operating systems, providing detailed, step-by-step hardening instructions. Automating CIS compliance in your Packer builds ensures that every image produced meets a consistent security standard. Tools like InSpec and OpenSCAP can be used as provisioners or post-build validators to verify CIS compliance. Each control maps to a specific numbered requirement in the CIS Benchmark document, creating a clear audit trail from your automation code to the compliance framework.
Minimal Image Philosophy
Minimal images contain only the packages and services necessary for their intended purpose. Smaller images have several advantages: faster boot times, reduced attack surface, smaller storage costs, faster distribution, and quicker provisioning. The principle of least privilege applies to images — if a package or service is not needed, it should not be included. This philosophy extends to the kernel itself, where unnecessary modules should be blacklisted to prevent loading.
| Hardening Area | Actions | CIS Reference | Impact |
|---|---|---|---|
| Filesystem | Set correct permissions, mount options (nodev, nosuid) | 1.x | High — Prevents local privilege escalation |
| Network | Sysctl tuning, disable IPv6, enable SYN cookies | 3.x | High — Prevents network-based attacks |
| SSH | Disable root login, key-only auth, limit retries | 5.x | Critical — Protects remote access |
| Logging | Enable auditd, configure log rotation | 4.x | Medium — Enables forensics and compliance |
| Services | Disable unused services, remove unnecessary packages | 2.x | High — Reduces attack surface |
| Updates | Enable automatic security updates | 1.x | Critical — Patches known vulnerabilities |
Image Cleanup and Optimization
The final provisioner in any image build should perform cleanup operations to minimize image size and remove sensitive build-time artifacts. This includes clearing package manager caches, removing temporary files, removing SSH keys generated during build, clearing log files, and zeroing out free space for VMware/QEMU images to enable thin provisioning compression.
bash
#!/bin/bash
# scripts/cleanup.sh - Final image cleanup
set -euo pipefail
echo "=== Image Cleanup ==="
# Clear package manager cache
apt-get autoremove -y
apt-get clean
rm -rf /var/lib/apt/lists/*
# Remove build-time SSH keys
rm -rf /root/.ssh/authorized_keys
rm -rf /home/*/.ssh/authorized_keys
rm -rf /etc/ssh/ssh_host_*
# Clear log files
find /var/log -type f -exec truncate -s 0 {} \;
rm -rf /var/log/*.gz /var/log/*.1
# Remove temporary files
rm -rf /tmp/* /var/tmp/*
# Remove cloud-init artifacts
cloud-init clean
# Truncate machine-id (will be regenerated on first boot)
truncate -s 0 /etc/machine-id
# Clear shell history
history -c
unset HISTFILE
# Zero free space (for better compression of VMware/QEMU images)
dd if=/dev/zero of=/zero.fill bs=1M 2>/dev/null || true
rm -f /zero.fill
sync
echo "=== Cleanup Complete ==="
Image Size Benchmarks
Target image sizes should be established as organizational standards. For reference, a well-optimized Ubuntu 24.04 base image with minimal packages should be under 2 GB for cloud deployments. A fully provisioned web server image with nginx, application code, and monitoring agents should typically be under 4 GB. Container images should target under 200 MB where possible. These benchmarks help teams identify configuration bloat and maintain deployment performance.
9. Integration with Terraform — AMI References, Data Sources
The integration between Packer and Terraform is one of the most important patterns in modern infrastructure automation. Packer creates the machine images, and Terraform uses those images to provision infrastructure. This separation of concerns — image building (Packer) versus infrastructure provisioning (Terraform) — is a cornerstone of immutable infrastructure practice. The two tools share HCL2 syntax, making configurations naturally composable, and they can be connected through manifests, data sources, and shared variables.
Using Manifest Files
The most common integration pattern uses the Packer manifest file as the contract between Packer and Terraform. After Packer builds an image, the manifest file contains the artifact ID (AMI ID, image name, etc.) that Terraform needs to reference. Terraform can read this manifest file using the jsondecode() function combined with the file() function to extract the image ID dynamically. This approach creates a clean, auditable handoff between the image build and infrastructure provisioning stages.
HCL2
# terraform/main.tf - Launching instances with Packer-built AMIs
variable "packer_manifest_path" {
default = "../packer/output/manifest.json"
}
locals {
manifest = jsondecode(file(var.packer_manifest_path))
ami_id = local.manifest.builds[0].artifacts[0].name
build_version = local.manifest.builds[0].custom_data.git_commit
}
resource "aws_instance" "web" {
ami = local.ami_id
instance_type = "t3.large"
tags = {
Name = "web-server"
AMIVersion = local.build_version
ManagedBy = "terraform"
PackerManaged = "true"
}
root_block_device {
volume_size = 50
volume_type = "gp3"
encrypted = true
}
}
Data Source References
For more dynamic scenarios, Terraform data sources can query cloud providers for the latest Packer-built images without relying on manifest files. This is particularly useful when Packer publishes AMIs to multiple regions or shares them across multiple AWS accounts. The data source pattern allows Terraform to automatically discover the most recent image based on tags or naming conventions, creating a fully decoupled pipeline where Packer and Terraform operate independently.
HCL2
data "aws_ami" "latest_web" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["packer-web-server-*"]
}
filter {
name = "state"
values = ["available"]
}
filter {
name = "tag:Environment"
values = ["production"]
}
filter {
name = "tag:PackerManaged"
values = ["true"]
}
}
data "aws_ami" "latest_api" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["packer-api-server-*"]
}
filter {
name = "tag:PackerManaged"
values = ["true"]
}
}
output "web_ami_id" {
value = data.aws_ami.latest_web.id
}
output "api_ami_id" {
value = data.aws_ami.latest_api.id
}
Shared Variable Patterns
When managing large numbers of images across multiple environments, maintaining consistency between Packer and Terraform configurations is crucial. Shared variable definitions, environment variable conventions, and configuration management pipelines help ensure that the image built by Packer matches the assumptions made by Terraform. Organizations typically maintain a shared variable repository or use a tool like Terragrunt to manage environment-specific configurations for both Packer and Terraform, ensuring that variables like region, instance type, and naming conventions remain synchronized.
Golden Image Module Pattern
A powerful pattern for large organizations is the Golden Image Module, where a Terraform module encapsulates the logic for launching instances from Packer-built images. The module accepts the image name as input and handles AMI discovery, instance launch, security group configuration, and monitoring setup. Different teams can use this module with different image names to deploy their specific workloads while inheriting the organization's standard infrastructure patterns. This module acts as an abstraction layer that prevents teams from directly interacting with raw AMI IDs and provides consistent tagging, monitoring, and security configurations across all workloads.
| Integration Method | Pros | Cons | Best For |
|---|---|---|---|
| Manifest File | Explicit, version-controlled, auditable | Requires file to exist, tight coupling | CI/CD pipelines with shared artifacts |
| Data Source (by name) | Dynamic discovery, no file dependency | Requires naming conventions, slower | Multi-region, multi-account setups |
| Data Source (by tag) | Flexible filtering, self-documenting | Depends on correct tagging | Large-scale, multi-variant deployments |
| SSM Parameter Store | Centralized, cross-account, auditable | Additional AWS dependency | Enterprise multi-account strategies |
10. CI/CD Integration — GitHub Actions, Jenkins, GitLab CI
Automating Packer builds through CI/CD pipelines is essential for maintaining up-to-date, secure, and consistent machine images. A well-designed CI/CD pipeline for image building should handle plugin initialization, credential management, parallel builds across platforms, artifact storage, image testing, compliance scanning, and notification. This section covers integration patterns for the three most popular CI/CD platforms, demonstrating production-grade configurations that handle real-world requirements.
GitHub Actions Integration
GitHub Actions provides a natural fit for Packer builds due to its native integration with GitHub repositories, support for reusable workflows, and extensive marketplace of actions. The following workflow demonstrates a production-grade Packer build pipeline with multi-platform support, testing, and image publishing. It uses matrix strategies for parallel builds across image variants, OIDC for AWS authentication (eliminating long-lived credentials), and artifact management for passing build outputs between jobs.
YAML
# .github/workflows/packer-build.yml
name: Packer Image Build
on:
push:
branches: [main]
paths:
- 'packer/**'
- 'ansible/**'
- '.github/workflows/packer-build.yml'
schedule:
- cron: '0 6 * * 1' # Weekly rebuild Monday 6 AM UTC
workflow_dispatch:
inputs:
target_platform:
description: 'Target platform (all, aws, azure, gcp)'
required: false
default: 'all'
build_reason:
description: 'Reason for build'
required: true
default: 'scheduled'
permissions:
id-token: write
contents: read
env:
PACKER_VERSION: "1.11.0"
jobs:
validate:
name: Validate Packer Config
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Packer
uses: hashicorp/setup-packer@main
with:
version: ${{ env.PACKER_VERSION }}
- name: Packer Init
run: packer init packer/
working-directory: .
- name: Packer Validate
run: packer validate packer/
working-directory: .
build-aws:
name: Build AWS ${{ matrix.variant }}
needs: validate
if: ${{ github.event.inputs.target_platform == 'all' || github.event.inputs.target_platform == 'aws' }}
runs-on: ubuntu-latest
strategy:
matrix:
variant: [web-server, api-server, worker]
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/packer-build
aws-region: us-east-1
- name: Setup Packer
uses: hashicorp/setup-packer@main
with:
version: ${{ env.PACKER_VERSION }}
- name: Packer Init
run: packer init packer/
- name: Build ${{ matrix.variant }}
run: |
packer build \
-var "variant=${{ matrix.variant }}" \
-var "environment=production" \
-var "git_commit=${{ github.sha }}" \
-only="amazon-ebs.${{ matrix.variant }}" \
packer/
- name: Upload Manifest
uses: actions/upload-artifact@v4
with:
name: manifest-${{ matrix.variant }}
path: packer/output/${{ matrix.variant }}-manifest.json
retention-days: 30
Jenkins Pipeline
For organizations using Jenkins, a declarative pipeline provides robust build orchestration with support for shared libraries, credential management, and complex branching strategies. Jenkins pipelines can leverage the Packer Builder plugin or invoke Packer directly through shell commands. The following example shows a Jenkinsfile that handles parallel builds across AWS and Azure, integrates with HashiCorp Vault for credential management, and runs InSpec tests after builds complete.
Groovy
// Jenkinsfile
pipeline {
agent {
docker {
image 'hashicorp/packer:1.11'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
environment {
AWS_DEFAULT_REGION = 'us-east-1'
VAULT_ADDR = credentials('vault-addr')
VAULT_TOKEN = credentials('vault-token')
}
parameters {
choice(name: 'PLATFORM', choices: ['all', 'aws', 'azure', 'gcp'], description: 'Target platform')
choice(name: 'VARIANT', choices: ['all', 'web-server', 'api-server', 'worker'], description: 'Image variant')
}
stages {
stage('Initialize') {
steps { sh 'packer init packer/' }
}
stage('Validate') {
steps { sh 'packer validate -only="${params.PLATFORM}.*" packer/' }
}
stage('Build') {
parallel {
stage('Build AWS') {
when { expression { params.PLATFORM in ['all', 'aws'] } }
steps {
withAWS(credentials: 'packer-aws-creds') {
sh "packer build -only=\"amazon-ebs.${params.VARIANT}\" packer/"
}
}
}
stage('Build Azure') {
when { expression { params.PLATFORM in ['all', 'azure'] } }
steps {
withCredentials([file(credentialsId: 'azure-arm-creds', variable: 'AZURE_AUTH')]) {
sh "packer build -only=\"azure-arm.${params.VARIANT}\" packer/"
}
}
}
}
}
stage('Test') {
steps { sh 'inspec exec tests/image-compliance.rb -t aws://' }
}
}
post {
always { archiveArtifacts artifacts: 'packer/output/*.json', allowEmptyArchive: true }
}
}
GitLab CI Integration
GitLab CI provides native integration with GitLab repositories and supports advanced features like DAG scheduling, cache management, and environment tracking. The following example demonstrates a complete GitLab CI configuration with parallel builds, artifact caching, and compliance testing integrated into the pipeline.
YAML
# .gitlab-ci.yml
stages:
- validate
- build
- test
- publish
variables:
PACKER_CACHE_DIR: "$CI_PROJECT_DIR/.packer_cache"
packer_validate:
stage: validate
image: hashicorp/packer:1.11
script:
- packer init packer/
- packer validate packer/
cache:
key: packer-plugins
paths:
- .packer_plugins/
build_aws:
stage: build
image: hashicorp/packer:1.11
needs: [packer_validate]
script:
- packer build -only="amazon-ebs.*" packer/
artifacts:
paths:
- packer/output/*.json
expire_in: 30 days
build_azure:
stage: build
image: hashicorp/packer:1.11
needs: [packer_validate]
script:
- packer build -only="azure-arm.*" packer/
artifacts:
paths:
- packer/output/*.json
expire_in: 30 days
test_images:
stage: test
image: chef/inspec:latest
needs: [build_aws, build_azure]
script:
- inspec exec tests/ -t aws://
allow_failure: false
CI/CD Pipeline Architecture
| CI/CD Platform | Secret Management | Parallel Execution | Caching | Native Packer Support |
|---|---|---|---|---|
| GitHub Actions | Repository Secrets, OIDC | Matrix Strategy | actions/cache | hashicorp/setup-packer |
| Jenkins | Credentials Plugin, Vault | Parallel Stage | stash/unstash | Packer Builder Plugin |
| GitLab CI | CI/CD Variables, Vault | DAG / Parallel | cache keyword | hashicorp/packer image |
| Azure DevOps | Pipelines Variables, Key Vault | Matrix Strategy | Cache task | Packer Tasks Extension |
11. Image Testing — InSpec, Packer Test, Validation
Testing machine images is a critical practice that ensures the images produced by Packer meet security, compliance, and functional requirements. Unlike traditional software testing, image testing validates the state of an entire operating system and its configured software stack. A comprehensive image testing strategy combines multiple testing approaches: syntax validation, security scanning, compliance verification, and functional testing. The goal is to catch issues at every level before images reach production environments where they could affect thousands of running instances.
Packer Validate and Format
The first line of defense is Packer's built-in validation command, which checks HCL2 syntax, variable types, plugin compatibility, and provider-specific constraints without actually building an image. This fast feedback loop catches configuration errors before they consume cloud resources in expensive build processes. The packer fmt command ensures consistent formatting across all team members, and the -check flag can be used in CI/CD pipelines to enforce formatting standards.
bash
#!/bin/bash
# scripts/validate.sh - Pre-build validation pipeline
set -euo pipefail
echo "=== Packer Validation Pipeline ==="
# 1. Format check
echo "Checking HCL2 formatting..."
packer fmt -check -diff packer/
# 2. Initialize required plugins
echo "Initializing plugins..."
packer init packer/
# 3. Validate all configurations
echo "Validating configuration..."
packer validate \
-var "environment=testing" \
-var "version=test" \
packer/
# 4. Variable validation test
echo "Testing variable validation..."
if packer validate -var "environment=invalid" packer/ 2>&1 | grep -q "Environment must be"; then
echo "Variable validation working correctly"
else
echo "WARNING: Variable validation may not be configured"
exit 1
fi
echo "=== Validation Complete ==="
InSpec Compliance Testing
InSpec is an open-source compliance testing framework that expresses security and compliance requirements as executable code. InSpec profiles can verify file permissions, package installations, service configurations, network settings, and user accounts. After Packer builds an image, InSpec tests can be run against a running instance to verify that the image meets all compliance requirements. InSpec profiles are version-controlled, auditable, and can be shared across teams as compliance packages.
Ruby
# tests/image-compliance.rb - InSpec compliance profile
title 'Packer Image Compliance Tests'
control 'cis-1.1.1' do
title 'Disable unused filesystems'
desc 'Ensure mounting of cramfs, freevxfs, hfs, hfsplus is disabled'
%w[cramfs freevxfs hfs hfsplus udf].each do |fs|
describe kernel_module(fs) do
it { should_not be_loaded }
end
end
end
control 'cis-4.1' do
title 'Audit System - auditd enabled'
desc 'Ensure auditd is installed and running'
describe package('auditd') do
it { should be_installed }
end
describe service('auditd') do
it { should be_enabled }
it { should be_running }
end
end
control 'cis-5.2' do
title 'SSH Server Configuration'
desc 'Ensure SSH is configured securely'
describe sshd_config do
its('Protocol') { should eq '2' }
its('PermitRootLogin') { should eq 'no' }
its('PasswordAuthentication') { should eq 'no' }
its('X11Forwarding') { should eq 'no' }
its('MaxAuthTries') { should cmp <= 3 }
its('ClientAliveInterval') { should cmp >= 300 }
its('AllowTcpForwarding') { should eq 'no' }
its('PermitEmptyPasswords') { should eq 'no' }
end
end
control 'cis-3.4' do
title 'Network Parameters - Hosts and Router'
desc 'Ensure network parameters are hardened'
describe sysctl('net.ipv4.ip_forward') do
its('value') { should eq 0 }
end
describe sysctl('net.ipv4.conf.all.accept_redirects') do
its('value') { should eq 0 }
end
describe sysctl('net.ipv4.conf.all.rp_filter') do
its('value') { should eq 1 }
end
end
control 'app-1.1' do
title 'Nginx Configuration'
desc 'Ensure nginx is installed and configured correctly'
describe package('nginx') do
it { should be_installed }
end
describe service('nginx') do
it { should be_enabled }
it { should be_running }
end
describe file('/etc/nginx/nginx.conf') do
its('content') { should match(/server_tokens off/) }
end
describe port(80) do
it { should be_listening }
end
end
control 'app-1.2' do
title 'Disk Space'
desc 'Ensure adequate disk space'
describe command('df / --output=pcent | tail -1 | tr -d " %"') do
its('stdout.to_i') { should be < 80 }
end
end
Packer Test Framework
Packer 1.8+ introduced the built-in packer test command, which allows you to write tests that validate Packer builds end-to-end. Packer tests spin up actual build instances, run the full build process, verify the output, and tear down resources. While slower than InSpec or validate, Packer tests provide the highest confidence that images build correctly across different environments and configurations.
Test Pyramid for Images
The image test pyramid defines the different levels of testing that should be applied to machine images, from fast unit-level checks to comprehensive integration tests. Following this pyramid ensures that most issues are caught at the fastest, cheapest level while still maintaining comprehensive coverage through higher-level tests. The pyramid also helps teams prioritize their testing investments and allocate CI/CD resources effectively.
| Test Level | Tool | Execution Time | Cost | Frequency |
|---|---|---|---|---|
| Syntax | packer validate, packer fmt | Seconds | Zero | Every commit / PR |
| Security | InSpec, Trivy, Lynis | 2-5 minutes | Low (single instance) | Every build |
| Functional | InSpec, Shell scripts, Smoke tests | 5-15 minutes | Medium (instance time) | Every build |
| Integration | Packer Test, Terratest | 30-60 minutes | High (full infra) | Nightly / Weekly |
12. Security — Secret Management, Encrypted Variables, Vault Integration
Security in Packer image automation spans multiple dimensions: protecting credentials used during builds, ensuring secrets do not leak into images, encrypting image artifacts, securing the CI/CD pipeline, and managing access to published images. A single misconfigured Packer build can expose AWS access keys, database passwords, or API tokens in a publicly shared AMI. Implementing robust security practices is not optional — it is a fundamental requirement for production image pipelines that handle sensitive workloads.
Encrypted Variables
Packer supports encrypted variable files using SOPS (Secrets OPerationS) by Mozilla, which integrates seamlessly with Packer and supports multiple key management backends including AWS KMS, GCP KMS, Azure Key Vault, and PGP. Encrypted variables ensure that sensitive values are never stored in plaintext in version control, providing defense-in-depth for credential management.
bash
# Encrypting variable files with SOPS
# 1. Create the variable file
cat > secrets.pkrvars.hcl << 'EOF'
aws_access_key = "AKIAIOSFODNN7EXAMPLE"
aws_secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
vault_token = "hvs.CAESIabc123def456"
ssh_private_key = "-----BEGIN RSA PRIVATE KEY-----\n..."
EOF
# 2. Encrypt with SOPS using AWS KMS
sops --encrypt --kms "arn:aws:kms:us-east-1:123456789:key/abc-123" \
secrets.pkrvars.hcl > secrets.pkrvars.sops.hcl
# 3. Use encrypted file with Packer
packer build -var-file=secrets.pkrvars.sops.hcl packer/
# 4. SOPS automatically decrypts at runtime
# The decrypted values never touch disk in plaintext
HashiCorp Vault Integration
HashiCorp Vault provides a more sophisticated approach to secret management for Packer builds. Instead of decrypting static secrets, Packer can dynamically request credentials from Vault at build time. Vault can generate short-lived AWS STS credentials, database passwords, and API tokens that automatically expire after the build completes. This eliminates the risk of long-lived credentials being exposed and ensures that every build uses fresh, auditable credentials.
HCL2
# vault-integrated-build.pkr.hcl
packer {
required_plugins {
amazon = {
version = ">= 1.2.0"
source = "github.com/hashicorp/amazon"
}
}
}
# Use dynamic AWS credentials from Vault
locals {
vault_address = env("VAULT_ADDR")
vault_token = env("VAULT_TOKEN")
}
source "amazon-ebs" "secure-build" {
access_key = vault("/aws/creds/packer-role", "access_key")
secret_key = vault("/aws/creds/packer-role", "secret_key")
ami_name = "secure-image-${formatdate("YYYYMMDD", timestamp())}"
instance_type = "m6i.large"
region = "us-east-1"
source_ami_filter {
filters = { name = "ubuntu/images/hvm-ssd-gp3-*-amd64-server-*" }
owners = ["099720109477"]
most_recent = true
}
ssh_username = "ubuntu"
# Encrypted EBS volumes
launch_block_device_mappings {
device_name = "/dev/sda1"
volume_size = 50
volume_type = "gp3"
encrypted = true
kms_key_id = "arn:aws:kms:us-east-1:123456789:key/abc-123"
delete_on_termination = true
}
}
Prevent Secrets from Leaking into Images
One of the most critical security practices is ensuring that build-time secrets never end up inside the final image. Secrets used by provisioners must be cleaned up after use. Packer's build instance is ephemeral and should be destroyed after the image is created, but any secrets written to disk within the instance could be captured in the image snapshot. Always use in-memory secret handling where possible, and explicitly overwrite and delete any files that contained secrets during the build process.
bash
#!/bin/bash
# scripts/secure-provisioning.sh
# Pattern for using secrets without leaking them into images
set -euo pipefail
# Retrieve secrets from Vault (not from environment or files)
SECRET=$(vault kv get -field=api_key secret/app/config)
DB_PASS=$(vault kv get -field=password secret/db/production)
# Use secrets immediately and securely
curl -H "Authorization: Bearer ${SECRET}" https://api.example.com/config > /tmp/config.json
# Apply configuration
sudo mv /tmp/config.json /opt/app/config.json
sudo chmod 600 /opt/app/config.json
# IMPORTANT: Clear the secrets from the shell
unset SECRET
unset DB_PASS
# Clear vault token
vault token revoke $(vault token lookup -format=json | jq -r '.id')
# Clear shell history and temp files
history -c
unset HISTFILE
rm -f /tmp/config.json
find /root -name "*.key" -o -name "*.pem" -o -name "*credentials*" -delete 2>/dev/null || true
Image Signing and Verification
After building an image, you can cryptographically sign it to prove its provenance and integrity. AWS supports AMI signing with AWS Signer, and GCP supports image signing with Cloud KMS. Signed images can be verified before deployment, ensuring that only authorized, tamper-proof images are used in production. This is a critical requirement for compliance frameworks that mandate image integrity verification.
Secure Build Environment
The Packer build environment itself must be secured. This includes using hardened CI/CD runners, isolating build networks, restricting IAM permissions to least privilege, enabling audit logging for all build operations, and using ephemeral build credentials that automatically expire. Build runners should be provisioned in isolated VPCs with no access to production networks, and all build activity should be logged to centralized SIEM systems for monitoring and incident response.
| Security Concern | Mitigation | Tool/Technique |
|---|---|---|
| Credential exposure in CI/CD | Use OIDC, short-lived tokens, Vault | GitHub OIDC, Vault AWS Secrets Engine |
| Secrets in images | Clean up secrets after use, never bake in | Script cleanup, Vault dynamic secrets |
| Unencrypted AMIs | Enable EBS encryption with KMS keys | KMS, encrypted launch_block_device_mappings |
| Unverified image provenance | Sign images, verify before deployment | AWS Signer, GCP Image Signing |
| Broad IAM permissions | Least-privilege IAM roles for builds | Scoped IAM policies, condition keys |
13. Image Versioning and Distribution — AMI, VHD, OCI
Image versioning and distribution are critical operations in enterprise image management. As organizations produce dozens or hundreds of images, they need systematic approaches to version tracking, artifact storage, cross-region distribution, and lifecycle management. Proper versioning enables rollback, audit trails, and selective deployment, while proper distribution ensures that images are available in all regions and accounts where they are needed. Without systematic versioning and distribution, organizations quickly lose track of which images are current, which are deprecated, and which instances are running which versions.
Versioning Strategies
Image versioning can follow several patterns depending on organizational needs. Semantic versioning (MAJOR.MINOR.PATCH) is common for application-specific images. Date-based versioning (YYYYMMDD-HHMM) works well for regularly rebuilt base images. Build number versioning (sequential integers) integrates naturally with CI/CD pipelines. Git SHA-based versioning provides direct traceability to source code. A composite strategy combining multiple approaches often provides the best balance of human readability and machine traceability.
| Versioning Strategy | Format | Pros | Cons | Best For |
|---|---|---|---|---|
| Semantic Versioning | v1.2.3 | Clear intent, standard convention | Requires manual version bumps | Application-specific images |
| Date-based | 20260715-0630 | Chronological, auto-generated | Less semantic meaning | Base OS images |
| Build Number | build-234 | Unique, CI/CD native | Requires CI/CD context | CI/CD-driven pipelines |
| Git SHA | a1b2c3d | Direct source traceability | Not human-friendly | Audit-critical environments |
| Composite | v1.2.3-20260715-234 | Combines all benefits | Complex | Enterprise at scale |
Cross-Region AMI Distribution
For global deployments, images must be available in multiple AWS regions. The Amazon EBS builder's ami_regions option automatically copies the AMI to specified regions after creation. However, cross-region copies can take 30+ minutes for large images, so consider building in multiple regions in parallel for time-critical pipelines. When sharing AMIs across accounts, the ami_users option controls which AWS accounts can access the images, and permissions are managed through the AMI launch permission API.
HCL2
source "amazon-ebs" "global-ubuntu" {
ami_name = "app-global-${formatdate("YYYYMMDD", timestamp())}"
instance_type = "m6i.xlarge"
region = "us-east-1"
ami_regions = [
"us-east-1", "us-west-2", "eu-west-1",
"eu-central-1", "ap-southeast-1", "ap-northeast-1",
]
ami_users = [
"111111111111", # Production account
"222222222222", # Staging account
"333333333333", # DR account
]
tags = {
Name = "app-global"
Version = var.version
GlobalBuild = "true"
}
}
Azure Shared Image Gallery
Azure Shared Image Gallery (now Azure Compute Gallery) provides native image distribution, versioning, and replication across regions. Images published to the gallery are automatically replicated to configured regions and can be versioned with semantic versions, replication status, and end-of-life dates. The gallery also supports image definitions that group versions of the same image, making it easy for consumers to reference the latest version of a particular image type without knowing specific version numbers.
Artifact Storage Patterns
For non-cloud image formats (VHD, QCOW2), artifacts should be stored in durable object storage with appropriate lifecycle policies. AWS S3, Azure Blob Storage, and GCS provide durable, versioned storage for image files. Implement lifecycle policies to archive old images to cheaper storage tiers and eventually delete them. Versioning on the storage bucket provides an additional safety net against accidental deletion or overwrites, which is critical for maintaining the ability to roll back to previous image versions.
HCL2
# Terraform configuration for S3 lifecycle on image artifacts
resource "aws_s3_bucket_lifecycle_configuration" "image_artifacts" {
bucket = aws_s3_bucket.image_artifacts.id
rule {
id = "archive-old-images"
status = "Enabled"
filter { prefix = "images/" }
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration { days = 365 }
}
}
Image Distribution Architecture
Lifecycle Management
Image lifecycle management defines when images should be deprecated, archived, and deleted. A typical lifecycle might keep images active for 90 days, archive to cold storage for another 275 days, and then permanently delete. This must be coordinated with infrastructure teams to ensure no running instances are using deprecated images. Automated tools can scan running instances against the image registry and alert when instances are running on images that are approaching end-of-life, giving teams time to plan upgrades or rebuilds.
14. Compliance and Auditing — Image Scanning, SBOM
In regulated industries and security-conscious organizations, machine images must meet compliance requirements defined by frameworks like CIS Benchmarks, NIST SP 800-123, PCI DSS, HIPAA, SOC 2, and FedRAMP. Compliance involves not only hardening images to meet security baselines but also maintaining audit trails, generating Software Bill of Materials (SBOM), scanning for vulnerabilities, and providing evidence of compliance for auditors. The compliance automation capability of Packer-driven image pipelines is one of their strongest advantages over manual image management processes.
Vulnerability Scanning
Every image should be scanned for known vulnerabilities (CVEs) before being published for use. Tools like Trivy, Grype, Clair, and AWS Inspector can scan image snapshots or running instances to identify vulnerable packages and configuration issues. Vulnerability scanning should be integrated into the CI/CD pipeline to block images with critical or high-severity vulnerabilities from being promoted to production. This automated gate prevents vulnerable images from ever reaching production environments.
bash
#!/bin/bash
# scripts/scan-image.sh - Vulnerability scanning pipeline
set -euo pipefail
AMI_ID=$1
REGION=${2:-us-east-1}
echo "=== Image Vulnerability Scanning ==="
echo "Scanning AMI: ${AMI_ID} in ${REGION}"
# 1. Start an instance from the AMI for scanning
INSTANCE_ID=$(aws ec2 run-instances \
--image-id "${AMI_ID}" \
--instance-type t3.medium \
--region "${REGION}" \
--query 'Instances[0].InstanceId' \
--output text)
echo "Launched scanning instance: ${INSTANCE_ID}"
aws ec2 wait instance-running --instance-ids "${INSTANCE_ID}" --region "${REGION}"
sleep 60
# 2. Run Trivy scan on the running instance
INSTANCE_IP=$(aws ec2 describe-instances --instance-ids "${INSTANCE_ID}" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text --region "${REGION}")
trivy rootfs --severity HIGH,CRITICAL \
--format json --output trivy-report.json \
"ssh://ubuntu@${INSTANCE_IP}"
# 3. Parse results
HIGH_VULNS=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "HIGH")] | length' trivy-report.json)
CRITICAL_VULNS=$(jq '[.Results[].Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' trivy-report.json)
echo "=== Scan Results ==="
echo "High vulnerabilities: ${HIGH_VULNS}"
echo "Critical vulnerabilities: ${CRITICAL_VULNS}"
# 4. Fail build if critical vulnerabilities found
if [ "${CRITICAL_VULNS}" -gt 0 ]; then
echo "FAILED: Critical vulnerabilities found. Blocking image publication."
aws ec2 terminate-instances --instance-ids "${INSTANCE_ID}" --region "${REGION}"
exit 1
fi
# 5. Cleanup
aws ec2 terminate-instances --instance-ids "${INSTANCE_ID}" --region "${REGION}"
echo "=== Scan Complete ==="
Software Bill of Materials (SBOM)
An SBOM is a comprehensive inventory of all software components, libraries, and dependencies included in an image. SBOMs are increasingly required by regulations (US Executive Order 14028, EU Cyber Resilience Act) and are essential for rapid vulnerability response. When a new CVE is disclosed, an SBOM allows you to immediately determine which images are affected and prioritize remediation efforts accordingly.
bash
# Generate SBOM using Syft
# Creates machine-readable inventory of all packages in the image
syft packages dir:/ -o spdx-json > sbom-spdx.json
syft packages dir:/ -o cyclonedx-json > sbom-cyclonedx.json
# Upload SBOM to S3 for audit trail
aws s3 cp sbom-spdx.json \
"s3://image-compliance/${AMI_ID}/sbom-spdx.json"
aws s3 cp sbom-cyclonedx.json \
"s3://image-compliance/${AMI_ID}/sbom-cyclonedx.json"
# Optionally attach SBOM to the AMI as a description tag
aws ec2 create-tags --resources "${AMI_ID}" \
--tags Key=SBOMLocation,Value="s3://image-compliance/${AMI_ID}/sbom-spdx.json"
Compliance Automation with OpenSCAP
OpenSCAP provides automated compliance checking against DISA STIGs, CIS Benchmarks, and custom OVAL profiles. It generates detailed compliance reports with pass/fail status for each control and overall compliance scores that can be presented to auditors. OpenSCAP integrates well with Packer builds as a provisioner step, allowing compliance verification to happen during the build process rather than as a separate post-build test.
Audit Trail and Evidence Collection
For compliance, you must maintain detailed audit trails of every image build. This includes who triggered the build, what source code version was used, what Packer configuration was applied, what vulnerabilities were found and remediated, who approved the image for production, and when the image was deployed. Store this evidence in a centralized, tamper-evident location like an immutable S3 bucket with versioning and object lock enabled. Audit evidence should be automatically generated and collected as part of the CI/CD pipeline, not manually gathered before audits.
| Compliance Requirement | Tool | Output | Storage |
|---|---|---|---|
| Vulnerability Scanning | Trivy, Grype, AWS Inspector | JSON/HTML scan reports | S3 + Compliance DB |
| CIS Benchmark | InSpec, OpenSCAP | Pass/Fail per control | S3 + Compliance DB |
| SBOM | Syft, CDX Generator | SPDX, CycloneDX JSON | S3 + Artifact Registry |
| Build Audit | Packer Manifest, CI/CD Logs | JSON manifests, pipeline logs | S3 (Object Lock) |
| Approval Evidence | Git commits, PR approvals | Git history, approval records | Git + Compliance DB |
15. Packer vs Docker — When to Use Each
A common question in infrastructure design is whether to use Packer for machine images or Docker for containers. While both tools create pre-configured, reproducible artifacts, they serve fundamentally different use cases and operate at different levels of abstraction. Understanding when to use each — and when to use both together — is essential for making informed architectural decisions. The choice is not always either-or; many successful architectures use both tools in complementary roles.
Fundamental Differences
Packer creates complete machine images — bootable disk images containing an entire operating system, kernel, and application stack. Docker creates container images — lightweight, layered filesystem snapshots that share the host OS kernel. This fundamental difference drives their respective strengths and use cases. Packer images provide complete isolation at the VM level, while Docker containers share the host kernel through namespaces and cgroups, making them lighter but less isolated.
| Dimension | Packer (Machine Images) | Docker (Containers) |
|---|---|---|
| Artifact Type | Full VM image (AMI, VHD, QCOW2) | Container image (OCI/Docker) |
| OS Kernel | Includes its own kernel | Shares host kernel |
| Isolation | Full VM-level isolation | Process-level (namespace/cgroup) |
| Size | Gigabytes (1-50 GB) | Megabytes (10-500 MB) |
| Boot Time | Seconds to minutes (cold boot) | Milliseconds |
| Best For | Legacy apps, compliance, stateful workloads | Microservices, stateless apps, CI/CD |
| Hardware Access | Direct GPU, custom drivers | Limited GPU passthrough |
| Networking | Full network stack | Container networking (CNI) |
| Statefulness | Natural fit for stateful workloads | Best for stateless workloads |
| Compliance | Easier to meet VM-level compliance | Requires container-specific controls |
When to Choose Packer
- Legacy Applications: Applications that require specific OS configurations, kernel modules, or system-level dependencies that do not work in containers. Many enterprise applications were designed for VM environments and would require significant rearchitecting to run in containers.
- Compliance Requirements: Regulated industries (healthcare, finance, government) that require FIPS 140-2 compliance, CIS-hardened OS baselines, or VM-level audit trails that are easier to implement and verify at the VM boundary.
- GPU/Hardware Workloads: Machine learning training, video processing, or scientific computing that requires direct GPU access or specialized hardware drivers that are difficult to passthrough to containers.
- Stateful Applications: Databases, message queues, and file systems that need persistent local storage, specific I/O configurations, and predictable performance characteristics.
- Multi-Tenant Isolation: Scenarios requiring strong isolation between tenants that container namespaces cannot provide, particularly in environments where container escape vulnerabilities are a concern.
When to Choose Docker
- Microservices: Applications decomposed into small, independent services that benefit from lightweight, fast-starting containers that can be independently scaled and updated.
- Stateless Applications: Web servers, API backends, worker processes that do not need persistent local state and can scale horizontally across many container instances.
- Rapid Scaling: Auto-scaling scenarios where containers can start in milliseconds versus minutes for VMs, enabling more responsive scaling to traffic spikes.
- Development Efficiency: Teams that benefit from consistent development environments across laptops, CI/CD, and production, with the ability to quickly spin up and tear down complex multi-service environments.
- Resource Efficiency: Running many lightweight workloads on shared infrastructure without the overhead of multiple VMs, reducing overall infrastructure costs.
Using Both Together
The most common enterprise pattern uses Packer to build the base infrastructure layer (hardened OS images, compute-optimized instances with drivers) and Docker to package application-level workloads. For example, Packer builds the golden AMI that includes a hardened Ubuntu OS, Docker runtime, NVIDIA drivers, and monitoring agents. Then Docker builds the application container that runs on top of this base infrastructure. This layered approach combines the security and compliance benefits of Packer images with the development agility and deployment flexibility of containers. The Packer-built AMI serves as the Kubernetes node image or ECS container instance, while Docker containers run the actual application workloads on these well-configured nodes.
16. Comparison with Image Factory, Baking, Containerization
Packer exists within a broader ecosystem of image and artifact management approaches. Understanding how Packer compares to and integratesates with other patterns — Image Factories, traditional baking approaches, and containerization platforms — helps architects make informed decisions about which tool or combination of tools best fits their requirements. This section provides a comprehensive comparison that positions Packer within the broader infrastructure automation landscape.
Image Factory Pattern
An Image Factory is an organizational pattern that treats machine image creation as a production process with defined inputs, transformation steps, quality gates, and outputs. Packer is the core build tool within an Image Factory, but the factory also encompasses source management, testing, compliance scanning, versioning, distribution, and lifecycle management. Think of Packer as the oven in a bakery — essential but only one part of the complete production line. The Image Factory pattern is the recommended approach for organizations producing more than a handful of image variants, as it provides the structure needed to manage complexity at scale.
Baking Approaches: Full Bake vs. Hybrid Bake
The "baking" metaphor refers to the degree to which software is pre-installed in the image. A full bake installs everything — OS, runtime, dependencies, application code — into the image. Instances boot and immediately serve traffic with no additional configuration. A hybrid bake installs only the OS and base dependencies, leaving application code to be deployed at runtime via configuration management or deployment pipelines. The choice between full and hybrid baking depends on deployment frequency, rollback requirements, and the team's operational maturity.
| Approach | Description | Boot Time | Flexibility | Complexity | Best For |
|---|---|---|---|---|---|
| Full Bake | Everything in the image, instant boot | Seconds | Low | Medium | Immutable deployments, auto-scaling |
| Hybrid Bake | Base in image, app deployed at boot | Minutes | High | Medium | Frequent app releases, shared base images |
| No Bake | Minimal image, everything at runtime | Minutes+ | Highest | High | Configuration management-first shops |
Containerization Platforms
Platforms like Kubernetes, ECS, and Cloud Run have shifted many workloads from VMs to containers. However, even in container-centric environments, Packer remains valuable for building the underlying node images — the EKS-optimized AMIs, the ECS container instances, and the Kubernetes worker node images that form the foundation of container orchestration platforms. The relationship between Packer and container platforms is complementary rather than competitive. Packer ensures that the infrastructure layer (the nodes running containers) is hardened, consistent, and compliant, while container platforms manage the application layer on top of this reliable foundation.
Tool Comparison Matrix
This comprehensive comparison positions Packer against alternative tools and approaches across multiple dimensions, helping architects evaluate trade-offs for their specific context. No single tool is optimal for all scenarios; the best choice depends on team expertise, organizational requirements, and the specific workload characteristics.
| Dimension | Packer | Docker | AWS Image Builder | Foreman |
|---|---|---|---|---|
| Multi-Cloud | Yes (10+ providers) | Yes (container standard) | No (AWS only) | Yes (with plugins) |
| VM Images | Yes (primary purpose) | No | Yes (EC2 only) | Yes |
| Container Images | Yes (Docker builder) | Yes (primary purpose) | No | No |
| Provisioners | Shell, Ansible, Chef, Puppet | Dockerfile only | SSM, Cloud-init | Puppet, Kickstart |
| IaC Integration | Excellent (HCL2, Terraform) | Good (Docker Compose) | Good (CloudFormation) | Good (Foreman API) |
| Testing | Packer Test, InSpec | Docker Test, hadolint | Limited | Test Runner |
| Community | Large, active | Very large | AWS-managed | Moderate |
| Learning Curve | Moderate | Moderate | Low-Moderate | High |
Decision Framework
When evaluating image building tools, consider these key questions: Do you need to support multiple cloud providers or only one? Do you need VM-level images or container images? What configuration management tools does your team already use? What compliance frameworks must you meet? What is your existing CI/CD infrastructure? Packer excels when you need multi-cloud VM images, have existing configuration management investments, or need to meet strict compliance requirements that are easier to enforce at the VM level. Docker excels for application packaging and deployment in container orchestration environments. The combination of both provides the most comprehensive approach to infrastructure and application image management.
17. Interview Questions and Answers
The following questions cover advanced Packer and image automation topics frequently asked in senior-level infrastructure and platform engineering interviews. Each answer provides architectural context beyond simple factual responses, reflecting the depth expected at the senior+ level. These questions test both theoretical understanding and practical experience with image automation at scale.
Q1: How does Packer achieve idempotent builds, and why is this important for immutable infrastructure?
Answer: Packer achieves idempotency by always starting from a known source image (AMI, base image) and applying the same sequence of provisioner steps. Since each build starts from the same source, the resulting image is deterministic regardless of when or where the build runs. This is critical for immutable infrastructure because instances launched from the same image must be functionally identical. If builds were not idempotent, running the same configuration on different days could produce different images, breaking the guarantee that replacing an instance with a new one from the same image is a safe, predictable operation. To maintain idempotency, provisioner scripts must handle already-installed packages gracefully, use consistent version pinning, avoid time-dependent configurations, and use configuration management tools that are designed for idempotent execution like Ansible or Chef.
Q2: Explain the difference between a Source and a Build in HCL2. When would you use the separation?
Answer: In HCL2, a Source defines a builder's configuration independently — which platform, base image, instance type, networking, and tags. A Build references one or more Sources and adds provisioners and post-processors on top. This separation is valuable when you want to create multiple image variants from the same base configuration without duplicating builder settings. For example, you might define one Ubuntu source and create three builds from it: a web server build, an API server build, and a worker build. Each build applies different provisioning steps to the same base, producing functionally different images from a single source definition. This pattern reduces configuration duplication, ensures consistency across variants, and makes it easy to add new variants by creating a new build block that references the existing source.
Q3: How would you design a Packer pipeline that builds images for AWS, Azure, and GCP simultaneously?
Answer: The design would define three sources — one for each cloud provider — using their respective builder plugins (amazon-ebs, azure-arm, googlecompute). All three sources would be referenced in a single build block with shared provisioners. Packer builds all sources in parallel by default, so the total build time is approximately equal to the slowest individual build rather than the sum of all builds. The provisioners would use platform-agnostic scripts where possible (shell scripts work on all three platforms for Linux images). Platform-specific provisioning would use conditional provisioner blocks with the only or except keywords. A manifest post-processor would collect all artifact IDs into a single JSON file. The CI/CD pipeline would use the cloud provider's native credential management (OIDC for AWS, service principal for Azure, workload identity for GCP) and run InSpec tests against each built image before publishing.
Q4: What are the security risks of using Shell provisioners, and how do you mitigate them?
Answer: Shell provisioners carry several security risks: secrets passed as environment variables may appear in process listings, inline scripts may contain hardcoded credentials, temporary files created during provisioning may persist in the image, and shell history may contain sensitive commands. Mitigations include: using Vault for dynamic secret injection rather than static environment variables, always cleaning up temporary files and shell history in the final provisioner step, using execute_command with sudo to avoid storing passwords in scripts, running cleanup scripts that zero out free space and remove SSH keys, and scanning the final image for secrets using tools like truffleHog or git-secrets before publishing. Additionally, the final provisioner should always perform comprehensive cleanup to remove all build-time artifacts.
Q5: How do you handle Packer builds that take too long for your CI/CD pipeline timeout?
Answer: Several optimization strategies can reduce build times. First, use the fastest available instance types for build VMs — compute-optimized instances like c6i or c7g provision faster than general-purpose instances. Second, parallelize builds across platforms using Packer's default parallel execution and the -parallel-builds flag. Third, optimize provisioner scripts by combining RUN commands in Ansible, caching package downloads, and using local mirrors or pre-downloaded packages. Fourth, reduce the number of provisioner steps by combining related operations into single scripts. Fifth, use Packer's only and except flags to build only specific platforms during development, reserving full multi-platform builds for scheduled or merge-triggered CI/CD jobs. Sixth, for very large images, consider splitting the pipeline into parallel jobs — one per platform — rather than running a single sequential build.
Q6: Describe the Packer test pyramid and explain why integration tests alone are insufficient.
Answer: The image test pyramid has four levels: syntax tests (seconds, zero cost), security tests (minutes, low cost), functional tests (minutes, medium cost), and integration tests (30-60 minutes, high cost). Integration tests alone are insufficient because they are slow, expensive, and catch problems late in the pipeline. A syntax error in an HCL2 file should be caught in seconds by packer validate, not after a 30-minute build fails. Security misconfigurations like weak SSH settings should be caught by InSpec tests within minutes, not discovered after a full integration test. The pyramid ensures fast feedback for common issues at the cheapest level, while integration tests validate end-to-end behavior for the most complex scenarios. Running only integration tests would mean that trivial errors take 30+ minutes to surface, dramatically slowing development velocity and increasing CI/CD costs.
Q7: How would you implement image rollback if a newly built image is found to have a critical vulnerability after deployment?
Answer: Image rollback requires three capabilities: versioned images, version-aware infrastructure, and automated replacement. First, maintain versioned images in your registry (AMI versions, Azure gallery versions) so that previous versions are always available. Second, Terraform configurations should reference images by version rather than "latest," enabling pinning to specific versions. Third, implement Auto Scaling Groups or instance refresh mechanisms that can replace running instances with ones launched from a different AMI. The rollback process would: (1) identify all instances running the affected image using tags or the manifest, (2) update the Terraform configuration to reference the previous known-good image version, (3) trigger an instance refresh or rolling replacement, (4) verify that all new instances are running the correct image version, and (5) investigate and remediate the vulnerability before rebuilding a new image. This entire process should be automated and tested regularly.
Q8: What is the role of Packer in a GitOps workflow for infrastructure management?
Answer: In a GitOps workflow, Packer images are built from configuration stored in Git, and the resulting image versions are recorded back to Git (in manifests, variable files, or SSM parameters). The workflow proceeds as follows: a developer modifies Packer configuration or provisioning scripts in a feature branch, opens a PR, and the CI/CD pipeline validates the configuration, builds the image, runs tests, and publishes the image. The manifest or image version is then committed back to Git (or stored in a GitOps-managed parameter store). Terraform configurations in a separate repository reference these image versions, and ArgoCD or Flux detects the change and reconciles the infrastructure. This creates a fully auditable, Git-native flow from code change to image build to infrastructure deployment, with every step traceable through Git history.
Q9: Explain how you would implement multi-account AMI sharing with proper access controls.
Answer: Multi-account AMI sharing involves several components: (1) Build AMIs in a dedicated image-building account with restricted IAM policies. (2) Use the ami_users option in the Amazon EBS builder to grant launch permissions to target accounts at build time. (3) For automated sharing, use a Lambda function or Step Functions workflow triggered by the manifest post-processor that reads the AMI ID and calls ec2:ModifyImageAttribute to share with additional accounts. (4) Implement cross-account IAM roles for Terraform to discover and use shared AMIs in target accounts. (5) Use AWS Organizations and Service Control Policies (SCPs) to restrict AMI usage to only approved images from the image-building account. (6) Tag all AMIs with metadata about who built them, when, and why, enabling auditors to verify that only approved images are in use.
Q10: How do you handle secret rotation in Packer builds that use Vault for credential management?
Answer: Vault handles secret rotation transparently through its dynamic secrets engine. When Packer requests credentials from Vault's AWS Secrets Engine, Vault generates short-lived STS credentials with a configurable TTL (typically 15-60 minutes). After the build completes, the credentials expire automatically. Vault can rotate the underlying IAM access keys on a schedule without affecting Packer builds, because each build requests fresh credentials rather than using stored ones. For static secrets stored in Vault KV, rotation involves updating the secret in Vault and triggering a rebuild of affected images. The Packer build does not need to change — it reads the current value from Vault at build time. This architecture eliminates the operational burden of manually updating credentials in multiple places when rotation is needed.