Skip to content

Packer — Automated Machine Image Building Guide

DodaTech Updated 2026-06-24 7 min read

In this tutorial, you'll learn about Packer. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Packer is an open-source tool by HashiCorp for creating identical machine images for multiple platforms from a single source template, automating the entire image build process with infrastructure-as-code practices.

What You'll Learn

Why It Matters

Manually creating machine images (golden AMIs, VM templates) is slow, inconsistent, and untraceable. Packer automates the process — you define the source OS, install software, apply configurations, and produce a ready-to-use image. DodaTech uses Packer to build golden AMIs for AWS EC2 instances, baking security agents, monitoring tools, and application dependencies into every image.

Real-World Use

DodaZIP's CI pipeline triggers a Packer build on every release. Packer starts a fresh Ubuntu 24.04 EC2 instance, installs the Durga Antivirus Pro agent, Node.js runtime, application code, monitoring stack (Node Exporter, Filebeat), and security hardening — then creates an AMI. Auto Scaling groups launch instances from this golden AMI, eliminating configuration drift entirely.

flowchart LR
    A[Packer Template] --> B[Source: ubuntu-24-04]
    A --> C[Provisioners]
    A --> D[Post-Processors]
    B --> E[Builder: amazon-ebs]
    C --> F[shell: install packages]
    C --> G[ansible: configure app]
    C --> H[file: copy artifacts]
    D --> I[Create AMI]
    D --> J[Share with accounts]
    D --> K[Cleanup builder]
    I --> L[AWS EC2 Launch]
    style A fill:#02A8EF,color:#fff
â„šī¸ Info

Prerequisites: Terraform or infrastructure-as-code familiarity. Cloud provider account (AWS, GCP, Azure).

Installation

# Install Packer on Ubuntu
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install packer

# Verify
packer --version

# Expected output:
# Packer v1.11.0

# Install via script
curl -fsSL https://releases.hashicorp.com/packer/1.11.0/packer_1.11.0_linux_amd64.zip -o packer.zip
unzip packer.zip
sudo mv packer /usr/local/bin/

HCL2 Template Structure

# dodazip-ami.pkr.hcl
packer {
  required_plugins {
    amazon = {
      version = ">= 1.3.0"
      source  = "github.com/hashicorp/amazon"
    }
    ansible = {
      version = ">= 1.1.0"
      source  = "github.com/hashicorp/ansible"
    }
  }
}

variable "ami_name" {
  type    = string
  default = "dodazip-golden-ami"
}

variable "instance_type" {
  type    = string
  default = "t3.medium"
}

variable "region" {
  type    = string
  default = "us-east-1"
}

locals {
  timestamp = formatdate("YYYYMMDDhhmmss", timestamp())
  ami_name  = "${var.ami_name}-${local.timestamp}"
}

source "amazon-ebs" "golden" {
  ami_name      = local.ami_name
  instance_type = var.instance_type
  region        = var.region

  source_ami_filter {
    filters = {
      name                = "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]
  }

  ssh_username = "ubuntu"

  launch_block_device_mappings {
    device_name           = "/dev/sda1"
    volume_size           = 30
    volume_type           = "gp3"
    delete_on_termination = true
  }

  tags = {
    Name        = local.ami_name
    Environment = "production"
    ManagedBy   = "packer"
    CreatedBy   = "dodatech-platform"
    BuildDate   = local.timestamp
  }

  run_tags = {
    Name        = "packer-builder-${local.timestamp}"
    Environment = "production"
    ManagedBy   = "packer"
  }

  run_volume_tags = {
    Name        = "packer-builder-${local.timestamp}"
    ManagedBy   = "packer"
  }

  snapshot_tags = {
    Name        = local.ami_name
    Environment = "production"
    ManagedBy   = "packer"
  }
}

build {
  name    = "dodazip-golden"
  sources = ["source.amazon-ebs.golden"]

  provisioner "shell" {
    inline = [
      "echo '=== Updating system packages ==='",
      "sudo apt-get update -qq",
      "sudo apt-get upgrade -y -qq",
      "echo '=== Installing system dependencies ==='",
      "sudo apt-get install -y -qq curl wget gnupg ca-certificates lsb-release",
    ]
  }

  provisioner "file" {
    source      = "provisioning/install-monitoring.sh"
    destination = "/tmp/install-monitoring.sh"
  }

  provisioner "shell" {
    script = "provisioning/install-monitoring.sh"
  }

  provisioner "ansible" {
    playbook_file = "provisioning/playbook.yml"
    ansible_env_vars = [
      "ANSIBLE_HOST_KEY_CHECKING=False]
    ]
  }

  provisioner "shell" {
    inline = [
      "echo '=== Cleaning up for smaller AMI ==='",
      "sudo apt-get autoremove -y -qq",
      "sudo apt-get clean -qq",
      "sudo rm -rf /var/log/*.log /tmp/* /var/tmp/*",
      "echo '=== Build complete ==='",
    ]
  }

  post-processor "manifest" {
    output     = "manifest.json"
    strip_path = true
  }
}

Building the Image

# Format and validate the template
packer fmt dodazip-ami.pkr.hcl
packer validate dodazip-ami.pkr.hcl

# Expected output:
# The configuration is valid.

# Build the AMI
packer build dodazip-ami.pkr.hcl

# Expected output:
# amazon-ebs.golden: output will be in this color.
# Build 'amazon-ebs.golden' running.
# amazon-ebs.golden: Creating temporary keypair for this instance...
# amazon-ebs.golden: Creating temporary security group for this instance...
# amazon-ebs.golden: Launching a source AWS instance...
# amazon-ebs.golden: Instance ID: i-0123456789abcdef0
# amazon-ebs.golden: Waiting for SSH to become available...
# amazon-ebs.golden: Connected to SSH!
# amazon-ebs.golden: Provisioning with shell script...
# amazon-ebs.golden: === Updating system packages ===
# amazon-ebs.golden: === Installing system dependencies ===
# amazon-ebs.golden: Provisioning with Ansible...
# amazon-ebs.golden: === Installing Durga Antivirus ===
# amazon-ebs.golden: === Installing Node Exporter ===
# amazon-ebs.golden: === Cleaning up for smaller AMI ===
# amazon-ebs.golden: Stopping instance...
# amazon-ebs.golden: Creating AMI dodazip-golden-20260624120000...
# amazon-ebs.golden: AMI: ami-0a1b2c3d4e5f67890
# amazon-ebs.golden: Cleaning up resources...
# Build 'amazon-ebs.golden' finished.

# ==> Builds finished. The artifacts of successful builds are:
# --> amazon-ebs.golden: AMIs were created:
# us-east-1: ami-0a1b2c3d4e5f67890

GCP Image Builder

# dodazip-gcp.pkr.hcl
packer {
  required_plugins {
    googlecompute = {
      version = ">= 1.1.0"
      source  = "github.com/hashicorp/googlecompute"
    }
  }
}

source "googlecompute" "golden" {
  project_id   = "dodatech-platform"
  source_image = "ubuntu-2404-lts-amd64"
  zone         = "us-central1-a"
  image_name   = "dodazip-golden-${local.timestamp}"
  image_family = "dodazip-golden"
  ssh_username = "ubuntu"
  machine_type = "e2-medium"
  disk_size    = 30
  disk_type    = "pd-ssd"

  tags = ["packer-builder"]
}

build {
  sources = ["source.googlecompute.golden"]

  provisioner "shell" {
    inline = [
      "sudo apt-get update -qq",
      "sudo apt-get install -y -qq nginx nodejs durga-antivirus-pro",
      "sudo systemctl enable nginx",
    ]
  }

  provisioner "file" {
    source      = "configs/"
    destination = "/opt/dodazip/"
  }
}

Variables and Auto-Encoding

# variables.pkr.hcl
variable "aws_access_key" {
  type      = string
  sensitive = true
}

variable "aws_secret_key" {
  type      = string
  sensitive = true
}

variable "source_ami" {
  type    = string
  default = null
}

variable "build_version" {
  type    = string
  default = "latest"
}
# Pass variables
packer build \
  -var 'aws_access_key=AKIA...' \
  -var 'aws_secret_key=...' \
  -var 'build_version=2.5.0' \
  dodazip-ami.pkr.hcl

# Use variable file
cat dodazip.auto.pkrvars.hcl
aws_access_key = "AKIA..."
aws_secret_key = "..."
build_version  = "2.5.0"
region         = "us-west-2"

CI/CD Integration

# .github/workflows/packer-build.yml
name: Build Golden AMI

on:
  push:
    branches: [main]
    paths:
      - 'packer/**'
      - 'provisioning/**'

jobs:
  packer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Packer
        uses: hashicorp/setup-packer@v3
        with:
          version: latest

      - name: Validate template
        run: packer validate packer/dodazip-ami.pkr.hcl

      - name: Build AMI
        run: |
          packer build \
            -var "aws_access_key=${{ secrets.AWS_ACCESS_KEY }}" \
            -var "aws_secret_key=${{ secrets.AWS_SECRET_KEY }}" \
            -var "build_version=${{ github.sha }}" \
            packer/dodazip-ami.pkr.hcl

Common Configuration Mistakes

  1. Not specifying source_ami_filter correctly: Wrong filters produce incompatible base images. Always set owners to the official Canonical (099720109477) or Microsoft publisher IDs.

  2. Forgetting cleanup in provisioners: Packer creates a snapshot of the entire filesystem. Without apt-get clean and log removal, AMIs are unnecessarily large (5-10GB extra).

  3. Using ssh_private_key_file without proper permissions: Packer generates ephemeral SSH keys by default. Using custom keys requires chmod 600 on the key file.

  4. Not tagging builder instances: Without run_tags, AWS shows orphaned instances during builds. Tags help identify and clean up failed builder instances.

  5. Hardcoding secrets in templates: Use variables marked sensitive = true and pass via environment variables or --var flags. Never commit secrets to version control.

Practice Questions

  1. What is a Packer builder? Answer: A builder creates a machine instance for a specific platform (AWS EC2, GCP Compute, Azure VM, Docker, VMware). It launches the source image, provisions it, and creates the output artifact.

  2. How do provisioners differ in Packer? Answer: Provisioners customize the image: shell runs scripts, file uploads files, <a href="/devops/ansible/">ansible</a> runs playbooks, chef runs cookbooks, <a href="/microsoft-technologies/powershell/">PowerShell</a> runs PowerShell scripts.

  3. What is a post-processor? Answer: Post-processors run after the image is created — extracting the manifest, compressing the image, uploading to artifact storage, or triggering downstream pipelines.

  4. How does Packer ensure idempotent builds? Answer: Packer always starts from a clean source image and applies provisioners fresh. There is no incremental state — each build produces identical output from the same source and provisioners.

Challenge

Create a Packer pipeline for a golden machine image: write an HCL2 template for AWS AMI (Ubuntu 24.04), install and configure NGINX, Node.js 22, Prometheus Node Exporter, Filebeat, and Durga Antivirus Pro, apply security hardening (SSH config, firewall rules, fail2ban), create the AMI with proper tags, share it with specified AWS accounts, store the manifest as a CI artifact, and trigger a Terraform workspace update to roll out the new AMI to Auto Scaling groups.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro