HCL

HashiCorp Configuration Language - the declarative syntax powering Terraform, Packer, Vault, and Consul.

Block Structure

Resource block
resource "proxmox_vm_qemu" "web" {
  name        = "web-01"
  target_node = "pve"
  cores       = 2
  memory      = 4096
}
Data source block
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
  }
}
Module block
module "network" {
  source = "./modules/network"

  vpc_cidr    = "10.0.0.0/16"
  environment = var.environment
}

STRUCTURE: HCL is blocks all the way down: type "label" "name" { body }.

Types & Variables

Primitive types
variable "name" {
  type    = string
  default = "web-01"
}

variable "count" {
  type    = number
  default = 3
}

variable "enabled" {
  type    = bool
  default = true
}
Collection types
variable "tags" {
  type = map(string)
  default = {
    Environment = "dev"
    Team        = "infra"
  }
}

variable "ports" {
  type    = list(number)
  default = [80, 443, 8080]
}

variable "servers" {
  type = list(object({
    name  = string
    cores = number
    memory = number
  }))
}

variable "cidrs" {
  type    = set(string)
  default = ["10.0.1.0/24", "10.0.2.0/24"]
}
Type constraints with validation
variable "environment" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Must be dev, staging, or prod."
  }
}

variable "disk_size" {
  type = number
  validation {
    condition     = var.disk_size >= 10 && var.disk_size <= 500
    error_message = "Disk size must be between 10 and 500 GB."
  }
}

Expressions

String interpolation
name = "${var.prefix}-${var.environment}-web"
Heredoc
description = <<-EOT
  This is a multi-line
  description that gets
  dedented automatically.
EOT
Conditional
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
For expressions
# List comprehension
upper_names = [for s in var.names : upper(s)]

# Filtered list
prod_servers = [for s in var.servers : s.name if s.env == "prod"]

# Map comprehension
name_to_ip = { for s in var.servers : s.name => s.ip }

# Map with grouping
by_env = { for s in var.servers : s.env => s.name... }
Splat expressions
# Equivalent to [for o in var.list : o.id]
ids = var.list[*].id

Built-in Functions

String functions
upper("hello")                  # "HELLO"
lower("HELLO")                  # "hello"
title("hello world")            # "Hello World"
trimspace("  hello  ")          # "hello"
replace("hello", "l", "L")     # "heLLo"
substr("hello", 0, 3)          # "hel"
join("-", ["a", "b", "c"])      # "a-b-c"
split(",", "a,b,c")             # ["a", "b", "c"]
format("Hello, %s!", "world")   # "Hello, world!"
Collection functions
length(var.list)
contains(var.list, "value")
concat(list1, list2)
flatten([["a"], ["b", "c"]])    # ["a", "b", "c"]
keys(var.map)
values(var.map)
merge(map1, map2)               # map2 wins on conflict
lookup(var.map, "key", "default")
element(var.list, 0)
slice(var.list, 0, 3)
sort(var.list)
distinct(var.list)
Numeric and logic
min(5, 12, 3)                   # 3
max(5, 12, 3)                   # 12
ceil(4.1)                       # 5
floor(4.9)                      # 4
abs(-5)                         # 5
coalesce("", "fallback")        # "fallback"
try(var.optional, "default")    # Safe access
Filesystem
file("${path.module}/script.sh")
filebase64("${path.module}/binary")
templatefile("${path.module}/tmpl.tftpl", { name = "web" })
Encoding
jsonencode({ name = "web", port = 80 })
yamlencode({ name = "web" })
base64encode("hello")
base64decode("aGVsbG8=")
Network
cidrsubnet("10.0.0.0/16", 8, 1)   # "10.0.1.0/24"
cidrhost("10.0.1.0/24", 5)         # "10.0.1.5"
cidrnetmask("10.0.1.0/24")         # "255.255.255.0"

Locals & Patterns

Computed locals
locals {
  environment = terraform.workspace
  name_prefix = "${var.project}-${local.environment}"

  common_tags = {
    Project     = var.project
    Environment = local.environment
    ManagedBy   = "terraform"
  }

  # Merge base tags with resource-specific tags
  all_tags = merge(local.common_tags, var.extra_tags)
}

resource "aws_instance" "web" {
  tags = local.all_tags
}
Dynamic blocks
resource "aws_security_group" "web" {
  name = "${local.name_prefix}-web-sg"

  dynamic "ingress" {
    for_each = var.allowed_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

PATTERN: locals for computed values and DRY tags. variables for user input.

Quick Reference

Pattern Description

variable "name" { type = string }

Declare input variable

locals { key = "value" }

Define local values

output "name" { value = var.x }

Expose output value

for_each = toset([…​])

Iterate over set of strings

count = var.enabled ? 1 : 0

Conditional resource creation

dynamic "block" { …​ }

Generate repeated nested blocks

"${var.name}-suffix"

String interpolation

[for s in var.list : upper(s)]

List comprehension (for expression)

{ for k, v in var.map : k ⇒ upper(v) }

Map comprehension

try(var.x, "default")

Safe access with fallback