Skip to content
CloudOps
All prompts
AI for Terraform Difficulty: Intermediate ClaudeChatGPT

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

  1. Default to for_each.
  2. count for simple/conditional.
  3. Stable keys are essential.
  4. 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

Newsletter

Get weekly AI workflows for DevOps engineers

Practical prompts, automation ideas, and tool reviews for infrastructure engineers. One email per week. No spam.