A resource needs a variable number of identical nested blocks (e.g. ingress rules) driven by a list/map.
→Use a `dynamic` block whose `for_each` iterates the collection; reference each element via the block iterator (default name = block label) inside `content {}`.
Why: Dynamic blocks generate repeated nested blocks without copy-paste; the iterator keeps each generated block bound to its source element.
Reference↗
Create one resource per entry in a map of objects, keyed stably so reordering never forces replacement.
→Set `for_each = var.objects` (a map). Use `each.key` for the stable key and `each.value.<attr>` for fields. Avoid `count` here — index shifts cause churn.
Why: Map keys are stable identities in state; list indices are positional and shift when elements are added/removed.
Decide between count and for_each for multiple instances.
→Use `for_each` when instances have distinct identities (a set/map); use `count` only for N identical, order-insensitive copies. Prefer for_each for anything that may grow/shrink.
Why: for_each addresses by key (resource["key"]); count addresses by index (resource[0]) which reshuffles on insertions/deletions.
An input variable is an object where some attributes are optional and need defaults.
→Type it as `object({ name = string, size = optional(number, 10) })`. `optional(type, default)` supplies the default when the caller omits the attribute.
Why: optional() with a default keeps callers terse while guaranteeing a concrete value downstream — no null-handling everywhere.
Reference↗
Validate an assumption about a resource before applying, or guarantee a result after.
→Use `lifecycle { precondition { ... } }` to assert inputs before create/update, and `postcondition` to assert outputs after. Both take `condition` + `error_message`.
Why: Custom conditions fail fast with a clear message instead of producing a broken apply or a confusing downstream error.
Reference↗
A resource must be recreated whenever another resource or attribute changes.
→Add `lifecycle { replace_triggered_by = [aws_x.y.id] }`. When the referenced value changes, Terraform forces replacement of this resource.
Why: Expresses a replacement dependency declaratively, avoiding manual `-replace` on every related change.
Replacing a resource causes downtime because the old one is destroyed before the new one exists.
→Set `lifecycle { create_before_destroy = true }` so Terraform provisions the replacement first, then destroys the old. Ensure unique names/no hard conflicts.
Why: Zero-downtime replacement; but watch for name collisions and quota limits while both exist briefly.
Reject invalid input values early (e.g. an environment that is not dev/stage/prod).
→Add a `validation { condition = contains(["dev","stage","prod"], var.env), error_message = "..." }` block to the variable.
Why: Catches bad input at plan time with a readable message instead of failing deep in a provider call.
Reference a value that may not exist without crashing the plan.
→Use `try(local.maybe.value, "default")` to fall back on errors, or `can(expr)` to get a boolean of whether an expression succeeds.
Why: Graceful handling of optional/variable-shaped data; avoids "Error: Unsupported attribute" on absent keys.
Transform a list into a map, or filter/shape a collection for a resource argument.
→Use a `for` expression: `{ for u in var.users : u.name => u.role if u.active }` (map) or `[for x in list : upper(x)]` (list).
Why: for expressions are the idiomatic way to reshape data; the `if` clause filters, `k => v` form builds maps.
A variable or output contains a secret that should not print in plan/apply output.
→Mark the variable `sensitive = true` (and outputs too). Terraform redacts it in CLI output, though it is still stored in state.
Why: Prevents accidental disclosure in logs/CI output; state itself must still be protected (encrypted backend, access control).
Manage resources in two regions/accounts within one configuration.
→Declare aliased providers (`provider "aws" { alias = "west" region = "us-west-2" }`) and set `provider = aws.west` on resources or pass into modules.
Why: Aliases let one config target multiple provider instances; modules receive them explicitly via the `providers` argument.
A hidden dependency (not expressed through references) causes ordering problems.
→Add `depends_on = [aws_iam_role_policy.x]` to force ordering. Use sparingly — prefer implicit dependencies via attribute references.
Why: Explicit depends_on handles dependencies the graph cannot infer, but overuse creates conservative, slower plans.
Render a config file/user-data from a template with structured variables.
→Use `templatefile("${path.module}/tpl.tftpl", { items = local.items })`; the template uses `%{ for }` / `${}` interpolation.
Why: templatefile keeps rendering pure/at plan time (unlike the deprecated template provider) and supports loops/conditionals.
Build a flat list of every (subnet, rule) combination to feed a single for_each.
→Use `setproduct(var.subnets, var.rules)` for the cross-product, or `flatten([for ...])` to collapse nested lists into one.
Why: These functions turn nested data into the flat, uniquely-keyable collection for_each requires.