Mastering Terraform’s for_each Meta-Argument: Dynamic, Granular Resource Creation
Learn how Terraform’s for_each meta‑argument lets you create multiple, uniquely identified resources with fine‑grained state control. This guide covers syntax, benefits, trade‑offs, and a step‑by‑step verification workflow using AWS security groups.
09 May 2026, 05:20 UTC

Why the Problem Matters
When you’re provisioning dozens of similar resources—say, security groups, IAM roles, or database instances—the most common pain point is repetitive code. A naïve approach is to copy‑paste a resource block and tweak a name, but that quickly becomes unmanageable and error‑prone. Terraform offers two mechanisms for dynamic resource creation: count and for_each. While count is simple, it assigns resources an implicit numeric index, which can break when you need stable identifiers or complex configurations. for_each solves this by letting you iterate over a map or set, giving each instance a meaningful key. The trade‑off is that you must structure your input data carefully and be aware of key stability. Understanding this balance is key to writing clean, maintainable Terraform code.
Thesis: Use for_each when you need distinct identifiers and fine‑grained state control
For scenarios where each resource instance should carry a unique, human‑readable identifier—like a security group name or a database cluster name—for_each provides:
- Explicit keys that survive across runs.
- Granular state updates: adding or removing a key only affects that resource.
- Cleaner modules that adapt to changing input sizes without code changes.
Section 1: Syntax & Basics
The for_each meta‑argument accepts a map or a set of strings. Each key/value pair becomes a separate resource instance. Inside the resource block you can reference the current key through each.key and the value through each.value.
variable \"sg_tags\" {
type = map(string)
default = {
\"frontend\" = \"allow-frontend-traffic\"
\"backend\" = \"allow-backend-traffic\"
\"db\" = \"allow-db-traffic\"
}
}
resource \"aws_security_group\" \"sg\" {
for_each = var.sg_tags
name = \"${each.key}-sg\"
description = each.value
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = \"tcp\"
cidr_blocks = [\"0.0.0.0/0\"]
}
}
Running terraform plan will list a separate security group for each map entry. The resource address becomes aws_security_group.sg[\"frontend\"], which is stable as long as the key doesn’t change.
Section 2: Granular State Management
One of the biggest advantages of for_each is how Terraform handles state changes. Suppose you later decide to remove the \"db\" security group. Updating the variable map and running terraform plan will show only that specific group marked for destruction. All other groups remain untouched. This granular control is invaluable for large infrastructures where a full redeploy would be costly.
- Adding a key creates a new resource instance.
- Removing a key schedules the corresponding resource for destruction.
- Changing a key’s value updates the resource in place (unless the key itself changes).
Because the key is part of the resource address, Terraform can track each instance across runs. If you accidentally change a key—for example, rename \"frontend\" to \"web\"—Terraform will treat it as a new resource and delete the old one. This is a source of accidental recreation, so keys should be stable.
Section 3: Trade‑offs & Limitations
While for_each offers many benefits, it’s not a silver bullet. Here are the key caveats:
- Key Stability: The key used in the map becomes part of the resource address. Changing a key causes Terraform to destroy the old instance and create a new one. Use immutable keys such as resource names or IDs.
- Complex Nested Structures: If you need to iterate over nested maps or lists, you may need to flatten the structure or use temporary locals. This can reduce readability if not documented.
- Count vs. for_each: Some providers or resources expect a numeric index. In those cases
countremains the appropriate choice.
In practice, weigh whether you need stable identifiers. If you’re only provisioning a handful of generic resources, count may be simpler. For dynamic, identifier‑driven provisioning, for_each shines.
Section 4: A Concrete Verification Workflow
Below is a step‑by‑step guide to verify that for_each behaves as expected. This example uses the AWS provider; replace placeholders with your own values.
- Initialize Terraform in the working directory:
terraform init - Plan the initial deployment:
Check that the output lists three security groups:terraform planfrontend-sg,backend-sg, anddb-sg. - Apply the configuration:
Confirm that the resources are created in AWS.terraform apply - Modify the variable map by removing the \"db\" entry:
Runvariable \"sg_tags\" { type = map(string) default = { \"frontend\" = \"allow-frontend-traffic\" \"backend\" = \"allow-backend-traffic\" } }terraform planagain. Terraform should show thataws_security_group.sg[\"db\"]will be destroyed, while the other two remain. - Inspect state to confirm mapping:
You should see three entries, one per key. Useterraform state listterraform state show aws_security_group.sg[\"frontend\"]to view attributes for a specific instance.
These steps demonstrate that for_each provides predictable, fine‑grained control over resource lifecycle.
Actionable Closing
When building reusable modules or scaling infrastructure, adopt for_each to:
- Eliminate copy‑paste code and reduce maintenance overhead.
- Ensure each instance has a stable, meaningful identifier.
- Gain granular control over state changes, minimizing unintended recreation.
Remember to keep your map keys immutable and document any complex transformations. With these practices, for_each becomes a powerful tool in your Terraform arsenal.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.