Packer’s HCL2 syntax is a significant improvement over its older JSON templates, offering better readability, reusability, and a more intuitive way to define infrastructure.
Let’s see Packer HCL2 in action. Imagine you want to build an AWS AMI for a web server.
Here’s a simplified JSON template:
{
"builders": [
{
"type": "amazon-ebs",
"region": "us-east-1",
"instance_type": "t2.micro",
"source_ami": "ami-0c55b159cbfafe1f0",
"ssh_username": "ec2-user",
"ami_name": "my-webserver-ami-{{timestamp}}"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"sudo yum update -y",
"sudo yum install -y httpd",
"sudo systemctl start httpd",
"sudo systemctl enable httpd"
]
}
]
}
Now, let’s convert this to HCL2. The equivalent HCL2 configuration would look like this:
source "amazon-ebs" "webserver" {
region = "us-east-1"
instance_type = "t2.micro"
source_ami = "ami-0c55b159cbfafe1f0"
ssh_username = "ec2-user"
ami_name = "my-webserver-ami-{{timestamp}}"
}
build {
sources = ["source.amazon-ebs.webserver"]
provisioner "shell" {
inline = [
"sudo yum update -y",
"sudo yum install -y httpd",
"sudo systemctl start httpd",
"sudo systemctl enable httpd"
]
}
}
This HCL2 configuration defines a source block named webserver of type amazon-ebs. This block encapsulates all the details about the builder. The build block then specifies which sources to use and what provisioners to run.
The primary problem HCL2 solves is the verbosity and structural rigidity of JSON for complex configurations. JSON, being a data serialization format, lacks constructs for logic, variables, and modularity. HCL2, on the other hand, is designed for configuration, introducing concepts like blocks, arguments, and expressions. This allows for a more declarative and maintainable way to define your machine images.
Internally, Packer parses HCL2 into an Abstract Syntax Tree (AST) and then traverses this tree to execute the defined steps. The source blocks define the "what" and "where" of building (e.g., the cloud provider, region, instance type), while the build block defines the "how" (e.g., which sources to use, what provisioners to run). Provisioners are executed sequentially in the order they appear within the build block.
You can also define variables in HCL2, making your templates more dynamic and reusable. For example:
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "instance_type" {
type = string
default = "t2.micro"
}
source "amazon-ebs" "webserver" {
region = var.aws_region
instance_type = var.instance_type
source_ami = "ami-0c55b159cbfafe1f0"
ssh_username = "ec2-user"
ami_name = "my-webserver-ami-{{timestamp}}"
}
build {
sources = ["source.amazon-ebs.webserver"]
provisioner "shell" {
inline = [
"sudo yum update -y",
"sudo yum install -y httpd",
"sudo systemctl start httpd",
"sudo systemctl enable httpd"
]
}
}
You can then override these variables using the -var flag when running packer build: packer build -var 'aws_region=us-west-2' my-template.hcl.
A key aspect of HCL2 that often surprises people is how it handles loops and conditional logic within build blocks, which is not directly supported in the same way as in general-purpose programming languages. Instead, HCL2 emphasizes composition and referencing. For instance, to create multiple AMIs with slight variations, you wouldn’t typically use a for_each loop directly within a build block for the entire build. Instead, you’d define separate source blocks or use HCL’s for expressions to generate configurations that are then referenced. This encourages a more declarative approach where you define distinct building units.
The next step in mastering Packer HCL2 is understanding how to structure larger, more complex configurations using modules and composite builds.