Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

Packer

HashiCorp's tool for automating the creation of machine images across multiple platforms from a single source configuration.

Packer

HashiCorp's tool for automating the creation of machine images across multiple platforms from a single source configuration.

Overview

Packer automates the creation of identical machine images for multiple platforms (AWS AMI, GCP Image, Azure Image, Docker, VMware, etc.) from a single declarative configuration. It provisions images with your applications and configurations pre-installed, enabling immutable infrastructure patterns and faster deployment times. Packer integrates seamlessly with configuration management tools like Ansible, Chef, and Puppet, as well as infrastructure provisioning tools like Terraform.

Template Filepacker initpacker validatepacker buildCreate BuildersRun ProvisionersExecutePost-ProcessorsMachine ImagesAWS AMIGCP ImageAzure ImageTemplate Filepacker initpacker validatepacker buildCreate BuildersRun ProvisionersExecutePost-ProcessorsMachine ImagesAWS AMIGCP ImageAzure Image

Template Structure

Packer uses HCL2 (HashiCorp Configuration Language) or JSON for template definitions. Modern Packer templates use HCL2 for better readability and features.

Key Concepts

  • Sources - Define where and how to create base images (formerly "builders")
  • Build - Orchestrates the image creation process
  • Provisioners - Install and configure software on the image
  • Post-processors - Process images after creation (compress, upload, etc.)
  • Variables - Parameterise templates for reusability
  • Locals - Define local variables for computed values

Basic Template Structure

# Packer configuration block
packer {
  required_plugins {
    amazon = {
      version = ">= 1.0.0"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

# Variable definitions
variable "ami_prefix" {
  type    = string
  default = "my-app"
}

variable "region" {
  type    = string
  default = "eu-west-1"
}

# Local variables
locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
  ami_name  = "${var.ami_prefix}-${local.timestamp}"
}

# Source configuration
source "amazon-ebs" "example" {
  ami_name      = local.ami_name
  instance_type = "t3.micro"
  region        = var.region
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"

  tags = {
    Name        = local.ami_name
    Environment = "production"
    ManagedBy   = "Packer"
  }
}

# Build configuration
build {
  sources = ["source.amazon-ebs.example"]

  provisioner "shell" {
    inline = [
      "echo 'Updating system packages'",
      "sudo apt-get update",
      "sudo apt-get upgrade -y"
    ]
  }

  provisioner "file" {
    source      = "app.conf"
    destination = "/tmp/app.conf"
  }

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

Examples

# Multi-source template (build for multiple platforms)
source "amazon-ebs" "aws" {
  ami_name      = "app-aws-${local.timestamp}"
  instance_type = "t3.micro"
  region        = "eu-west-1"
  source_ami    = "ami-0c55b159cbfafe1f0"
  ssh_username  = "ubuntu"
}

source "googlecompute" "gcp" {
  project_id   = "my-project"
  source_image = "ubuntu-2204-lts"
  zone         = "europe-west2-a"
  image_name   = "app-gcp-${local.timestamp}"
  ssh_username = "ubuntu"
}

build {
  sources = [
    "source.amazon-ebs.aws",
    "source.googlecompute.gcp"
  ]

  provisioner "shell" {
    script = "install.sh"
  }
}

Building Machine Images

AWS AMI

source "amazon-ebs" "web_server" {
  # Authentication (can also use AWS CLI profiles or instance roles)
  region     = "eu-west-1"
  access_key = var.aws_access_key  # Better to use env vars or IAM roles
  secret_key = var.aws_secret_key

  # Source AMI selection
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]  # Canonical
  }

  # Instance configuration
  instance_type = "t3.micro"
  ssh_username  = "ubuntu"

  # AMI configuration
  ami_name        = "web-server-${local.timestamp}"
  ami_description = "Web server image with Nginx"

  # EBS volume configuration
  launch_block_device_mappings {
    device_name = "/dev/sda1"
    volume_size = 20
    volume_type = "gp3"
    iops        = 3000
    throughput  = 125
    delete_on_termination = true
  }

  # Networking
  subnet_id              = "subnet-12345678"
  security_group_ids     = ["sg-12345678"]
  associate_public_ip_address = true

  # AMI distribution
  ami_regions = ["eu-west-1", "eu-west-2", "us-east-1"]

  # Snapshot tags
  snapshot_tags = {
    Name      = "web-server-snapshot"
    ManagedBy = "Packer"
  }

  tags = {
    Name        = "web-server"
    Environment = "production"
    OS          = "Ubuntu 22.04"
    ManagedBy   = "Packer"
  }
}

# Alternative: Using existing AMI ID
source "amazon-ebs" "from_ami_id" {
  region        = "eu-west-1"
  source_ami    = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  ssh_username  = "ubuntu"
  ami_name      = "my-ami-${local.timestamp}"
}

GCP Image

source "googlecompute" "app_server" {
  # Authentication
  project_id          = "my-gcp-project"
  account_file        = "service-account.json"  # Or use gcloud auth

  # Source image
  source_image_family = "ubuntu-2204-lts"
  # Or specific image: source_image = "ubuntu-2204-jammy-v20230302"

  # Instance configuration
  zone          = "europe-west2-a"
  machine_type  = "e2-micro"
  ssh_username  = "ubuntu"

  # Image configuration
  image_name        = "app-server-${local.timestamp}"
  image_description = "Application server image"
  image_family      = "app-server"

  # Disk configuration
  disk_size    = 20
  disk_type    = "pd-standard"

  # Networking
  network    = "default"
  subnetwork = "default"

  # Image storage
  image_storage_locations = ["eu"]

  # Labels (GCP's tags)
  labels = {
    environment = "production"
    managed_by  = "packer"
    application = "app-server"
  }

  # Use preemptible instance to reduce costs
  preemptible = true
}

Azure Image

source "azure-arm" "app_image" {
  # Authentication
  subscription_id = var.azure_subscription_id
  client_id       = var.azure_client_id
  client_secret   = var.azure_client_secret
  tenant_id       = var.azure_tenant_id

  # Or use Azure CLI authentication
  use_azure_cli_auth = true

  # Resource configuration
  managed_image_resource_group_name = "packer-images-rg"
  managed_image_name                = "app-server-${local.timestamp}"

  # Location
  location = "UK South"

  # Source image
  image_publisher = "Canonical"
  image_offer     = "0001-com-ubuntu-server-jammy"
  image_sku       = "22_04-lts"
  image_version   = "latest"

  # VM configuration
  vm_size = "Standard_B2s"

  # OS disk
  os_type         = "Linux"
  os_disk_size_gb = 30

  # Build resource group (temporary)
  build_resource_group_name = "packer-build-rg"

  # Azure Image Gallery (Shared Image Gallery)
  shared_image_gallery_destination {
    subscription        = var.azure_subscription_id
    resource_group      = "shared-images-rg"
    gallery_name        = "MyImageGallery"
    image_name          = "AppServer"
    image_version       = "1.0.${local.timestamp}"
    replication_regions = ["UK South", "UK West"]
  }

  # Tags
  azure_tags = {
    Environment = "Production"
    ManagedBy   = "Packer"
    Application = "AppServer"
  }
}

Variables and User Variables

Variable Types

# String variable
variable "region" {
  type        = string
  description = "AWS region for AMI"
  default     = "eu-west-1"
}

# Number variable
variable "volume_size" {
  type    = number
  default = 20
}

# Boolean variable
variable "enable_monitoring" {
  type    = bool
  default = true
}

# List variable
variable "ami_regions" {
  type    = list(string)
  default = ["eu-west-1", "eu-west-2"]
}

# Map variable
variable "tags" {
  type = map(string)
  default = {
    Environment = "dev"
    Team        = "platform"
  }
}

# Object variable
variable "instance_config" {
  type = object({
    type = string
    size = number
  })
  default = {
    type = "t3.micro"
    size = 20
  }
}

# Variable with validation
variable "environment" {
  type        = string
  description = "Environment name"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

# Sensitive variable (won't be logged)
variable "api_key" {
  type      = string
  sensitive = true
}

Variable Input Methods

# 1. Default values in template (shown above)

# 2. Variable files (.pkrvars.hcl)
# variables.pkrvars.hcl
region      = "us-east-1"
environment = "production"
tags = {
  Team    = "DevOps"
  Project = "WebApp"
}

# Use with: packer build -var-file="variables.pkrvars.hcl" template.pkr.hcl

# 3. Auto-loaded variable files
# Files named *.auto.pkrvars.hcl are automatically loaded

# 4. Command-line variables
# packer build -var="region=eu-west-2" -var="environment=staging" template.pkr.hcl

# 5. Environment variables
# export PKR_VAR_region="eu-west-1"
# export PKR_VAR_environment="production"

Local Variables

locals {
  # Timestamp for unique names
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")

  # Computed values
  ami_name = "${var.app_name}-${var.environment}-${local.timestamp}"

  # Conditional logic
  instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"

  # Merged maps
  default_tags = {
    ManagedBy = "Packer"
    CreatedAt = local.timestamp
  }

  all_tags = merge(local.default_tags, var.custom_tags)

  # List operations
  all_regions = concat(["eu-west-1"], var.additional_regions)
}

Provisioners

Provisioners install and configure software on the machine image after the initial instance is created.

shellfileansibleInstance CreatedProvisioner TypeExecute ShellScriptsUpload FilesRun AnsiblePlaybooksConfigure SoftwareCreate ImageshellfileansibleInstance CreatedProvisioner TypeExecute ShellScriptsUpload FilesRun AnsiblePlaybooksConfigure SoftwareCreate Image

Shell Provisioner

# Inline shell commands
provisioner "shell" {
  inline = [
    "echo 'Updating system'",
    "sudo apt-get update",
    "sudo apt-get upgrade -y",
    "sudo apt-get install -y nginx",
  ]
}

# Execute script file
provisioner "shell" {
  script = "scripts/install.sh"
}

# Execute multiple scripts
provisioner "shell" {
  scripts = [
    "scripts/update-system.sh",
    "scripts/install-dependencies.sh",
    "scripts/configure-app.sh"
  ]
}

# With environment variables
provisioner "shell" {
  environment_vars = [
    "APP_VERSION=${var.app_version}",
    "ENVIRONMENT=${var.environment}",
    "DB_HOST=${var.db_host}"
  ]
  script = "scripts/install-app.sh"
}

# Execute as root (use sparingly)
provisioner "shell" {
  execute_command = "sudo sh -c '{{ .Vars }} {{ .Path }}'"
  script          = "scripts/root-install.sh"
}

# With custom interpreter
provisioner "shell" {
  inline = [
    "Write-Host 'Configuring Windows server'",
    "Install-WindowsFeature -Name Web-Server"
  ]
  inline_shebang = "/usr/bin/pwsh -Command"  # PowerShell
}

# Expect reboot
provisioner "shell" {
  inline             = ["sudo reboot"]
  expect_disconnect  = true
}

# Pause before executing (wait for cloud-init, etc.)
provisioner "shell" {
  pause_before = "30s"
  inline       = ["echo 'Starting configuration'"]
}

# Run only on specific sources
provisioner "shell" {
  only   = ["source.amazon-ebs.ubuntu"]
  script = "scripts/aws-specific.sh"
}

# Skip on specific sources
provisioner "shell" {
  except = ["source.googlecompute.debian"]
  script = "scripts/install.sh"
}

File Provisioner

# Upload single file
provisioner "file" {
  source      = "configs/app.conf"
  destination = "/tmp/app.conf"
}

# Upload directory
provisioner "file" {
  source      = "configs/"
  destination = "/tmp/"
}

# Upload and then move to final location
provisioner "file" {
  source      = "configs/nginx.conf"
  destination = "/tmp/nginx.conf"
}

provisioner "shell" {
  inline = [
    "sudo mv /tmp/nginx.conf /etc/nginx/nginx.conf",
    "sudo chown root:root /etc/nginx/nginx.conf",
    "sudo chmod 644 /etc/nginx/nginx.conf"
  ]
}

# Upload with specific permissions (Unix-like systems)
provisioner "file" {
  source      = "scripts/startup.sh"
  destination = "/tmp/startup.sh"
}

provisioner "shell" {
  inline = [
    "sudo mv /tmp/startup.sh /usr/local/bin/startup.sh",
    "sudo chmod +x /usr/local/bin/startup.sh"
  ]
}

# Generate content inline
provisioner "shell" {
  inline = [
    "cat > /tmp/config.json <<'EOF'",
    jsonencode({
      version     = var.app_version
      environment = var.environment
    }),
    "EOF"
  ]
}

Ansible Provisioner

# Basic Ansible playbook
provisioner "ansible" {
  playbook_file = "ansible/playbook.yml"
}

# With inventory file
provisioner "ansible" {
  playbook_file   = "ansible/playbook.yml"
  inventory_file  = "ansible/inventory"
}

# With extra variables
provisioner "ansible" {
  playbook_file = "ansible/playbook.yml"
  extra_arguments = [
    "--extra-vars",
    "app_version=${var.app_version} environment=${var.environment}"
  ]
}

# With Ansible roles
provisioner "ansible" {
  playbook_file = "ansible/playbook.yml"
  roles_path    = "ansible/roles"

  # Ansible configuration
  ansible_env_vars = [
    "ANSIBLE_HOST_KEY_CHECKING=False",
    "ANSIBLE_SSH_ARGS='-o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s'"
  ]

  extra_arguments = [
    "--vault-password-file", "~/.vault_pass",
    "--tags", "install,configure"
  ]
}

# Use Ansible Local (runs on the image itself)
provisioner "ansible-local" {
  playbook_file   = "playbook.yml"
  playbook_dir    = "ansible"
  role_paths      = ["ansible/roles/app", "ansible/roles/common"]

  # Install Ansible first if not present
  install_command = "sudo apt-get update && sudo apt-get install -y ansible"
}

# Galaxy roles
provisioner "ansible-local" {
  playbook_file = "playbook.yml"
  galaxy_file   = "requirements.yml"

  # This will run: ansible-galaxy install -r requirements.yml
}

Other Common Provisioners

# Chef
provisioner "chef-solo" {
  cookbook_paths = ["cookbooks"]
  run_list       = ["recipe[base]", "recipe[app]"]
}

# Puppet
provisioner "puppet-masterless" {
  manifest_file = "manifests/default.pp"
  module_paths  = ["modules"]
}

# PowerShell (Windows)
provisioner "powershell" {
  inline = [
    "Install-WindowsFeature -Name Web-Server",
    "New-Item -Path C:\\inetpub\\wwwroot\\app -ItemType Directory"
  ]
}

# Windows-Restart
provisioner "windows-restart" {
  restart_timeout = "15m"
}

# Breakpoint (for debugging)
provisioner "breakpoint" {
  disable = false
  note    = "Debug point - check configuration before proceeding"
}

Post-Processors

Post-processors run after the image is created to perform additional operations.

# Generate manifest file
post-processor "manifest" {
  output     = "manifest.json"
  strip_path = true
  custom_data = {
    build_time  = local.timestamp
    version     = var.app_version
    environment = var.environment
  }
}

# Compress image (for formats that support it)
post-processor "compress" {
  output = "{{.BuildName}}.tar.gz"
  compression_level = 6
}

# Docker import
post-processor "docker-import" {
  repository = "myorg/myapp"
  tag        = "${var.app_version}"
}

# Docker push
post-processor "docker-tag" {
  repository = "myorg/myapp"
  tags       = ["${var.app_version}", "latest"]
}

post-processor "docker-push" {
  login          = true
  login_username = var.docker_username
  login_password = var.docker_password
}

# Checksum
post-processor "checksum" {
  checksum_types = ["sha256", "md5"]
  output         = "{{.BuildName}}_{{.ChecksumType}}.checksum"
}

# Shell command post-processor
post-processor "shell-local" {
  inline = [
    "echo 'Image created: ${local.ami_name}'",
    "echo 'Notifying deployment system...'",
    "curl -X POST https://deploy.example.com/webhook -d '{\"image\": \"${local.ami_name}\"}'"
  ]
}

# Vagrant box
post-processor "vagrant" {
  output = "builds/{{.Provider}}/{{.BuildName}}.box"
}

# Post-processor sequence (chained)
post-processors {
  post-processor "compress" {
    output = "output.tar.gz"
  }

  post-processor "checksum" {
    checksum_types = ["sha256"]
  }

  post-processor "shell-local" {
    inline = ["echo 'Upload to artifact repository'"]
  }
}

Common Patterns

Multi-Cloud Image Build

# Build identical images across AWS, GCP, and Azure
variable "app_version" {
  type = string
}

locals {
  timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

source "amazon-ebs" "app" {
  ami_name      = "app-${var.app_version}-${local.timestamp}"
  instance_type = "t3.micro"
  region        = "eu-west-1"
  source_ami_filter {
    filters = {
      name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"
}

source "googlecompute" "app" {
  project_id          = "my-project"
  source_image_family = "ubuntu-2204-lts"
  zone                = "europe-west2-a"
  image_name          = "app-${var.app_version}-${local.timestamp}"
  ssh_username        = "ubuntu"
}

source "azure-arm" "app" {
  use_azure_cli_auth                = true
  managed_image_resource_group_name = "images-rg"
  managed_image_name                = "app-${var.app_version}-${local.timestamp}"
  location                          = "UK South"

  image_publisher = "Canonical"
  image_offer     = "0001-com-ubuntu-server-jammy"
  image_sku       = "22_04-lts"

  os_type = "Linux"
  vm_size = "Standard_B2s"
}

build {
  sources = [
    "source.amazon-ebs.app",
    "source.googlecompute.app",
    "source.azure-arm.app"
  ]

  # Common provisioning for all platforms
  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get upgrade -y"
    ]
  }

  provisioner "ansible" {
    playbook_file = "ansible/app.yml"
    extra_arguments = [
      "--extra-vars",
      "app_version=${var.app_version}"
    ]
  }

  post-processor "manifest" {
    output = "manifest-${var.app_version}.json"
  }
}

Golden Image Pipeline

# Base image with security hardening and common tools
source "amazon-ebs" "base" {
  ami_name      = "base-hardened-${local.timestamp}"
  instance_type = "t3.micro"
  region        = "eu-west-1"
  source_ami_filter {
    filters = {
      name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"

  tags = {
    Type      = "Base"
    Hardened  = "true"
    ManagedBy = "Packer"
  }
}

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

  # Security hardening
  provisioner "ansible" {
    playbook_file = "ansible/security-hardening.yml"
  }

  # Install common monitoring tools
  provisioner "shell" {
    scripts = [
      "scripts/install-cloudwatch-agent.sh",
      "scripts/install-datadog-agent.sh"
    ]
    environment_vars = [
      "DD_API_KEY=${var.datadog_api_key}"
    ]
  }

  # CIS benchmark compliance
  provisioner "ansible" {
    playbook_file = "ansible/cis-benchmark.yml"
  }

  # Cleanup
  provisioner "shell" {
    scripts = ["scripts/cleanup.sh"]
  }

  post-processor "manifest" {
    output = "base-image-manifest.json"
  }
}

Application Image from Base

# Build application image from base image
data "amazon-ami" "base" {
  filters = {
    name = "base-hardened-*"
    "tag:Type" = "Base"
  }
  most_recent = true
  owners      = ["self"]
}

source "amazon-ebs" "app" {
  ami_name      = "app-${var.app_version}-${local.timestamp}"
  instance_type = "t3.micro"
  region        = "eu-west-1"
  source_ami    = data.amazon-ami.base.id
  ssh_username  = "ubuntu"

  tags = {
    Type       = "Application"
    Version    = var.app_version
    BaseImage  = data.amazon-ami.base.id
  }
}

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

  # Install application dependencies
  provisioner "shell" {
    script = "scripts/install-app-deps.sh"
  }

  # Deploy application code
  provisioner "file" {
    source      = "builds/app-${var.app_version}.tar.gz"
    destination = "/tmp/app.tar.gz"
  }

  provisioner "shell" {
    inline = [
      "sudo mkdir -p /opt/app",
      "sudo tar -xzf /tmp/app.tar.gz -C /opt/app",
      "sudo chown -R app:app /opt/app"
    ]
  }

  # Configure systemd service
  provisioner "file" {
    source      = "systemd/app.service"
    destination = "/tmp/app.service"
  }

  provisioner "shell" {
    inline = [
      "sudo mv /tmp/app.service /etc/systemd/system/app.service",
      "sudo systemctl daemon-reload",
      "sudo systemctl enable app.service"
    ]
  }

  post-processor "manifest" {
    output = "app-manifest.json"
  }
}

Immutable Infrastructure Pattern

# Complete application baked into image
build {
  sources = ["source.amazon-ebs.app"]

  # Install application stack
  provisioner "ansible-local" {
    playbook_file = "ansible/full-stack.yml"
    role_paths    = [
      "ansible/roles/nginx",
      "ansible/roles/app",
      "ansible/roles/monitoring"
    ]
    extra_arguments = [
      "--extra-vars",
      jsonencode({
        app_version = var.app_version
        config      = var.app_config
      })
    ]
  }

  # Bake configuration
  provisioner "file" {
    content = templatefile("templates/config.tpl", {
      environment = var.environment
      region      = var.region
    })
    destination = "/tmp/config.yml"
  }

  provisioner "shell" {
    inline = [
      "sudo mv /tmp/config.yml /etc/app/config.yml"
    ]
  }

  # Cleanup and optimisation
  provisioner "shell" {
    scripts = [
      "scripts/remove-temp-files.sh",
      "scripts/clear-logs.sh",
      "scripts/optimise-image.sh"
    ]
  }
}

Integration with Terraform

Using Packer Images in Terraform

# Packer manifest post-processor output
# manifest.json:
# {
#   "builds": [{
#     "artifact_id": "eu-west-1:ami-0123456789abcdef0",
#     "custom_data": {
#       "version": "1.2.3"
#     }
#   }]
# }

# Terraform configuration
# Read Packer manifest
locals {
  packer_manifest = jsondecode(file("manifest.json"))
  ami_id          = split(":", local.packer_manifest.builds[0].artifact_id)[1]
}

# Use AMI in Terraform
resource "aws_instance" "app" {
  ami           = local.ami_id
  instance_type = "t3.micro"

  tags = {
    Name    = "app-server"
    Version = local.packer_manifest.builds[0].custom_data.version
  }
}

Data Source Approach

# Terraform: Find latest Packer-built AMI
data "aws_ami" "app" {
  most_recent = true
  owners      = ["self"]

  filter {
    name   = "name"
    values = ["app-*"]
  }

  filter {
    name   = "tag:ManagedBy"
    values = ["Packer"]
  }

  filter {
    name   = "tag:Environment"
    values = [var.environment]
  }
}

resource "aws_launch_template" "app" {
  name_prefix   = "app-"
  image_id      = data.aws_ami.app.id
  instance_type = "t3.micro"
}

resource "aws_autoscaling_group" "app" {
  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  min_size = 2
  max_size = 10

  tag {
    key                 = "ImageId"
    value               = data.aws_ami.app.id
    propagate_at_launch = true
  }
}

Automated Pipeline

# Complete CI/CD pipeline pattern

# 1. Packer template with version tagging
# packer.pkr.hcl
variable "version" {
  type = string
}

source "amazon-ebs" "app" {
  ami_name = "app-${var.version}"
  # ... other config

  tags = {
    Version     = var.version
    Environment = var.environment
    ManagedBy   = "Packer"
    BuildDate   = local.timestamp
  }
}

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

  # ... provisioners

  post-processor "manifest" {
    output = "packer-manifest-${var.version}.json"
    custom_data = {
      version     = var.version
      environment = var.environment
      git_commit  = var.git_commit
    }
  }
}

# 2. Build script
# build.sh
#!/bin/bash
VERSION=${1:-$(git describe --tags --always)}
ENVIRONMENT=${2:-dev}

# Build image with Packer
packer build \
  -var "version=$VERSION" \
  -var "environment=$ENVIRONMENT" \
  -var "git_commit=$(git rev-parse HEAD)" \
  packer.pkr.hcl

# Extract AMI ID from manifest
AMI_ID=$(jq -r '.builds[0].artifact_id' packer-manifest-${VERSION}.json | cut -d: -f2)

# Update Terraform variable file
cat > terraform/ami.auto.tfvars <<EOF
app_ami_id = "$AMI_ID"
app_version = "$VERSION"
EOF

# Deploy with Terraform
cd terraform
terraform init
terraform plan -out=tfplan
terraform apply tfplan

Blue-Green Deployment

# Terraform configuration for blue-green deployment
variable "app_ami_id" {
  description = "AMI ID from Packer build"
  type        = string
}

variable "deployment_colour" {
  description = "blue or green"
  type        = string
  default     = "blue"
}

# Blue environment
resource "aws_launch_template" "blue" {
  name_prefix   = "app-blue-"
  image_id      = var.deployment_colour == "blue" ? var.app_ami_id : data.aws_ami.current_blue.id
  instance_type = "t3.micro"
}

resource "aws_autoscaling_group" "blue" {
  name_prefix = "app-blue-"

  launch_template {
    id      = aws_launch_template.blue.id
    version = "$Latest"
  }

  min_size         = var.deployment_colour == "blue" ? 2 : 0
  max_size         = var.deployment_colour == "blue" ? 10 : 0
  desired_capacity = var.deployment_colour == "blue" ? 2 : 0

  target_group_arns = var.deployment_colour == "blue" ? [aws_lb_target_group.app.arn] : []
}

# Green environment
resource "aws_launch_template" "green" {
  name_prefix   = "app-green-"
  image_id      = var.deployment_colour == "green" ? var.app_ami_id : data.aws_ami.current_green.id
  instance_type = "t3.micro"
}

resource "aws_autoscaling_group" "green" {
  name_prefix = "app-green-"

  launch_template {
    id      = aws_launch_template.green.id
    version = "$Latest"
  }

  min_size         = var.deployment_colour == "green" ? 2 : 0
  max_size         = var.deployment_colour == "green" ? 10 : 0
  desired_capacity = var.deployment_colour == "green" ? 2 : 0

  target_group_arns = var.deployment_colour == "green" ? [aws_lb_target_group.app.arn] : []
}

# Deployment process:
# 1. Build new AMI with Packer
# 2. terraform apply -var="deployment_colour=green" -var="app_ami_id=ami-new"
# 3. Test green environment
# 4. Switch traffic: update deployment_colour to "green"
# 5. Scale down blue: terraform apply again

Quick Reference

Essential Commands

# Initialise template (download plugins)
packer init template.pkr.hcl

# Format template
packer fmt template.pkr.hcl

# Validate template
packer validate template.pkr.hcl

# Validate with variables
packer validate -var-file="prod.pkrvars.hcl" template.pkr.hcl

# Build image
packer build template.pkr.hcl

# Build with variables
packer build -var="region=eu-west-1" -var="version=1.0.0" template.pkr.hcl

# Build with variable file
packer build -var-file="prod.pkrvars.hcl" template.pkr.hcl

# Build specific sources only
packer build -only="amazon-ebs.app" template.pkr.hcl

# Build except specific sources
packer build -except="googlecompute.app" template.pkr.hcl

# Debug mode (verbose logging)
packer build -debug template.pkr.hcl

# Enable logging
PACKER_LOG=1 packer build template.pkr.hcl

# Set log path
PACKER_LOG=1 PACKER_LOG_PATH=packer.log packer build template.pkr.hcl

# Force build (ignore warnings)
packer build -force template.pkr.hcl

# Parallel builds (default: 0 = unlimited)
packer build -parallel-builds=2 template.pkr.hcl

# Show Packer version
packer version

# Inspect template
packer inspect template.pkr.hcl

# Console (test expressions)
packer console template.pkr.hcl

Template Patterns

Pattern Use Case
${var.name} Reference variable
${local.name} Reference local variable
${source.type.name.attribute} Reference source attribute
${build.ID} Build-time variable
${timestamp()} Current timestamp
${uuid()} Generate UUID
${env("VAR")} Environment variable

Common Functions

# String functions
upper("hello")                          # "HELLO"
lower("HELLO")                          # "hello"
title("hello world")                    # "Hello World"
replace("hello", "l", "r")              # "herro"
split(",", "a,b,c")                     # ["a", "b", "c"]
join(",", ["a", "b", "c"])              # "a,b,c"
regex_replace(timestamp(), "[- TZ:]", "") # Clean timestamp

# Collection functions
length(["a", "b", "c"])                 # 3
concat(["a"], ["b"])                    # ["a", "b"]
merge({a=1}, {b=2})                     # {a=1, b=2}

# Encoding functions
jsonencode({key = "value"})             # JSON string
yamlencode({key = "value"})             # YAML string
base64encode("hello")                   # "aGVsbG8="

# Filesystem functions
file("path/to/file")                    # Read file content
templatefile("template.tpl", {var = "value"}) # Render template

# Type conversion
tostring(42)                            # "42"
tonumber("42")                          # 42
tobool("true")                          # true
tolist(["a", "b"])                      # List type
tomap({key = "value"})                  # Map type

Common Issues and Solutions

Build Failures

Issue Solution
source_ami_filter returned no results Update filter criteria or check AMI availability in region
Timeout waiting for SSH Check security group rules, verify ssh_username, increase ssh_timeout
Authentication failed Verify credentials, check IAM permissions, use instance profile
Provisioner failed Add pause_before to wait for cloud-init, check script exit codes
AMI already exists Use unique names with ${local.timestamp}, or enable force_deregister

Authentication Issues

# AWS: Use IAM instance profile (recommended for CI/CD)
source "amazon-ebs" "app" {
  # Remove access_key and secret_key
  # Packer will use instance metadata or AWS CLI config
  region = "eu-west-1"
  # ...
}

# AWS: Use named profile
source "amazon-ebs" "app" {
  profile = "my-profile"
  region  = "eu-west-1"
  # ...
}

# GCP: Use application default credentials
source "googlecompute" "app" {
  # Remove account_file
  # Packer will use gcloud auth application-default login
  project_id = "my-project"
  # ...
}

# Azure: Use Azure CLI
source "azure-arm" "app" {
  use_azure_cli_auth = true
  # ...
}

SSH Connection Timeouts

source "amazon-ebs" "app" {
  # Increase SSH timeout
  ssh_timeout = "10m"

  # Use bastion host
  ssh_bastion_host     = "bastion.example.com"
  ssh_bastion_username = "ec2-user"
  ssh_bastion_private_key_file = "~/.ssh/bastion.pem"

  # Or use AWS Systems Manager Session Manager (no SSH needed)
  communicator = "ssh"
  ssh_interface = "session_manager"

  # Required IAM instance profile for Session Manager
  iam_instance_profile = "PackerInstanceProfile"
}

Provisioner Failures

# Wait for cloud-init to complete
provisioner "shell" {
  pause_before = "30s"
  inline = [
    "cloud-init status --wait",
    "sudo apt-get update"
  ]
}

# Handle errors gracefully
provisioner "shell" {
  inline = [
    "sudo apt-get update || echo 'Update failed, continuing...'",
    "sudo apt-get install -y package || true"
  ]
}

# Use error handling in scripts
provisioner "shell" {
  script = "install.sh"
}

# install.sh
#!/bin/bash
set -e  # Exit on error
set -u  # Exit on undefined variable
set -o pipefail  # Exit on pipe failure

# Your commands here

Debugging

# Enable debug mode (pauses after each step)
packer build -debug template.pkr.hcl

# Enable verbose logging
PACKER_LOG=1 packer build template.pkr.hcl

# Save logs to file
PACKER_LOG=1 PACKER_LOG_PATH=packer.log packer build template.pkr.hcl

# Use breakpoint provisioner
provisioner "breakpoint" {
  disable = false
  note    = "Check application configuration before continuing"
}

Performance Optimisation

# Use faster instance types for building
source "amazon-ebs" "app" {
  instance_type = "c5.2xlarge"  # More CPU for faster builds

  # Use faster EBS volume types
  launch_block_device_mappings {
    device_name = "/dev/sda1"
    volume_type = "gp3"
    iops        = 16000
    throughput  = 1000
  }
}

# Parallel builds
packer build -parallel-builds=4 template.pkr.hcl

# Cache package downloads
provisioner "shell" {
  inline = [
    "export DEBIAN_FRONTEND=noninteractive",
    "sudo apt-get update",
    "sudo apt-get install -y --no-install-recommends packages"
  ]
}

# Minimise image size
provisioner "shell" {
  scripts = ["cleanup.sh"]
}

# cleanup.sh
#!/bin/bash
# Clear package cache
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*

# Clear logs
sudo find /var/log -type f -delete

# Clear temporary files
sudo rm -rf /tmp/*
sudo rm -rf /var/tmp/*

# Clear bash history
history -c
cat /dev/null > ~/.bash_history

Security Best Practices

# Use secrets from environment or secret managers
variable "api_key" {
  type      = string
  sensitive = true
  default   = env("API_KEY")
}

# Use temporary security groups
source "amazon-ebs" "app" {
  temporary_security_group_source_cidrs = ["10.0.0.0/8"]  # Restrict to VPC

  # Or use existing security group
  security_group_ids = ["sg-12345678"]
}

# Don't expose SSH to internet
source "amazon-ebs" "app" {
  associate_public_ip_address = false
  subnet_id                   = "subnet-private"

  # Use Session Manager instead
  communicator  = "ssh"
  ssh_interface = "session_manager"
}

# Clean up sensitive data
provisioner "shell" {
  inline = [
    "sudo shred -u /etc/ssh/*_key /etc/ssh/*_key.pub",
    "sudo find /root/.ssh /home/*/.ssh -type f -exec shred -u {} \\;",
    "sudo rm -rf /var/log/auth.log*"
  ]
}