Provisioners and Post-Processors¶
Provisioners install software and configure the system inside the builder. Post-processors transform or publish the resulting artifact. Together they turn a raw OS into a fully-baked deployable image.
Provisioners Overview¶
Provisioners run in the order declared, sequentially per source. Any failure aborts the build for that source (unless on_error is set).
build {
sources = ["source.amazon-ebs.ubuntu"]
provisioner "shell" {
inline = ["sudo apt-get update"]
}
provisioner "file" {
source = "app.jar"
destination = "/tmp/app.jar"
}
provisioner "shell" {
inline = ["sudo mv /tmp/app.jar /opt/app.jar"]
}
}
The shell Provisioner¶
Most-used provisioner. Runs shell commands on the builder instance via SSH.
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
]
}
provisioner "shell" {
scripts = [
"scripts/harden.sh",
"scripts/install-agents.sh",
]
}
provisioner "shell" {
script = "scripts/bootstrap.sh"
environment_vars = ["APP_VERSION=${var.app_version}"]
execute_command = "sudo -E bash '{{ .Path }}'"
}
inline: list of commands executed in one shell sessionscript: single local script uploaded and executedscripts: multiple scripts run in orderenvironment_vars: exported before executionexecute_command: custom invocation (useful forsudo -E)expect_disconnect: tolerate the SSH disconnect caused byrebootpause_before,pause_after: delaysvalid_exit_codes: default is[0]; set to allow non-zero exits
The file Provisioner¶
Copies files or directories to/from the builder.
provisioner "file" {
source = "configs/"
destination = "/tmp/configs/"
}
provisioner "file" {
source = "/var/log/custom.log"
destination = "./local-copy.log"
direction = "download"
}
direction = "upload"(default) or"download"- The destination must exist or be writable by the SSH user
- Use a staging dir like
/tmpthenshellto move with sudo
The ansible Provisioner¶
Runs Ansible against the builder from the Packer host. Requires Ansible installed on the host.
provisioner "ansible" {
playbook_file = "playbook.yml"
user = "ubuntu"
extra_arguments = ["-vv", "--extra-vars", "env=build"]
groups = ["build"]
}
Features:
- Automatic inventory generation
- Support for roles (
ansible.cfgorroles_path) - Can target Windows via WinRM
The ansible-local Provisioner¶
Runs Ansible inside the builder, not on the host. Requires Ansible installed on or installed into the builder.
provisioner "ansible-local" {
playbook_file = "playbook.yml"
playbook_dir = "./playbooks"
}
Use when:
- Host cannot SSH directly to builder (network constraints)
- Building Windows images where running Ansible from host is awkward
- You want the playbook available inside the image for re-runs
PowerShell and Windows Provisioners¶
For Windows builds:
provisioner "powershell" {
inline = [
"Install-WindowsFeature -Name Web-Server",
"Set-Service -Name W3SVC -StartupType Automatic",
]
}
provisioner "windows-shell" {
inline = ["net user /add myuser P@ssw0rd"]
}
provisioner "windows-restart" {
restart_timeout = "10m"
}
Chef and Puppet Provisioners¶
For teams already using these systems:
chef-solo: runs local cookbookschef-client: registers with a Chef serverpuppet-masterless: runs manifests locallypuppet-server: registers with PE
Modern practice leans toward shell + ansible, but existing shops use these.
The breakpoint Provisioner¶
Pauses the build for interactive debugging.
provisioner "breakpoint" {
disable = false
note = "Inspect after nginx install"
}
Packer prints SSH details and waits for you to press enter. Open a second terminal, SSH in, poke around, then continue. Remove before production pipelines.
The shell-local Provisioner¶
Runs commands on the host that is running Packer, not on the builder.
provisioner "shell-local" {
inline = [
"echo Building AMI at $(date)",
"./scripts/fetch-certs.sh",
]
}
Useful for:
- Fetching artifacts locally before
fileupload - Notifying external systems (Slack, Datadog)
- Pre- or post-build housekeeping
Error Handling¶
provisioner "shell" {
inline = ["false"]
on_error = "cleanup" # cleanup, abort, run-cleanup-provisioner, ask
}
cleanup(default): clean up builder resources and exitabort: leave the instance running for debuggingrun-cleanup-provisioner: run the cleanup provisioner firstask: interactive prompt
CLI flag: packer build -on-error=abort .
Provisioner Filtering¶
Restrict a provisioner to specific sources using only and except:
provisioner "shell" {
only = ["amazon-ebs.prod"]
inline = ["echo prod-only"]
}
Post-Processors Overview¶
Post-processors run after the builder produces an artifact. They can chain (the output of one becomes input to the next) using post-processors blocks:
build {
# ...
post-processor "shell-local" {
inline = ["echo Build complete"]
}
post-processors {
post-processor "manifest" { output = "manifest.json" }
post-processor "checksum" { checksum_types = ["sha256"] }
}
}
Note the singular post-processor for independent runs, plural post-processors (a block containing post-processor entries) for chained sequences.
The manifest Post-Processor¶
Writes a JSON file describing all artifacts from the build:
post-processor "manifest" {
output = "manifest.json"
strip_path = true
custom_data = {
build_id = "${formatdate("YYYYMMDD-hhmm", timestamp())}"
}
}
Contents (example):
{
"builds": [{
"name": "base",
"builder_type": "amazon-ebs",
"build_time": 1690000000,
"files": null,
"artifact_id": "us-east-1:ami-abc123",
"packer_run_uuid": "..."
}],
"last_run_uuid": "..."
}
Consume in Terraform or pipelines to pick up AMI IDs programmatically.
The checksum Post-Processor¶
Generates hashes for output files:
post-processor "checksum" {
checksum_types = ["md5", "sha256", "sha512"]
}
Useful for artifacts like qcow2 or OVA files. AMIs do not have a filesystem hash concept.
The docker Post-Processors¶
docker-tag: apply tags to a committed imagedocker-push: push to a registrydocker-save: save image as tarballdocker-import: import tarball as Docker image
post-processors {
post-processor "docker-tag" {
repository = "myorg/myapp"
tag = ["latest", "${timestamp()}"]
}
post-processor "docker-push" {
login = true
login_server = "registry.example.com"
login_username = var.registry_user
login_password = var.registry_pass
}
}
The amazon-import Post-Processor¶
Imports a VMDK or OVA file as an AMI. Used with vsphere-iso or other non-AWS sources to produce an AMI in AWS.
The googlecompute-import Post-Processor¶
Similarly imports a raw disk image into GCP as a Compute image.
The vagrant Post-Processor¶
Packages a build as a Vagrant box:
post-processor "vagrant" {
keep_input_artifact = true
output = "output/ubuntu-{{.Provider}}.box"
}
The hcp-packer-registry Post-Processor¶
Registers the artifact as an iteration in HCP Packer:
post-processor "hcp-packer-registry" {
bucket_name = "ubuntu-base"
description = "Ubuntu 22.04 with hardening"
bucket_labels = {
os = "ubuntu-22.04"
}
build_labels = {
build_id = "${formatdate("YYYYMMDD-hhmm", timestamp())}"
}
}
Requires HCP_CLIENT_ID and HCP_CLIENT_SECRET env vars.
Chaining Considerations¶
When chaining post-processors, by default each subsequent post-processor consumes the output of the previous and replaces the artifact. Set keep_input_artifact = true to preserve both.
post-processors {
post-processor "compress" {
keep_input_artifact = true
}
post-processor "shell-local" {
inline = ["upload-to-s3.sh"]
}
}
Best Practices¶
- Idempotency: scripts should be safe to re-run. Use
apt-get install -y(idempotent) nottar xfon a volatile path. - Clean up during build:
apt-get clean, remove SSH host keys, zero free space if size matters. - Don't leave secrets on disk: any provisioner that writes secrets must remove them before image capture.
- Tag and version: every artifact should be traceable back to a git commit, build timestamp, and pipeline run.
- Prefer Ansible for complex config: shell scripts become unwieldy past ~50 lines.
Common Errors¶
Permission denied: wrongssh_usernamefor the source AMIdial tcp: i/o timeout: security group or subnet does not allow SSH from Packer's IPFailed to upload file: destination directory does not exist or no write permissionansible-playbook: command not found: Ansible not installed on the Packer host (useansible-localinstead)WinRM connection failed: Windows build missinguser_datathat enables WinRM
Exam-Ready Checklist¶
- Know differences between shell, shell-local, file, ansible, ansible-local
- Know when to use
breakpoint - Can chain post-processors with
post-processorsblocks - Know the most-used post-processors (manifest, checksum, docker-*, hcp-packer-registry)
- Understand
on_erroroptions - Can filter provisioners with
onlyandexcept