Terraform `for_each` vs `count` Prompt
Choose between for_each and count for resource iteration — key stability, addition/removal behavior, when to use each.
- Target user
- Terraform engineers iterating resources
- Difficulty
- Intermediate
- Tools
- Claude, ChatGPT
The prompt
You are a senior Terraform engineer who has explained for_each vs count countless times — addressing key stability and refactoring. I will provide: - The iteration use case - Current implementation - Symptom (resources destroyed on list change, address weirdness) Your job: 1. **count basics**: - Indexes: `[0]`, `[1]`, ... - Addresses by index - Removing middle element shifts indexes - Causes destroys/recreates downstream 2. **for_each basics**: - Uses map keys or set members - Addresses by key: `["a"]`, `["b"]` - Removing one doesn't affect others - Stable across changes 3. **When count works**: - Fixed number, order matters - Or, simple case with no removal - Or, conditional via `count = condition ? 1 : 0` 4. **When for_each works** (usually preferred): - Variable-length collections - Items can be added/removed mid-list - Keys carry meaning 5. **For converting count → for_each**: - Use `moved` block per index - Map old index to new key 6. **For conditional resources**: - `count = condition ? 1 : 0` is OK - Or: `for_each = condition ? toset(["this"]) : toset([])` 7. **For maps of objects**: - for_each with rich keys - Per-key configuration 8. **For nested loops**: - dynamic blocks inside resources - Combined with for_each / count Mark DESTRUCTIVE: removing middle item from count list (destroys + recreates trailing), changing for_each key (destroys + recreates), key collisions. --- Iteration use case: [DESCRIBE] Current implementation: [PASTE] Symptom: [DESCRIBE]
Why this prompt works
Wrong choice causes destroy/recreate storms. This prompt walks tradeoffs.
How to use it
- Default to for_each.
- count for simple/conditional.
- Stable keys are essential.
- Use moved for refactor.
Patterns
Bad: count with list that changes
variable "users" {
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "users" {
count = length(var.users)
name = var.users[count.index]
}
# Addresses:
# aws_iam_user.users[0] = alice
# aws_iam_user.users[1] = bob
# aws_iam_user.users[2] = charlie
# If you remove "bob": ["alice", "charlie"]
# Addresses:
# aws_iam_user.users[0] = alice (unchanged)
# aws_iam_user.users[1] = charlie (was bob; now charlie → destroy bob, recreate as charlie? Actually: shifts index, destroy charlie@2, recreate charlie@1)
# DISASTROUS
Good: for_each with set/map
variable "users" {
default = ["alice", "bob", "charlie"]
}
resource "aws_iam_user" "users" {
for_each = toset(var.users)
name = each.value
}
# Addresses:
# aws_iam_user.users["alice"]
# aws_iam_user.users["bob"]
# aws_iam_user.users["charlie"]
# Remove "bob":
# Only aws_iam_user.users["bob"] is destroyed
# alice, charlie unchanged
Map of objects (rich for_each)
variable "users" {
default = {
alice = { uid = 1001, groups = ["admin"] }
bob = { uid = 1002, groups = ["developer"] }
charlie = { uid = 1003, groups = ["developer", "admin"] }
}
}
resource "aws_iam_user" "users" {
for_each = var.users
name = each.key
tags = {
UID = each.value.uid
}
}
# each.key = username; each.value = config object
Conditional resource
# count is fine here (0 or 1)
resource "aws_security_group" "extra" {
count = var.create_extra_sg ? 1 : 0
name = "extra"
}
# Or for_each
resource "aws_security_group" "extra" {
for_each = var.create_extra_sg ? toset(["this"]) : toset([])
name = "extra"
}
Migrating count → for_each
# OLD: count
# resource "aws_instance" "web" {
# count = length(var.web_names)
# ami = "..."
# }
# NEW: for_each
resource "aws_instance" "web" {
for_each = toset(var.web_names)
ami = "..."
}
# Migrate state with moved blocks
moved {
from = aws_instance.web[0]
to = aws_instance.web["web-01"]
}
moved {
from = aws_instance.web[1]
to = aws_instance.web["web-02"]
}
moved {
from = aws_instance.web[2]
to = aws_instance.web["web-03"]
}
Nested for_each / dynamic block
resource "aws_security_group" "web" {
name = "web"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = ingress.key
}
}
}
variable "ingress_rules" {
default = {
https = {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ssh = {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
}
Common findings this catches
- Removing list element destroys trailing → switch to for_each.
- for_each over sensitive value → use null_resource workaround.
- for_each key collision → unique keys.
- count = 0 to disable → fine.
- Index references downstream → fragile.
- Mass migration without moved → destroy/recreate.
- Nested loops unreadable → refactor.
When to escalate
- Mass refactor — staged.
- Key strategy across modules — design.
- Performance at scale — profile.
Related prompts
-
Terraform Dynamic Blocks Prompt
Generate nested blocks dynamically — security group rules, tags, conditional blocks, complex iteration.
-
Terraform Module Composition Prompt
Design Terraform modules — input/output contracts, composition, versioning, public vs private registry, when to abstract.
-
Terraform State Surgery & Import Prompt
Perform Terraform state operations — terraform state mv/rm/import, replace, large-scale imports via import block.