Variables and Best Practices
Learn how to use variables effectively and structure Packer projects for production
TLDR: Variables make templates reusable. Define them with types and defaults, override them at runtime or with variable files. Structure projects with clear directories, use version control, and implement proper testing before deploying images.
Variables let you write templates once and use them in multiple contexts - different regions, environments, versions, or cloud providers.
Defining Variables
variable "region" {
type = string
default = "us-east-1"
description = "AWS region to build in"
}
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "app_version" {
type = string
# No default - must be provided
}
variable "enable_monitoring" {
type = bool
default = true
}
variable "tags" {
type = map(string)
default = {
Environment = "production"
ManagedBy = "packer"
}
}
Using Variables
Reference variables with var.name:
source "amazon-ebs" "app" {
region = var.region
instance_type = var.instance_type
ami_name = "myapp-${var.app_version}-{{timestamp}}"
tags = var.tags
}
provisioner "shell" {
environment_vars = [
"APP_VERSION=${var.app_version}",
"MONITORING=${var.enable_monitoring}"
]
script = "scripts/install.sh"
}
Providing Variable Values
Command line:
packer build -var="region=us-west-2" -var="app_version=1.2.3" template.pkr.hcl
Environment variables:
export PKR_VAR_region="us-west-2"
export PKR_VAR_app_version="1.2.3"
packer build template.pkr.hcl
Variable files (production.pkrvars.hcl):
region = "us-east-1"
app_version = "2.0.0"
instance_type = "t3.small"
tags = {
Environment = "production"
Project = "myapp"
CostCenter = "engineering"
}
Use with:
packer build -var-file="production.pkrvars.hcl" template.pkr.hcl
Sensitive Variables
Mark variables as sensitive to hide them from output:
variable "api_token" {
type = string
sensitive = true
}
Values are hidden in Packer output:
==> amazon-ebs.app: Setting API token to <sensitive>
Variable Validation
Add validation rules:
variable "region" {
type = string
validation {
condition = contains(["us-east-1", "us-west-2", "eu-west-1"], var.region)
error_message = "Region must be us-east-1, us-west-2, or eu-west-1."
}
}
variable "app_version" {
type = string
validation {
condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.app_version))
error_message = "Version must be in semantic versioning format (e.g., 1.2.3)."
}
}
Local Variables
Compute values from other variables:
locals {
timestamp = formatdate("YYYY-MM-DD-hhmm", timestamp())
ami_name = "${var.app_name}-${var.app_version}-${local.timestamp}"
common_tags = merge(
var.tags,
{
BuildDate = local.timestamp
GitCommit = var.git_commit
}
)
}
source "amazon-ebs" "app" {
ami_name = local.ami_name
tags = local.common_tags
}
Project Structure
Organize Packer projects for maintainability:
packer-templates/
├── builds/
│ ├── web-server/
│ │ ├── template.pkr.hcl
│ │ ├── variables.pkr.hcl
│ │ ├── production.pkrvars.hcl
│ │ ├── staging.pkrvars.hcl
│ │ └── README.md
│ └── database/
│ ├── template.pkr.hcl
│ └── variables.pkr.hcl
├── scripts/
│ ├── install-nginx.sh
│ ├── configure-monitoring.sh
│ └── cleanup.sh
├── files/
│ ├── nginx.conf
│ ├── app.service
│ └── motd
├── ansible/
│ ├── playbook.yml
│ └── roles/
├── Makefile
└── README.md
Makefile for Common Tasks
.PHONY: init validate fmt build-staging build-prod
init:
packer init builds/
validate:
packer validate builds/
fmt:
packer fmt -recursive .
build-staging:
packer build \
-var-file="builds/web-server/staging.pkrvars.hcl" \
builds/web-server/
build-prod:
packer build \
-var-file="builds/web-server/production.pkrvars.hcl" \
builds/web-server/
test:
packer build -only="docker.*" builds/web-server/
Version Control
.gitignore:
# Packer cache
packer_cache/
*.box
*.ova
*.tar.gz
# Build artifacts
manifest.json
output-*/
# Credentials
*.pem
*.key
credentials.json
*.pkrvars.hcl
!example.pkrvars.hcl
# Terraform
.terraform/
*.tfstate
*.tfstate.backup
Testing Images
Pre-Build Validation
# Validate syntax
packer validate template.pkr.hcl
# Format check
packer fmt -check template.pkr.hcl
Test Build with Docker
Test provisioning locally before cloud builds:
source "docker" "test" {
image = "ubuntu:22.04"
commit = true
}
source "amazon-ebs" "prod" {
# ... AWS configuration
}
build {
sources = [
"source.docker.test",
"source.amazon-ebs.prod"
]
# Same provisioners for both
provisioner "shell" {
script = "install.sh"
}
}
Test with Docker only:
packer build -only="docker.test" template.pkr.hcl
Post-Build Testing
Launch an instance and run tests:
#!/bin/bash
# test-image.sh
AMI_ID=$(jq -r '.builds[0].artifact_id' manifest.json | cut -d: -f2)
# Launch instance
INSTANCE_ID=$(aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type t3.micro \
--query 'Instances[0].InstanceId' \
--output text)
# Wait for instance
aws ec2 wait instance-running --instance-ids $INSTANCE_ID
# Get IP
IP=$(aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--query 'Reservations[0].Instances[0].PublicIpAddress' \
--output text)
# Run tests
curl -f http://$IP/ || exit 1
ssh ubuntu@$IP 'systemctl is-active nginx' || exit 1
# Cleanup
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
echo "Tests passed!"
CI/CD Integration
GitHub Actions
name: Build AMI
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Packer
uses: hashicorp/setup-packer@main
- name: Initialize Packer
run: packer init builds/
- name: Validate templates
run: packer validate builds/
- name: Build AMI
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
packer build \
-var="app_version=${GITHUB_REF#refs/tags/v}" \
-var-file="production.pkrvars.hcl" \
builds/web-server/
- name: Upload manifest
uses: actions/upload-artifact@v3
with:
name: manifest
path: manifest.json
GitLab CI
variables:
PACKER_VERSION: "1.10.0"
stages:
- validate
- build
validate:
stage: validate
image: hashicorp/packer:${PACKER_VERSION}
script:
- packer init builds/
- packer validate builds/
- packer fmt -check -recursive .
build-ami:
stage: build
image: hashicorp/packer:${PACKER_VERSION}
only:
- tags
script:
- packer build -var="app_version=$CI_COMMIT_TAG" builds/
artifacts:
paths:
- manifest.json
Security Best Practices
Never commit credentials: Use environment variables or secret management.
Use IAM roles: For AWS builds, use IAM instance profiles instead of access keys when possible.
Encrypt images: Enable encryption for AMIs and disk images:
source "amazon-ebs" "encrypted" {
encrypt_boot = true
kms_key_id = var.kms_key_id
}
Scan images: Run security scanners on built images before deployment.
Minimal permissions: Build instances only need permissions they actually use.
Secure build instances: Use private subnets and security groups that restrict access.
Performance Optimization
Use faster instance types: Build time is money. Use larger instances for faster builds.
Parallelize builds: Build multiple platforms simultaneously (default).
Cache dependencies: Download large files once, reuse across builds.
Clean up efficiently: Use find with -delete instead of rm -rf for large directories.
What's Next
You now know how to create production-ready Packer templates with variables, testing, and CI/CD integration. Use these patterns to build reliable, versioned infrastructure images that deploy consistently across your environments.
Found an issue?