The Terraform Language In Depth

Terraform / Infrastructure as Code

Chapter 3 · The Terraform Language In Depth

Chapter 2 wrote one resource in isolation. Real infrastructure is many resources referencing each other — this chapter covers how HCL expresses those relationships, and the meta-arguments that control how resources are created.

HCL Syntax Building Blocks

A block has a type, zero or more labels, and a body of arguments: resource "type" "name" { key = value }. Values can be strings, numbers, booleans, lists (["a", "b"]), or maps ({ key = "value" }). String interpolation embeds an expression inside a string with ${...}, and comments use # or /* */.

variable "env" { default = "dev" } resource "local_file" "config" { # interpolation combines a literal string and an expression filename = "${path.module}/${var.env}-config.txt" }

Data Sources

A resource block creates and manages something. A data block only reads something that already exists — infrastructure this configuration doesn't own, like a shared VPC created elsewhere, or the latest AMI ID published by a cloud provider.

data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] } resource "aws_instance" "web" { ami = data.aws_ami.ubuntu.id }

The Implicit Dependency Graph

data.aws_ami.ubuntu.id above is a reference — and referencing one resource's attribute from inside another is exactly what tells Terraform "create/read this one first." Terraform builds this into a full dependency graph (a DAG) automatically, from every reference in the configuration, and applies resources in the correct order — running independent resources in parallel where nothing connects them. File order never matters — only the actual references do.

count

Creates multiple copies of a resource from a single block, indexed numerically via count.index.

resource "local_file" "server_config" { count = 3 filename = "${path.module}/server-${count.index}.txt" }

Resource addresses become local_file.server_config[0], [1], [2].

for_each

Creates multiple copies keyed by a map or set of strings, via each.key/each.value — the modern default for anything beyond a fixed, never-changing count.

resource "local_file" "server_config" { for_each = toset(["web", "api", "worker"]) filename = "${path.module}/${each.key}.txt" }

Resource addresses become local_file.server_config["web"], ["api"], ["worker"] — keyed by a stable name, not a position.

count vs. for_each — removing from the middle
With count = 3, removing the resource at index 1 shifts index 2 down to fill the gap — Terraform sees this as "destroy index 1 AND index 2, then recreate a new index 1," not "destroy the one item that was actually removed." With for_each, deleting "api" from the set only ever affects the resource keyed "api" — the other two are untouched, because their addresses were never based on position in the first place.

depends_on

For the rare case where one resource genuinely depends on another but no attribute reference exists to express it (e.g. IAM permission propagation delays), depends_on states the dependency explicitly. Used sparingly — an implicit reference is almost always preferable when one is available, since it stays correct even if the configuration is later refactored.

resource "aws_instance" "web" { depends_on = [aws_iam_role_policy.web_policy] }

The lifecycle Block

A meta-argument block controlling special-case behavior for how a resource is created, updated, or destroyed:

resource "aws_instance" "web" { lifecycle { create_before_destroy = true prevent_destroy = true ignore_changes = [tags] } }
  • create_before_destroy — builds the replacement before tearing down the original, avoiding downtime on a forced replacement
  • prevent_destroy — makes terraform destroy (or a plan that would destroy this resource) fail outright, a safety rail for genuinely critical resources
  • ignore_changes — tells Terraform to stop reporting drift on specific attributes, e.g. tags a separate system updates automatically
Default to for_each
Unless a collection is guaranteed to always be exactly N items with no individual identity, for_each's stable, name-based addressing avoids the reordering trap count can cause.

Hands-On Exercises

Exercise 1

Explain why referencing one resource's attribute inside another resource's argument is what creates Terraform's dependency graph, and why the order the blocks appear in the file doesn't matter.

📄 View solution
Exercise 2

A team manages 3 identical resources with count = 3 and needs to delete only the middle one (index 1). Explain what Terraform plans to do to the other two resources, and why for_each avoids this problem.

📄 View solution
Exercise 3

Explain what prevent_destroy protects against, and describe a concrete real-world resource where it's genuinely worth setting.

📄 View solution

Chapter 3 Quick Reference

  • data — reads existing infrastructure; never creates or manages it
  • Dependency graph — built from references (resource.attribute), not file order
  • count — numeric index, position-based addresses, reordering risk
  • for_each — key-based addresses, stable under add/remove — the modern default
  • depends_on — explicit dependency, used only when no implicit reference exists
  • lifecyclecreate_before_destroy, prevent_destroy, ignore_changes