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 /* */.
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.
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 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 addresses become local_file.server_config["web"], ["api"], ["worker"] — keyed by a stable name, not a position.
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.
The lifecycle Block
A meta-argument block controlling special-case behavior for how a resource is created, updated, or destroyed:
create_before_destroy— builds the replacement before tearing down the original, avoiding downtime on a forced replacementprevent_destroy— makesterraform destroy(or a plan that would destroy this resource) fail outright, a safety rail for genuinely critical resourcesignore_changes— tells Terraform to stop reporting drift on specific attributes, e.g. tags a separate system updates automatically
for_each's stable, name-based addressing avoids the reordering trap count can cause.
Hands-On Exercises
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 solutionA 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.
Explain what prevent_destroy protects against, and describe a concrete real-world resource where it's genuinely worth setting.
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
- lifecycle —
create_before_destroy,prevent_destroy,ignore_changes