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.
flowchart LR
A[Template File] --> B[packer init]
B --> C[packer validate]
C --> D[packer build]
D --> E[Create Builders]
E --> F[Run Provisioners]
F --> G[Execute Post-Processors]
G --> H[Machine Images]
H --> I[AWS AMI]
H --> J[GCP Image]
H --> K[Azure 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.
flowchart TD
A[Instance Created] --> B{Provisioner Type}
B -->|shell| C[Execute Shell Scripts]
B -->|file| D[Upload Files]
B -->|ansible| E[Run Ansible Playbooks]
C --> F[Configure Software]
D --> F
E --> F
F --> G[Create 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*"
]
}