HCL Blocks, Expressions, and Functions

Scenario: A configuration repeats the same environment name and CIDR range in a dozen places, until a rename touches eleven of the twelve and produces subtly inconsistent resources.

HCL (HashiCorp Configuration Language) is built from two ingredients: blocks (the resource ".." ".." { } structures you've already seen, which declare what exists) and expressions (the values and calculations inside those blocks, which describe what those things should look like). Expressions can be simple literals ("prod", 443), references to other values (var.environment, fakecloud_network.app.id), or transformations using built-in functions and constructs: conditionals (condition ? true_val : false_val), for expressions that loop over a collection to build a new one, splat expressions (resource..id) that pull one attribute out of every instance of a resource, and string templates ("${var.env}-app"). This is deliberately a limited* toolkit - HCL is not a general-purpose programming language, and Terraform authors resist the temptation to turn it into one, because a plan needs to stay something a human reviewer can actually predict by reading it.

Complex data has real semantics you need to know before reaching for it: a list is ordered and can hold duplicates; a set is unordered and automatically discards duplicates; a map associates string keys with values of one consistent type; an object has named, individually-typed attributes (like a small struct); a tuple is an ordered sequence where each position can have its own type. Converting a list to a set with toset() is a real semantic change, not just a type formality - order and duplicate information are genuinely thrown away.

Analogy: Expressions in HCL are like formulas in a spreadsheet layered over facts about your infrastructure. A well-named formula (=SUM(revenue_row)) removes repetitive manual addition; a deeply nested formula that references six other nested formulas becomes something nobody can audit at a glance - at that point you've quietly built a second programming language inside your spreadsheet, just a much less debuggable one.

A worked configuration

locals {
  prefix = "${var.environment}-${var.service}"
  ports  = toset([443, 8443, 443])
}

resource "fakecloud_server" "api" {
  name = local.prefix
  tags = merge(var.common_tags, { Service = var.service })
}

Before embedding an unfamiliar function or transformation directly into a resource block, open terraform console (an interactive REPL that evaluates HCL expressions against your current configuration) and test the exact expression in isolation first. In real teams, keep business logic and important policy decisions visible in named locals rather than buried inside one dense one-line expression - a plan reviewer six months from now, possibly on-call and stressed, needs to be able to read local.effective_tags and understand what it means without decoding a function-call pyramid.

Common mistakes in production

Goal: Treat the plan as a change contract: understand every create, update, replacement, and destroy before approving it. Reinforce this lesson in the tf-count-for-each Terraform lab. Open /labs/terraform and choose slug tf-count-for-each; the lab runs real Terraform against the offline FakeCloud provider.