Skip to main content

Packer Components

Packer components build machine images (AMIs, VM images, container images) using HashiCorp Packer. They allow you to manage image builds with the same stack-based configuration approach used for Terraform and Helmfile.

Available Configuration Sections

Packer components support the common configuration sections:

vars
Variables passed to packer.
env
Environment variables during execution.
settings
Integrations and metadata.
metadata
Component behavior and inheritance.
command
Override packer binary.
hooks
Lifecycle event handlers.

Component Structure

A typical Packer component configuration:

components:
packer:
ami-ubuntu:
metadata:
component: ami-ubuntu
vars:
region: us-east-1
instance_type: t3.medium
source_ami_filter:
name: "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
owner: "099720109477" # Canonical
ami_name_prefix: "acme-ubuntu"
env:
AWS_PROFILE: acme-prod

Packer Directory Structure

Packer components are located in the path configured in atmos.yaml:

# atmos.yaml
components:
packer:
base_path: components/packer

Example structure:

components/packer/
├── ami-ubuntu/
│ ├── template.pkr.hcl
│ ├── variables.pkr.hcl
│ └── scripts/
│ ├── setup.sh
│ └── cleanup.sh
├── ami-eks-node/
│ ├── template.pkr.hcl
│ └── variables.pkr.hcl
└── docker-base/
└── template.pkr.hcl

Packer Template

Each Packer component contains HCL templates that use variables passed from Atmos:

components/packer/ami-ubuntu/variables.pkr.hcl
variable "region" {
type = string
description = "AWS region to build the AMI"
}

variable "instance_type" {
type = string
default = "t3.medium"
description = "EC2 instance type for the builder"
}

variable "source_ami_filter" {
type = object({
name = string
owner = string
})
description = "Filter for source AMI"
}

variable "ami_name_prefix" {
type = string
description = "Prefix for the AMI name"
}

variable "tags" {
type = map(string)
default = {}
description = "Tags to apply to the AMI"
}
components/packer/ami-ubuntu/template.pkr.hcl
packer {
required_plugins {
amazon = {
version = ">= 1.2.0"
source = "github.com/hashicorp/amazon"
}
}
}

source "amazon-ebs" "ubuntu" {
region = var.region
instance_type = var.instance_type

source_ami_filter {
filters = {
name = var.source_ami_filter.name
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = [var.source_ami_filter.owner]
}

ami_name = "${var.ami_name_prefix}-ubuntu-{{timestamp}}"

tags = merge(var.tags, {
Name = "${var.ami_name_prefix}-ubuntu"
BuildTime = timestamp()
})

ssh_username = "ubuntu"
}

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

provisioner "shell" {
scripts = [
"${path.root}/scripts/setup.sh",
"${path.root}/scripts/cleanup.sh"
]
}
}

Component-Type Defaults

Define defaults for all Packer components:

# Apply to all Packer components
packer:
vars:
region: us-east-1
tags:
ManagedBy: Atmos
Builder: Packer
env:
PACKER_LOG: "1"
AWS_PROFILE: acme-build

# Individual components
components:
packer:
ami-ubuntu:
vars:
instance_type: t3.medium

Complete Example

stacks/orgs/acme/plat/prod/us-east-1.yaml
import:
- catalog/packer/_defaults
- orgs/acme/plat/prod/_defaults

vars:
region: us-east-1
stage: prod

packer:
vars:
tags:
Environment: prod
ManagedBy: Atmos
env:
AWS_PROFILE: acme-prod

components:
packer:
ami-ubuntu-base:
metadata:
component: ami-ubuntu
vars:
instance_type: t3.medium
ami_name_prefix: "acme-prod"
source_ami_filter:
name: "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
owner: "099720109477"

ami-eks-node:
metadata:
component: ami-eks-node
vars:
instance_type: t3.large
ami_name_prefix: "acme-prod-eks"
kubernetes_version: "1.28"
source_ami_filter:
name: "amazon-eks-node-1.28-*"
owner: "602401143452" # Amazon EKS
settings:
depends_on:
- component: ami-ubuntu-base
type: packer

docker-base:
metadata:
component: docker-base
vars:
repository: "acme/base"
tag: "{{ .stage }}-{{ timestamp }}"
base_image: "ubuntu:22.04"

Template Configuration

Atmos supports two ways to specify Packer templates:

Directory Mode (Default)

By default, Atmos passes the component directory to Packer, which automatically loads all *.pkr.hcl files. This is the recommended approach for organizing Packer configurations:

components:
packer:
my-ami:
# No settings.packer.template - uses directory mode
# Packer loads all *.pkr.hcl files from the component directory
vars:
region: us-east-1

This allows you to organize your Packer component with multiple files:

components/packer/my-ami/
├── variables.pkr.hcl # Variable declarations
├── main.pkr.hcl # Source and build blocks
├── locals.pkr.hcl # Local values (optional)
└── plugins.pkr.hcl # Required plugins (optional)

Single File Mode

For simple components or when you need to specify a particular template file:

components:
packer:
my-ami:
settings:
packer:
template: main.pkr.hcl # Use specific file
vars:
region: us-east-1

You can also use the --template (or -t) flag on the command line to override the template:

atmos packer validate my-ami -s prod --template main.pkr.hcl
Directory Mode Benefits

Directory mode is recommended because it:

  • Follows Packer best practices for organizing configurations
  • Allows separation of variables, sources, and builds into logical files
  • Simplifies stack manifests by not requiring explicit template configuration

Running Packer Commands

Atmos provides commands that wrap Packer operations:

# Initialize Packer plugins
atmos packer init ami-ubuntu -s plat-ue1-prod

# Validate templates
atmos packer validate ami-ubuntu -s plat-ue1-prod

# Build images
atmos packer build ami-ubuntu -s plat-ue1-prod

# Run any packer subcommand
atmos packer <subcommand> <component> -s <stack>

Environment Variables

Common environment variables for Packer components:

PACKER_LOG
Enable logging (set to 1).
PACKER_LOG_PATH
Path to log file.
AWS_PROFILE
AWS profile for authentication.
AWS_REGION
Default AWS region.
PACKER_CACHE_DIR
Directory for Packer cache.

Variables File Generation

Atmos generates a variables file that Packer uses during builds:

// atmos-packer.pkrvars.json (auto-generated)
{
"region": "us-east-1",
"instance_type": "t3.medium",
"ami_name_prefix": "acme-prod",
"source_ami_filter": {
"name": "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*",
"owner": "099720109477"
},
"tags": {
"Environment": "prod",
"ManagedBy": "Atmos"
}
}

Passing Secrets Securely

The generated variables file is written to disk in plaintext (world‑readable, 0644) so Packer can read it. Unlike Terraform — which keeps secret‑bearing vars off disk — Packer performs no off‑disk partitioning. Never put secrets in a Packer component's vars section.

Secrets belong in env, not vars

Values in the env section are injected only into the Packer subprocess environment and are never written to the variables file. Declare secrets under secrets.vars, reference them with !secret in env, and read them in the Packer template with the env function.

stacks/deploy/prod.yaml
components:
packer:
bastion:
secrets:
vars:
GITHUB_TOKEN:
description: "Token for provisioners that clone private repos"
store: ssm-secrets # a `secret: true` store; or use `sops: <provider>`
required: true
env:
# Injected ONLY into the Packer subprocess environment — never written to the
# variables file on disk.
GITHUB_TOKEN: !secret GITHUB_TOKEN
vars:
region: us-east-1 # non-secret values only

Read the secret from the environment in the Packer template (Packer's env function is allowed in variable defaults and locals):

components/packer/bastion/build.pkr.hcl
variable "github_token" {
type = string
sensitive = true
default = env("GITHUB_TOKEN") # read from the environment, not the variables file
}

Multi-Cloud Support

Packer components can build images for multiple cloud providers:

AWS AMI

components:
packer:
ami-builder:
vars:
region: us-east-1
source_ami_filter:
name: "ubuntu/images/*"
owner: "099720109477"

Azure VM Image

components:
packer:
azure-image:
vars:
azure_subscription_id: "{{ env \"ARM_SUBSCRIPTION_ID\" }}"
azure_resource_group: "packer-images-rg"
azure_location: "eastus"
env:
ARM_CLIENT_ID: "{{ env \"ARM_CLIENT_ID\" }}"
ARM_CLIENT_SECRET: "{{ env \"ARM_CLIENT_SECRET\" }}"

GCP Machine Image

components:
packer:
gcp-image:
vars:
project_id: "my-gcp-project"
zone: "us-central1-a"
source_image_family: "ubuntu-2204-lts"
env:
GOOGLE_APPLICATION_CREDENTIALS: "/path/to/credentials.json"

Docker Image

components:
packer:
docker-image:
vars:
repository: "myregistry/myimage"
tag: "latest"
base_image: "ubuntu:22.04"
env:
DOCKER_HOST: "unix:///var/run/docker.sock"

Best Practices

  1. Version Pin Plugins: Always specify explicit plugin versions in your Packer templates.

  2. Use Source AMI Filters: Use filters with most_recent = true to automatically get the latest base images.

  3. Tag Everything: Apply consistent tags to all built images for tracking and cost allocation.

  4. Use Provisioner Scripts: Keep provisioning logic in external scripts for easier testing and maintenance.

  5. Clean Up: Include cleanup scripts to reduce image size and remove sensitive data.

  6. Use Dependencies: Define depends_on when images need to be built in a specific order.

  7. Centralize Defaults: Define common settings in catalog defaults and override only when necessary.