Terraform — Part 9 — Meta-Arguments: count, for_each, depends_on, lifecycle
Part 4 previewed these four as arguments Terraform itself interprets rather than the provider. This part gives each one the depth it needs, including the count-vs-for_each trade-off that trips up nearly every newcomer.
count and for_each both create multiple instances of a resource; depends_on forces an ordering Terraform couldn’t infer; lifecycle changes how Terraform treats replacement, destruction, and drift for one resource.
Creates N near-identical instances, addressed by numeric index.
Creates one instance per map or set entry, addressed by a stable key.
A nested block controlling replacement order, drift, and accidental deletion.
Introduction
Part 4 named these four as special because Terraform itself interprets them on almost any resource, regardless of provider. Naming them wasn’t the same as explaining them — this part does that properly, one at a time.
count: Multiple Instances by Number
count takes a whole number and creates that many near-identical instances of a resource:
resource "random_pet" "server" {
count = 3
length = 2
}
Inside the block, count.index gives the current instance’s position, starting at 0 — useful for making each instance slightly different:
resource "random_pet" "server" {
count = 3
length = 2
prefix = "server-${count.index}"
}
Individual instances are addressed with bracket notation: random_pet.server[0], random_pet.server[1], random_pet.server[2]. The bare name random_pet.server without a bracket now refers to the whole set.
count are identified purely by their numeric position. Remove or reorder an item in the middle of whatever list drives your count, and every instance after that point shifts index — which Terraform reads as “destroy this, create that,” not “nothing changed here.”
for_each: Multiple Instances by Key
for_each solves exactly that problem by creating one instance per entry in a map or set, addressed by a stable key instead of a position:
resource "random_pet" "server" {
for_each = toset(["web", "api", "worker"])
length = 2
prefix = each.key
}
Inside the block, each.key gives the current map key or set member, and each.value gives the corresponding map value — for a set, both are identical, since a set has no separate value. Instances are addressed by that key rather than a position: random_pet.server["web"], random_pet.server["api"], random_pet.server["worker"].
Removing "api" from that set now does exactly what you’d expect: Terraform destroys only the "api" instance. "web" and "worker" are untouched, because their keys never changed.
count vs for_each: Which One to Reach For
| Situation | Use |
|---|---|
| Truly identical instances, count driven by a plain number | count |
| Instances that need stable identity as the set changes over time | for_each |
| Each instance needs distinct, meaningful configuration (not just an index) | for_each |
| List of items might be reordered or have entries removed from the middle | for_each |
In practice, for_each is the safer default for anything beyond a fixed, truly interchangeable count — the stable-key addressing avoids a whole category of unnecessary destroy-and-recreate churn that count is prone to as configuration evolves.
depends_on: Forcing a Hidden Dependency
Part 4 showed that referencing another resource’s attribute automatically creates a dependency — Terraform sees the reference and orders operations correctly on its own. depends_on exists for the narrower case where a resource depends on another resource’s behaviour, without referencing any of its actual data:
resource "aws_instance" "app" {
depends_on = [aws_iam_role_policy.app_policy]
# ...
}
HashiCorp’s own guidance is direct about when to use it: only when a resource relies on another’s behaviour but doesn’t access any of its data through arguments — and even then, as a last resort. Overusing depends_on makes Terraform’s plans more conservative than they need to be, sometimes replacing resources that didn’t actually need to change, simply because the forced dependency link doesn’t carry the fine-grained “which specific value matters” information a real reference does.
random_pet.server.id example did. Only fall back to depends_on when there’s genuinely no attribute to reference and the ordering still matters.
lifecycle: Changing How Terraform Treats One Resource
Unlike the other three, lifecycle is a nested block, not a simple argument, and it holds four settings that change how Terraform manages that specific resource:
Creates the replacement before destroying the original, for cases where the API can’t update in place.
Makes Terraform refuse to run any plan that would destroy this resource, with an explicit error.
Tells Terraform to stop proposing updates for specific attributes, letting an external process manage them.
A fourth, replace_triggered_by, forces a resource to be replaced whenever a referenced resource or attribute changes, even if nothing in this resource’s own configuration did.
resource "aws_instance" "critical_db" {
# ...
lifecycle {
prevent_destroy = true
}
}
prevent_destroy is one of the cheapest safeguards available for anything genuinely expensive to lose — a database, a piece of stateful storage — and it costs nothing until the day someone’s plan would have destroyed it by mistake.
Quick Reference Summary
| Meta-Argument | Purpose |
|---|---|
count |
N instances, addressed by numeric index. |
for_each |
One instance per map/set entry, addressed by stable key. |
depends_on |
Forces an ordering dependency with no data reference to infer it from. Last resort. |
lifecycle |
Nested block: replacement order, deletion protection, drift ignoring, forced replacement. |
Final Thoughts
These four cover most of the situations where “just write a resource block” isn’t quite enough — multiple instances, ordering that isn’t obvious from data references, or a resource that needs special handling on replacement or deletion. None of them are needed until they’re needed; reaching for them prematurely, especially depends_on, usually adds complexity without adding safety.
for_each over count for anything beyond a fixed interchangeable quantity, default to a real reference over depends_on, and reach for lifecycle specifically when a resource’s replacement or deletion behaviour needs to differ from Terraform’s default.
Terraform — Part 10 — Modules: Building Reusable Infrastructure. We cover how to package a group of resources into something callable, versionable, and reusable across environments.