Automating Software Installation with Packer Shell Provisioners
Learn how to use Packer shell provisioners with external scripts to automate software installation during image builds, avoid drift, and ensure privilege escalation.
16 Sept 2025, 12:27 UTC

To bake software into a machine image, use the provisioner "shell" block to execute installation scripts after the OS boots but before the final snapshot is taken. This eliminates runtime installation delays and prevents configuration drift across your fleet.
How Shell Provisioners Operate
A provisioner is a set of instructions that Packer runs on a temporary instance. The shell provisioner specifically connects via SSH (for Linux) or WinRM (for Windows) using the credentials defined in the builder block. It then uploads and executes a script or a set of inline commands.
Because provisioners run as the user specified in the builder, they often lack the permissions required to install system-level packages. To resolve this, you must use an execute_command that includes privilege escalation, such as sudo, to ensure the script can modify the root filesystem.
Worked Configuration: Installing Nginx
The following HCL2 example uses an Amazon EBS builder to create an Ubuntu 22.04 image. It utilizes an external script file to keep the build template clean and version-controllable.
source "amazon-ebs" "ubuntu" {
ami_name = "packer-ubuntu-nginx-{{timestamp}}"
instance_type = "t3.micro"
ssh_username = "ubuntu"
source_ami_filter {
filters = {
name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
owners = ["099720109477"]
most_recent = true
}
}
build {
sources = ["source.amazon-ebs.ubuntu"]
provisioner "shell" {
# Use sudo -E to preserve environment variables during execution
execute_command = "{{ .Vars }} sudo -E bash '{{ .Path }}'"
script = "scripts/install-nginx.sh"
pause_before = "15s"
}
}
In this configuration, .Path is a Packer internal variable that points to the uploaded script on the remote machine. The pause_before argument is used to allow cloud-init processes to complete their initial setup before the provisioner attempts to lock the package manager (apt).
Limitations and Common Pitfalls
- Silent Failures: If the
execute_commanddoes not properly handle sudo passwords or permissions, the script may fail silently or partially. Always ensure the builder user has passwordless sudo access or provide the necessary credentials via secure variables. - Inline Script Limits: Using the
inlineargument for long scripts can lead to shell character limit errors or encoding issues. For any script longer than a few lines, use thescriptargument to reference an external file. - Timing Issues:
pause_beforeandpause_afterare static timers, not health checks. If a network-heavy installation takes longer than expected, a static pause will not prevent a race condition. Implementwhileloops inside your shell scripts to poll for service readiness. - Secret Exposure: Hard-coding API keys or passwords inside the
provisionerblock or the script file exposes them in the image metadata and Packer logs. Use environment variables or Packer variables marked assensitive.
Verification and Testing
To ensure the build is configured correctly before spending time and credits on a full image creation, run the following command from your terminal:
packer validate .
This checks the HCL2 syntax and verifies that all referenced script paths exist. To debug a failing provisioner in real-time, run the build with the debug flag:
packer build -debug .
Final Verification: A successful Packer exit code does not guarantee the software is functional. The only reliable way to verify the result is to launch a temporary instance from the resulting AMI/VMDK and run a diagnostic check, such as curl -I localhost for an Nginx installation.
Rollback
Because Packer creates a new image rather than modifying an existing one, there is no state to roll back on the target system. To revert a change, modify the script or HCL2 template and trigger a new build to generate a corrected image version.