Skip to main content

Packer Template Model

JSON templates will not be covered in this study, only HCL2 which is recommended by HashiCorp.

Packer uses the HashiCorp Configuration Language - HCL - designed to allow concise descriptions of the steps needed to get a build file.

A brief explanation about the HCL2 model https://developer.hashicorp.com/packer/guides/hcl

Everything can be defined within a single file, but it's interesting to separate the files for better organization.

  • variables.pkr.hcl
  • packer.pkr.hcl
  • build1.pkr.hcl
  • build2.pkr.hcl

Block Types​

The Packer language - HCL2 includes several built-in blocks that you can use to configure builds. A block is a container for configuration.

The most important blocks can be divided into a few main types:

  • build blocks contain configuration for a specific combination of builders, provisioners, and post-processors used to create a specific image artifact.

  • source blocks contain configuration for builder plugins. Once defined, sources can be used and configured later by the "build" block.

  • provisioners blocks contain configuration for provisioner plugins. These blocks are nested inside a build block.

  • post-processors blocks contain configuration for post-processor plugins and sequences of post-processor plugins. They are also nested inside build blocks.

  • variable blocks contain configuration for variables that can be defaulted in the configuration or defined by the user at runtime.

  • locals blocks contain configuration for variables that can be created using HCL functions or data sources, or composed of variables created in variable blocks.

  • packer provides information to the Packer core about which version it can run. The "required_plugins" block helps the Packer core

Variable​

An example of the variable block could be defined inside a variables.pkr.hcl file.

As best practice, always have the description.

variable "project_name" {
type = string
description = "Project Name "
}

variable "region" {
type = string
default = "us-east-1"
description = "Region to create source in aws"
sensitive = false
# When a variable is sensitive all string-values from that variable will be
# obfuscated from Packer's output.
}

Locals​

File used to manipulate variables internally with interpolation or even set a variable by function. It is not mandatory, but it is a matter of organization.

I particularly prefer to declare a separate file for locals called locals.pkr.hcl

locals {
timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}

# Use the singular local block if you need to mark a local as sensitive
local "mylocal" {
expression = "${var.secret_api_key}"
sensitive = true
}

Packer​

This single block is used to configure some behaviors of Packer itself, such as the minimum required version of Packer needed to apply your configuration.

It is good practice to always define a minimum version.

I like to isolate this block in packer.pkr.hcl

packer {
required_plugins {
happycloud = {
version = ">= 2.7.0"
source = "github.com/hashicorp/happycloud"
}
}
required_version = ">= 1.8.5"
}

Data​

This block defines data sources, for example, to fetch some resource in the cloud to be referenced during the build process

I like to keep these blocks also separate in data.pkr.hcl when necessary


The following two blocks I like to keep in the same file which would be main.pkr.hcl

Source​

Source blocks define the builder configuration, but it is in the build block that they are instantiated. In summary, it will create the foundation for the build to happen. If we are talking about creating an AMI based on another, this block will declare what the configuration of an EC2 should be when the build instantiates.

Each source must be unique with the name, but can have the same types.

## amazon-ebs is the type and ec2main is the name to be referenced later by the build block

source "amazon-ebs" "ec2main" {

ami_name = "${var.project_name}-${local.timestamp}"
ami_regions = var.ami_regions
instance_type = var.instance_type
region = var.region
source_ami_filter {
filters = {
name = var.image_name
virtualization-type = var.virtualization
root-device-type = "ebs"
}
owners = ["${var.image_owner}"]
most_recent = true
}
ssh_username = var.ssh_username
ssh_timeout = var.ssh_timeout
ssh_keep_alive_interval = var.ssh_keep_alive_interval
ssh_pty = var.ssh_pty
}

Each source type has its own configuration. It is necessary to study each one when they need to be used.

This block could be entirely declared inside build, but it's not organized. However, we could only rewrite some values if necessary. The advantage of this is even code reuse. That's why I like to keep it in the same file.

Build​

It is the main block and only with this block could achieve a compilation directly in packer if all variables were set or no extra plugin was necessary.

This block defines which builders are started, how to provision and if necessary what to do with the generated artifact.


build {
# Name if set will name the log, but it's not mandatory
name = "buildname"

# list of sources that will be used without renaming any attribute
sources = [
"source.amazon-ebs.ec2main",
]

# If there is a source that will be used and you want to rename something, you must separate this and make the change
source "source.amazon-ebs.ec2secondary" {
output = "different value"
name = "differentname"
}

# There are different provisioners which is what will actually execute to customize the image.

provisioner "shell" {
scripts = fileset(".", "scripts/{install,secure}.sh")
}

# If you want to do something with the artifact, use the post-processor
post-processor "shell-local" {
inline = ["echo Hello World from ${source.type}.${source.name}"]
}
}

Multiple provisioners and multiple post-processors can be defined

Additional Reading​

Worth a quick read on Expressions and syntax