Packer, the golden child of image building, and Puppet, the old guard of configuration management, can be a surprisingly potent, if sometimes prickly, combo.

Let’s spin up a quick Ubuntu 22.04 image with a basic Nginx setup using Packer and Puppet.

{
  "builders": [
    {
      "type": "amazon-ebs",
      "region": "us-east-1",
      "instance_type": "t2.micro",

      "ami_name": "packer-puppet-nginx-{{timestamp}}",

      "ssh_username": "ubuntu",
      "source_ami_filter": {
        "filters": {
          "name": "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20230110",
          "virtualization-type": "hvm"
        },
        "owners": [ "099720109477" ]
      }
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "sudo apt-get update",
        "sudo apt-get install -y ruby-dev build-essential zlib1g-dev libssl-dev",
        "sudo gem install puppet --no-ri --no-rdoc"
      ]
    },
    {
      "type": "puppet-masterless",
      "manifest_file": "manifests/nginx.pp",
      "module_paths": [
        "modules"
      ]
    }
  ]
}

And here’s our manifests/nginx.pp:

class { 'nginx': }

And a minimal modules/nginx/manifests/init.pp:

class nginx {
  package { 'nginx':
    ensure => installed,
  }
}

When you run packer build your-template.json, Packer will:

  1. Launch an EC2 instance using the specified source_ami_filter.
  2. Connect to it via SSH.
  3. Execute the shell provisioner: update package lists, install Ruby development tools (needed for gem), and then install Puppet itself directly onto the instance. This is the "masterless" part – Puppet runs locally.
  4. Execute the puppet-masterless provisioner. This tells Puppet to apply manifests/nginx.pp, looking for any required modules in the modules directory. In this case, it finds the nginx class and ensures the nginx package is installed.
  5. Once provisioning is complete, Packer creates an AMI from the instance.

The core problem this solves is creating immutable infrastructure. Instead of configuring servers after they launch, you bake your desired state into an AMI. When you need a new server, you launch an instance from this pre-configured AMI, drastically reducing configuration drift and ensuring consistency.

Internally, Packer acts as an orchestrator. It handles the lifecycle of the temporary EC2 instance: creation, SSH connection, running provisioners in order, and finally, snapshotting the disk to create the AMI. The puppet-masterless provisioner specifically uses puppet apply under the hood on the target instance.

The real magic happens with the module_paths. If your nginx.pp relied on more complex configurations, you’d structure them into Puppet modules and point Packer to their location. This keeps your manifests clean and your modules reusable.

What most people don’t realize is that the puppet-masterless provisioner doesn’t just run puppet apply. It also intelligently copies your manifest_file and any referenced module_paths to the instance before executing puppet apply. This means you don’t need to pre-install your Puppet code onto the image yourself; Packer handles the transfer.

The next thing you’ll likely wrestle with is how to manage secrets or more complex Puppet hiera data within your Packer builds.

Want structured learning?

Take the full Packer course →