Packer variables let you parameterize your builds, making them reusable and adaptable. The most surprising thing about them is how deeply intertwined they are with the Go templating engine Packer uses, which means you’re not just substituting strings; you’re executing code.

Let’s watch this in action. Imagine you want to build an AMI that’s tagged with the build timestamp and a specific version.

Here’s a packer.json snippet:

{
  "builders": [
    {
      "type": "amazon-ebs",
      "region": "us-east-1",
      "source_ami_filter": {
        "filters": {
          "name": "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
        },
        "most_recent": true
      },

      "ami_name": "my-ubuntu-{{timestamp}}",

      "tags": {
        "Version": "1.0.0",

        "BuildDate": "{{timestamp}}"

      },
      "instance_type": "t2.micro",
      "ssh_username": "ubuntu",
      "ami_description": "Ubuntu 20.04 LTS built by Packer"
    }
  ],
  "variables": {
    "env": "dev"
  }
}

When you run packer build packer.json, Packer will process the {{timestamp}} and {{env}} placeholders. {{timestamp}} will be replaced by the current date and time in RFC3339 format (e.g., 2023-10-27T10:30:00Z). The ami_name will become my-ubuntu-2023-10-27T10:30:00Z, and the BuildDate tag will also show this timestamp. The env variable, defined in the JSON, will be substituted as dev if not overridden.

You can provide variables via the command line, environment variables, or separate files.

Command Line: packer build -var 'env=prod' packer.json

Environment Variable: export PK_VAR_env=staging packer build packer.json

Variable File: Create a vars/dev.json:

{
  "env": "dev"
}

And then build: packer build -var-file=vars/dev.json packer.json

The order of precedence for variables is: command line > environment variables > variable files > variables defined in packer.json.

The real power comes from using Go’s templating functions within your variable substitutions. This isn’t just string replacement.

Consider this packer.json:

{
  "builders": [
    {
      "type": "amazon-ebs",
      "region": "us-east-1",
      "source_ami_filter": {
        "filters": {
          "name": "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
        },
        "most_recent": true
      },

      "ami_name": "my-app-{{user `app_version`}}-{{timestamp | humanizeDuration}}",

      "tags": {

        "Version": "{{user `app_version`}}",


        "BuildDate": "{{timestamp}}"

      },
      "instance_type": "t2.micro",
      "ssh_username": "ubuntu",

      "ami_description": "Ubuntu 20.04 LTS with app version {{user `app_version`}}"

    }
  ],
  "variables": {
    "app_version": "latest"
  }
}

Here, {{user app_version}} pulls a variable named app_version that you’d typically provide on the command line or via an environment variable. The {{timestamp | humanizeDuration}} is where Go templating shines. humanizeDuration is a built-in function that takes a duration (like the one provided by timestamp) and formats it into something more human-readable, like "30 minutes ago."

To build this, you’d likely run: packer build -var 'app_version=1.5.2' packer.json

The ami_name might look like: my-app-1.5.2-2023-10-27T10:30:00Z. If you wanted to use the humanized duration, you’d need to pass the timestamp directly to the function, which isn’t directly supported for {{timestamp}}. However, you can use other Go template functions. For instance, to format the timestamp differently:

"ami_name": "my-app-{{user app_version}}-{{timestamp | date "2006-01-02T15:04:05Z"}}"

This would produce an ami_name like my-app-1.5.2-2023-10-27T10:30:00Z.

Local variables, defined within a locals block, are evaluated once and can be reused throughout the template. They are particularly useful for complex or repeated calculations.

{
  "builders": [
    {
      "type": "amazon-ebs",
      "region": "us-east-1",
      "source_ami_filter": {
        "filters": {
          "name": "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"
        },
        "most_recent": true
      },

      "ami_name": "webserver-{{local `build_id`}}-{{timestamp | date "2006-01-02"}}",

      "tags": {

        "BuildID": "{{local `build_id`}}",


        "Environment": "{{user `env`}}"

      },
      "instance_type": "t2.micro",
      "ssh_username": "ubuntu",

      "ami_description": "Webserver AMI for {{user `env`}}"

    }
  ],
  "variables": {
    "env": "staging"
  },
  "locals": {

    "build_id": "{{timestamp | randAlphaNumeric 8}}",


    "base_ami": "{{user `source_ami` | default `ami-0abcdef1234567890`}}"

  }
}

In this example, {{local build_id}} generates a random 8-character alphanumeric string using randAlphaNumeric 8 and assigns it to build_id. This build_id is then used in the ami_name and tags. The {{local base_ami}} demonstrates using a default value if the source_ami variable isn’t provided.

To build this, you might run: packer build -var 'env=production' -var 'source_ami=ami-0fedcba9876543210' packer.json

The ami_name could become: webserver-aBcDeF12-2023-10-27.

The key takeaway is that variables in Packer are not static substitutions. They are dynamic expressions evaluated by a Go templating engine, allowing for functions, logic, and even conditional defaults. This makes your Packer configurations incredibly powerful and flexible.

The next step is understanding how to use provisioners and post-processors in conjunction with these variables to create highly customized and automated machine images.

Want structured learning?

Take the full Packer course →