Skip to main content
RunBook Academy

TerraformIV · HCL: The Terraform Configuration LanguageHCL

HCL Fundamentals

Foundation⏱ ~24 min🧪 Lab requiredbashterraform

What you'll learn

  • Read and write the basic HCL block structure
  • Identify the value types: string, number, bool, list, map, set, object, tuple, null
  • Use references to read attributes from resources and variables
  • Recognise the expressions Terraform supports and which to avoid

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12

Not yet marked complete on this device.

HCL is the language the configuration is written in. The course teaches enough HCL to read and write Terraform confidently. It deliberately does not teach every HCL feature — most production configurations use a small subset of the language.

The block structure

HCL has exactly three block types:

# Top-level configuration
terraform {
  required_version = ">= 1.9.0"
}

# Provider configuration
provider "aws" {
  region = "us-east-1"
}

# Resource declaration
resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"
}

# Data source
data "aws_ami" "ubuntu" {
  most_recent = true
  # ...
}

# Module call
module "network" {
  source = "./modules/network"
}

# Variable
variable "environment" {
  type    = string
  default = "dev"
}

# Output
output "instance_id" {
  value = aws_instance.web.id
}

# Local
locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

The blocks are not interchangeable. A resource block declares a resource; a data block reads from the provider; a variable block declares an input. The block type is structural; the provider documentation is the authority for what arguments are valid inside each block.

Argument value types

HCL has nine value types:

TypeExampleNotes
string"hello", "${var.environment}"Double-quoted; supports interpolation
number42, 3.14No thousand separators
booltrue, falseNot 1 / 0
list(...)["a", "b", "c"]Ordered, indexable
map(...){key = "value"}Key-value pairs
settoset(["a", "b"])Unordered, no duplicates
object({key = "value"})Structured record
tuple["a", 1, true]Heterogeneous list
nullnullThe absence of a value

Two of these deserve particular attention:

list vs set — a list is ordered, a set is unordered. The state records the order of a list (so the same code always produces the same real-world resources); for a set, order is unstable. Use for_each over a set, not count, to avoid sensitive-to-order bugs.

object vs map — an object has typed fields; a map has homogeneous values. Use object when the structure matters (“a name and a port, not a name and a port and a tag and a subnet”). Use map when the structure is uniform (“all values are strings”).

References

A reference is a way to read an attribute from another object:

resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"
  subnet_id     = aws_subnet.public.id           # reference to another resource
  vpc_security_group_ids = [
    aws_security_group.web.id,
  ]
}

output "private_ip" {
  value = aws_instance.web.private_ip             # attribute of the resource
}

output "ami_id" {
  value = data.aws_ami.ubuntu.id                  # attribute of a data source
}

output "db_endpoint" {
  value = module.database.endpoint                # output of a module
}

The reference aws_instance.web.id is the resource address of the aws_instance.web resource, followed by the attribute name. The full resource address is the basis for terraform state commands and the resource graph.

A reference creates an implicit dependency. Terraform will not create the aws_instance.web until the aws_subnet.public is created. The dependency graph is built from the references.

String interpolation

name = "web-${var.environment}"

The ${...} syntax is template interpolation. The result is a string. The expression inside ${...} can be any valid HCL expression.

subnet_cidr = "10.0.${var.az_number}.0/24"

Moustache-style references ({{...}}) are not supported in HCL.

Conditionals

instance_type = var.environment == "prod" ? "t3.large" : "t3.small"

The conditional expression condition ? value_if_true : value_if_false is a one-line if. Use it sparingly — deeply nested conditionals are hard to read.

For expressions

# List comprehension
upper_names = [for n in var.names : upper(n)]

# Map comprehension
name_by_id = { for n in var.names : n.id => n.name }

# Filter
dev_names = [for n in var.names : n if n.environment == "dev"]

For expressions are powerful. They are also a frequent source of hard-to-read code. The course recommends:

  • Use comprehensions for simple transformations.
  • Use a for loop in code generation (a separate script) for complex transformations.
  • If the comprehension is more than one line, prefer a for loop or a module.

Functions

HCL has a large standard library of functions:

name = upper(var.name)
endpoint = "https://${var.subdomain}.example.com"
ports = [for p in var.ports : tonumber(p)]

The full list is in the Terraform documentation. A few worth knowing:

  • lookup(map, key, default) — look up a key in a map with a default.
  • merge(map1, map2, ...) — merge maps; later maps take precedence.
  • concat(list1, list2, ...) — concatenate lists.
  • flatten(list) — flatten nested lists.
  • format("...", args) — format a string.
  • jsonencode(value) / jsondecode(string) — JSON serialisation.
  • try(expr, default) — try an expression and return a default on failure.

Conditional resource creation

resource "aws_instance" "web" {
  count = var.create_instance ? 1 : 0

  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"
}

count and for_each are not value types — they are meta-arguments that control how many instances of a resource are created. The course has a dedicated lesson on count and for_each.

The style guide

The official Terraform style guide is short:

  • Two-space indentation.
  • One blank line between blocks.
  • Resource and variable names in snake_case.
  • Resource names should be descriptive (“web” instead of “i”).
  • Argument order should be consistent.
# Good
resource "aws_instance" "web" {
  ami           = "ami-0e1bed4f"
  instance_type = "t3.medium"
  tags = {
    Name = "web-${var.environment}"
  }
}

# Less good
resource "aws_instance" "web" {
ami="ami-0e1bed4f"
instance_type = "t3.medium"
tags = {
  Name = "web-${var.environment}"
}
}

terraform fmt enforces most of the style guide. Run it on every commit.

What to avoid

A few patterns that should be avoided in production:

Magic strings. A literal string in a configuration that is used in multiple places. Replace with a variable.

# Avoid
resource "aws_instance" "web" {
  instance_type = "t3.medium"
  tags = {
    Environment = "production"
  }
}

# Prefer
variable "instance_type" {
  type    = string
  default = "t3.medium"
}

variable "environment" {
  type = string
}

resource "aws_instance" "web" {
  instance_type = var.instance_type
  tags = {
    Environment = var.environment
  }
}

Deeply nested expressions. A one-liner that reads flatten([for m in var.modules : keys(m)]) is hard to read. Prefer a local or a for loop.

Clever use of lookup and coalesce. A default value that hides a missing key is a debugging nightmare. Let the plan fail if the configuration is wrong.

Recursion. Terraform does not allow recursive modules. The reason is that the resource graph must be a DAG. If you find yourself needing recursion, you are probably modelling the wrong thing.

What comes next

The next lesson is expressions in depth: most of what you read in a Terraform configuration is an expression. The expressions lesson is the second half of the HCL material.

Knowledge check · 7 questions

  1. Q1. Which is a valid HCL block type?

  2. Q2. How do you reference an attribute of a resource?

  3. Q3. In HCL, `null` and `undefined` are the same value.

  4. Q4. What is the role of `count` in a resource block?

  5. Q5. Which of the following are valid HCL value types? (Select all that apply.)

  6. Q6. What is the role of the `for` expression in HCL?

  7. Q7. A configuration has `[for s in var.list : upper(s)]` in an output. What does this do?

Passing score: 75%. Answers are checked in this browser.