Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
stop-slop, taste-skill, terrashark had embedded .git dirs causing Woodpecker clone to fail on submodule update.
3.8 KiB
3.8 KiB
Identity Churn
Use this guide when resource addresses or object identity can shift unexpectedly.
Symptoms
- plan shows broad replace actions after small list edits
- renaming resources/modules triggers destroy/create
- refactor from
counttofor_eachcauses churn - imported resources keep drifting because addressing is unstable
Primary causes
- index-based identity (
count) used for long-lived logical objects - keys derived from unstable data (sorted lists, transient IDs)
- missing
movedblocks during refactor - hidden dependencies forcing replacement chains
for_eachkeys derived from values unknown at plan time
Prevention rules
- use
for_eachfor long-lived identities - choose stable keys from business identity (e.g.,
zone-a,payments-api) - keep identity attributes separate from mutable attributes
- add
movedblocks before first apply after rename/restructure
Decision matrix: count vs for_each
Use count only when:
- resource is truly optional singleton (
0or1) - no downstream references depend on stable per-item addresses
Use for_each when:
- multiple logical instances are expected
- insertion/removal/reordering happens over time
- downstream references need stable keys
- keys are fully known during planning
If keys are unknown at plan time, for_each will fail planning. In that case:
- drive
for_eachfrom known input keys, or - use
countfor conditional/singleton creation when key-stablefor_eachis not possible
Safe migration playbook (count -> for_each)
- define stable key map
- refactor resource to
for_each - add one
movedblock per old index - verify plan reports move operations, not replace
- apply in lower environment first
Example:
locals {
app_subnets = {
a = { cidr = "10.40.1.0/24", az = "us-east-1a" }
b = { cidr = "10.40.2.0/24", az = "us-east-1b" }
}
}
resource "aws_subnet" "app" {
for_each = local.app_subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr
availability_zone = each.value.az
tags = {
Name = "app-${each.key}"
}
}
moved {
from = aws_subnet.app[0]
to = aws_subnet.app["a"]
}
moved {
from = aws_subnet.app[1]
to = aws_subnet.app["b"]
}
Rename playbook
When renaming resource/module labels, add moved first:
moved {
from = module.network_core
to = module.network_foundation
}
LLM mistake checklist
Common model mistakes to correct:
- defaults to
countfor every collection - omits
movedblocks in refactors - uses list index as identity key
- suggests
terraform state mvin automation wheremovedis safer and reviewable - builds
for_eachkeys from computed IDs not known until apply
Known-at-plan example (for_each failure pattern)
Bad (key depends on apply-time value):
resource "aws_security_group_rule" "egress" {
for_each = toset([aws_security_group.ecs.id])
type = "egress"
from_port = 443
to_port = 443
protocol = "tcp"
security_group_id = each.value
cidr_blocks = ["0.0.0.0/0"]
}
Safer fallback for optional singleton behavior:
resource "aws_security_group_rule" "egress" {
count = var.enable_egress_rule ? 1 : 0
type = "egress"
from_port = 443
to_port = 443
protocol = "tcp"
security_group_id = aws_security_group.ecs.id
cidr_blocks = ["0.0.0.0/0"]
}
Verification commands
terraform fmt -check
terraform validate
terraform plan -out=plan.bin
terraform show plan.bin | grep -i moved
OpenTofu equivalent:
tofu fmt -check
tofu validate
tofu plan -out=plan.bin
tofu show plan.bin | grep -i moved