We are still cooking the magic in the way!
Meta-Arguments: count, for_each & lifecycle
Meta-Arguments: count, for_each & lifecycle
Every resource block in Terraform, by default, manages exactly one infrastructure object. The moment you need ten security group rules, five S3 buckets, or a fleet of IAM users, repeating resource blocks is not an option — it destroys readability and makes the configuration drift-prone. Meta-arguments are special arguments recognised by Terraform itself (not the provider) that change how a resource behaves: how many copies exist, which set of values drives each copy, and what rules govern creation, deletion, and replacement. At Google, Stripe, and Cloudflare, correct use of count, for_each, and lifecycle is a prerequisite for writing any production module.
count: Simple Numeric Replication
count takes a non-negative integer and tells Terraform to create that many identical (or near-identical) copies of the resource. Each instance is addressed as resource_type.resource_name[index], with count.index available inside the block to differentiate instances.
count, Terraform identifies each instance by its numeric index. If you remove an element from the middle of a list (e.g., you had 5 instances and remove index 2), Terraform re-indexes everything above the removed element. This causes it to destroy and recreate instances 2, 3, and 4 — even though you only wanted to remove one. For any resource whose identity matters (EC2, RDS, IAM user), prefer for_each with a set or map so each instance has a stable string key.for_each: Key-Driven Replication
for_each accepts either a set(string) or a map(any) and creates one resource instance per element. Each instance is addressed as resource_type.resource_name["key"]. Because instances are keyed by string rather than integer, adding or removing one element only affects that specific instance — all other instances remain untouched. This is the production-safe default for all non-trivial multi-resource patterns.
list(string), convert it before passing to for_each: for_each = toset(var.my_list). For maps derived from complex objects, use a for expression in a local: local.user_map = { for u in var.users : u.name => u } then for_each = local.user_map. Never pass a list directly — Terraform will error.lifecycle: Controlling Creation, Deletion, and Replacement
The lifecycle block sits inside any resource and overrides Terraform's default behaviour for that resource's life cycle events. It has four arguments: create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by. Getting these right is the difference between a zero-downtime deployment and a 3 AM incident.
create_before_destroy
By default, when Terraform must replace a resource (a change forces a new resource — e.g., changing an AMI or a launch template image), it destroys the old resource first, then creates the new one. This means a brief gap in capacity. For load-balanced fleets, TLS certificates, and IAM roles with policy attachments, this gap is unacceptable. create_before_destroy = true reverses the order: Terraform creates the replacement, then destroys the original once the replacement is confirmed.
create_before_destroy = true, the new resource must exist simultaneously with the old one. AWS requires unique names for most resources. Use name_prefix instead of name so Terraform can generate a unique suffix for the new resource. With a fixed name, the create step fails because the name is still taken by the old resource.prevent_destroy
prevent_destroy = true causes Terraform to error — and halt the plan — if the plan would destroy that resource. This is a last-resort guard for resources that must never be accidentally deleted: production RDS clusters, S3 buckets with compliance data, KMS keys, and Elasticsearch domains. It is a code-level safeguard, not a permissions safeguard — a determined operator can remove the block and re-run. Layer it with AWS resource policies and SCPs for defence in depth.
ignore_changes
ignore_changes tells Terraform to stop tracking drift on specific attributes. The canonical use case is when an external system (an autoscaler, a human operator, a configuration management tool) legitimately changes an attribute after Terraform creates the resource. Without ignore_changes, Terraform would detect the drift on every plan and revert it — breaking the external system's changes. Common attributes: desired_capacity on an ASG, ami when images are rotated by an external process, and tags when a tag policy injects cost-allocation tags outside Terraform.
create_before_destroy = true (no downtime), replace_triggered_by = [aws_launch_template.web] (automatic cascade), and ignore_changes = [desired_capacity] (respect the autoscaler). This trio is the standard pattern in platform engineering teams at companies like Airbnb, Lyft, and GitHub. Trying to manage fleet rotations without these arguments leads to manual taint cycles and scheduled downtime windows.Conditional Resources with count
A common pattern is count = var.condition ? 1 : 0 to conditionally create a resource. This is the only Terraform-idiomatic way to express "maybe create this resource". When the count is 0, Terraform manages no instances and the resource is effectively absent. When referencing such a conditional resource from another resource, use one(resource_type.name[*].attribute) to safely extract the value (returns null when count is 0, rather than erroring).
for_each map or set are known at plan time. If the key comes from a resource attribute not yet created (e.g., a dynamically assigned ID), Terraform errors with "The set of keys cannot be determined until apply." The solution is to use known values as keys — names, slugs, and static identifiers — not computed IDs. If you must use a computed value, fall back to count with a length(), accepting the index-stability trade-off.