Provisioners in Depth

Master Packer provisioners to customize your machine images

TLDR: Provisioners install software and configure your images. Common ones are shell scripts, file uploads, and configuration management tools like Ansible. They run in order, and you can control which builders they apply to.

Provisioners are where the actual customization happens. This is where you install packages, copy files, configure services, and prepare the image for production use.

Shell Provisioner

The most common provisioner. Runs shell commands or scripts:

Inline commands:

provisioner "shell" {
  inline = [
    "sudo apt-get update",
    "sudo apt-get install -y nginx redis-server",
    "sudo systemctl enable nginx redis-server"
  ]
}

External script:

provisioner "shell" {
  script = "scripts/setup-app.sh"
}

Multiple scripts:

provisioner "shell" {
  scripts = [
    "scripts/install-dependencies.sh",
    "scripts/configure-services.sh",
    "scripts/cleanup.sh"
  ]
}

Environment variables:

provisioner "shell" {
  environment_vars = [
    "APP_VERSION=${var.app_version}",
    "ENVIRONMENT=production"
  ]
  script = "scripts/install-app.sh"
}

File Provisioner

Copies files from your computer to the image:

Single file:

provisioner "file" {
  source      = "configs/nginx.conf"
  destination = "/tmp/nginx.conf"
}

Directory:

provisioner "file" {
  source      = "app/"
  destination = "/tmp/app"
}

Content from variable:

provisioner "file" {
  content     = templatefile("config.tpl", { db_host = var.db_host })
  destination = "/tmp/config.json"
}

The file provisioner uploads to temporary locations. Use a shell provisioner to move files to their final destinations:

provisioner "file" {
  source      = "app.tar.gz"
  destination = "/tmp/app.tar.gz"
}

provisioner "shell" {
  inline = [
    "sudo tar -xzf /tmp/app.tar.gz -C /opt/",
    "sudo chown -R appuser:appuser /opt/app",
    "rm /tmp/app.tar.gz"
  ]
}

Ansible Provisioner

Runs Ansible playbooks against the image:

provisioner "ansible" {
  playbook_file = "./playbook.yml"
  extra_arguments = [
    "--extra-vars",
    "ansible_python_interpreter=/usr/bin/python3"
  ]
  ansible_env_vars = [
    "ANSIBLE_HOST_KEY_CHECKING=False"
  ]
}

When to use Ansible provisioner:

  • You already have Ansible playbooks
  • Complex configuration logic
  • Need idempotency (running multiple times)
  • Want to use Ansible roles from Galaxy

Example playbook (playbook.yml):

---
- name: Configure web server
  hosts: default
  become: yes
  tasks:
    - name: Install packages
      apt:
        name:
          - nginx
          - postgresql-client
        state: present
        update_cache: yes
    
    - name: Copy nginx config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: reload nginx
    
    - name: Enable nginx
      systemd:
        name: nginx
        enabled: yes
  
  handlers:
    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded

Chef Provisioner

Runs Chef recipes:

provisioner "chef-solo" {
  cookbook_paths = ["cookbooks"]
  run_list       = ["recipe[nginx]", "recipe[app::deploy]"]
  json = {
    nginx = {
      port = 8080
    }
  }
}

Puppet Provisioner

Applies Puppet manifests:

provisioner "puppet-masterless" {
  manifest_file = "manifests/default.pp"
  module_paths  = ["modules"]
}

Provisioner Order and Execution

Provisioners run in the order defined:

build {
  sources = ["source.amazon-ebs.web"]
  
  # 1. Update system
  provisioner "shell" {
    inline = ["sudo apt-get update"]
  }
  
  # 2. Install base packages
  provisioner "shell" {
    inline = ["sudo apt-get install -y curl wget git"]
  }
  
  # 3. Copy application files
  provisioner "file" {
    source      = "app/"
    destination = "/tmp/app"
  }
  
  # 4. Run configuration
  provisioner "ansible" {
    playbook_file = "playbook.yml"
  }
  
  # 5. Cleanup
  provisioner "shell" {
    inline = [
      "sudo apt-get clean",
      "sudo rm -rf /tmp/*"
    ]
  }
}

Selective Provisioning

Run provisioner on specific builders:

provisioner "shell" {
  only = ["amazon-ebs.production"]
  inline = ["echo 'Production specific setup'"]
}

Exclude builders:

provisioner "shell" {
  except = ["docker.test"]
  inline = ["echo 'Runs on all builders except docker.test'"]
}

Conditional provisioning:

variable "install_monitoring" {
  type    = bool
  default = true
}

provisioner "shell" {
  inline = [
    var.install_monitoring ? "sudo apt-get install -y datadog-agent" : "echo Skipping monitoring"
  ]
}

Error Handling

Pause on failure:

provisioner "shell" {
  pause_before = "10s"
  inline       = ["some-command"]
}

Timeout:

provisioner "shell" {
  timeout = "30m"
  script  = "scripts/long-running-task.sh"
}

Max retries:

provisioner "shell" {
  max_retries = 3
  inline      = ["curl -f https://api.example.com/health"]
}

Continue on error (not recommended):

provisioner "shell" {
  inline = ["command-that-might-fail || true"]
}

Common Patterns

Wait for System to be Ready

provisioner "shell" {
  inline = [
    "cloud-init status --wait",  # Wait for cloud-init to finish
    "sudo systemctl is-system-running --wait"  # Wait for system to be fully up
  ]
}

Install from Package Repository

provisioner "shell" {
  inline = [
    "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -",
    "sudo add-apt-repository 'deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable'",
    "sudo apt-get update",
    "sudo apt-get install -y docker-ce"
  ]
}

Download and Install Binary

provisioner "shell" {
  inline = [
    "wget https://releases.example.com/app-${var.version}.tar.gz",
    "sudo tar -xzf app-${var.version}.tar.gz -C /opt/",
    "sudo ln -s /opt/app-${var.version}/bin/app /usr/local/bin/app"
  ]
}

Configure Service

provisioner "file" {
  source      = "configs/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"
  ]
}

Cleanup for Production

provisioner "shell" {
  inline = [
    # Clean package manager cache
    "sudo apt-get clean",
    "sudo rm -rf /var/lib/apt/lists/*",
    
    # Remove temporary files
    "sudo rm -rf /tmp/*",
    "sudo rm -rf /var/tmp/*",
    
    # Clear logs
    "sudo find /var/log -type f -exec truncate -s 0 {} \\;",
    
    # Remove bash history
    "rm -f ~/.bash_history",
    "sudo rm -f /root/.bash_history",
    
    # Clear machine ID (important for cloud images)
    "sudo truncate -s 0 /etc/machine-id",
    "sudo rm -f /var/lib/dbus/machine-id"
  ]
}

Best Practices

Use scripts for complex logic: Inline provisioners are good for simple commands. For anything complex, use external scripts.

Make provisioners idempotent: Provisioners should be safe to run multiple times during development.

Test provisioners locally: Use Docker builder to test provisioning logic before cloud builds.

Separate concerns: One provisioner per logical task (install, configure, cleanup).

Use variables: Don't hardcode versions or URLs. Use variables so templates are reusable.

Check exit codes: Shell commands fail on non-zero exit. Test commands locally first.

Clean up thoroughly: Remove build artifacts, package caches, and temporary files to reduce image size.

What's Next

Provisioners customize your images. The next chapter covers variables and how to make templates reusable across different environments and use cases.

Found an issue?