The Developer's Guide to HCL, Part 2: Mastering the Syntax

Learn the core building blocks of HCL syntax: arguments, blocks, identifiers, comments, and the primitive and complex data types used in modern IaC.

A solid understanding of HCL's fundamental syntax is essential for effectively writing configurations. HCL is designed to be human-readable and writable, built around a few key constructs. More in Part 1.

HCL Arguments and Blocks

HCL syntax is primarily built around two key elements: arguments and blocks.

  • Arguments: An argument assigns a value to a particular name. The syntax is name = expression.
    • image_id = "ami-0c55b31ad54g39a5b" is an example where image_id is the argument name and "ami-0c55b31ad54g39a5b" is its string value.
    • The context where an argument appears (e.g., within a specific resource block) determines the valid value types and whether the argument is required or optional. Many arguments accept arbitrary expressions, allowing values to be literal or programmatically generated.
  • Blocks: A block is a container for other content, including arguments and potentially other nested blocks, creating a hierarchical configuration structure.
    • A block has a type (e.g., resource, source, variable) and can have one or more labels. For example, in resource "aws_instance" "web" {... }, resource is the block type, and "aws_instance" and "web" are labels.
    • The block type defines how many labels are required. For instance, a source block in Packer might expect two labels (e.g., source "amazon-ebs" "example"). Some block types may require no labels.
    • The block body, enclosed in curly braces ({ and }), contains the arguments and nested blocks that define the configuration object.
    • HCL uses a limited number of top-level block types (blocks that can appear outside any other block). Most features in tools like Packer and Terraform (resources, input variables, data sources, etc.) are implemented as top-level blocks.

This structure of arguments and blocks allows for clear and organized configuration files.

HCL Identifiers

Identifiers are names used for arguments, block types, and most tool-specific constructs like resources and variables.

  • They can contain letters, digits, underscores (_), and hyphens (-).
  • The first character of an identifier must not be a digit to avoid ambiguity with literal numbers.
  • Packer, for example, implements the Unicode identifier syntax, extended to include the ASCII hyphen character -.

Consistent and descriptive identifiers are crucial for readability and maintainability.

Data Types in HCL: Primitive and Complex

HCL supports several data types for argument values and expressions. These can be broadly categorized into primitive and complex types.

  • Primitive Types:
    • string: A sequence of Unicode characters representing text, e.g., "hello" or "${var.name}-app". Strings can be defined with double quotes or using a "heredoc" syntax for multi-line strings.
    • number: A numeric value, which can represent both whole numbers (e.g., 15) and fractional values (e.g., 6.283185).
    • bool: A boolean value, which can be either true or false.
  • Complex Types (Collections):
    • list (or tuple): An ordered sequence of values, identified by consecutive zero-based integer indices, e.g., ["us-west-1a", "us-west-1c"] or [var.a, var.b]. Elements can be of mixed types.
    • map (or object): A collection of key-value pairs, where keys are strings and values can be of any type, e.g., { name = "Mabel", age = 52 } or { type = "t2.micro", disk_size = 50 }.
    • set: An unordered collection of unique values where all elements must be of the same type. for_each requires sets when iterating over a flat list of strings.
  • null: A special value representing the absence or omission of a value. If an argument is set to null, the HCL-consuming application (like Terraform or Nomad) typically behaves as if the argument was not set at all, potentially using a default value or raising an error if the argument is mandatory. This is particularly useful in conditional expressions.

HCL-consuming applications like Nomad and Terraform often perform automatic type conversion where possible. For example, if an argument expects a string but is given a number, the number will typically be converted to its string representation.

Table 2: Common HashiCorp HCL Data Types

Type

Description

Example Literal(s)

Key Characteristics

string

Sequence of Unicode characters (text)

"hello", "${var.name}", <<EOF...EOF

Used for names, descriptions, paths. Supports interpolation and heredocs.

number

Numeric value (integer or fractional)

15, 3.14, ${3 * 4}

Used for counts, sizes, ports, etc.

bool

Boolean value

true, false, ${var.enabled}

Used for flags, conditional logic.

list

Ordered sequence of values

["a", "b", 1], [var.a, var.b]

Zero-indexed. Elements can be of mixed types.

map

Collection of key-value pairs (keys are strings)

{ name = "app", version = "1.2" }

Keys must be unique. Values can be of mixed types.

object

A map with a predefined structure and types

(Used in type constraints for variables)

Defines expected attributes and their types for a structured variable.

set

Unordered collection of unique values

toset(["a", "b"])

All elements must be of the same type. Required by for_each for flat lists.

null

Represents absence or omission of a value

null

Makes an argument behave as if it wasn't set.

Documenting Your HCL Code

Comments are crucial for explaining the intent and logic behind HCL configurations. HCL supports three syntaxes:

/*...*/ (Slash-star and star-slash): Delimiters for a multi-line comment.

/*
  This is a multi-line comment.
*/
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16" /* Inline comment */
}

// (Double slash): Also begins a single-line comment. terraform fmt often converts this to #.

// This is also a single-line comment
variable "region" {
  type = string // Describes the AWS region
}

# (Hash symbol): Begins a single-line comment. This is the idiomatic and recommended style.

# This is a single-line comment
resource "aws_instance" "example" { # This comment is at the end of a line
  ami = "ami-12345"
}

Character Encoding and Line Endings

To ensure consistency and avoid parsing issues:

  • Character Encoding: HCL configuration files must always be UTF-8 encoded.
  • Line Endings: HCL accepts Unix-style (LF) or Windows-style (CRLF) line endings. However, the idiomatic style is Unix (LF), and terraform fmt will typically enforce this.

Next up, common issues:

The Developer’s Guide to HCL, Part 3: Common Challenges & Solutions
Solve frequent HCL issues. Learn when to use for_each vs. count, master dynamic blocks, troubleshoot errors, and manage variables and secrets effectively.

Key Sources