autojanet/skills/terrashark/references/identity-churn.md
Zoë cfec11bb46
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix: convert skill submodules to plain directories
stop-slop, taste-skill, terrashark had embedded .git dirs causing
Woodpecker clone to fail on submodule update.
2026-05-30 15:44:44 -07:00

148 lines
3.8 KiB
Markdown

# 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 `count` to `for_each` causes 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 `moved` blocks during refactor
- hidden dependencies forcing replacement chains
- `for_each` keys derived from values unknown at plan time
## Prevention rules
- use `for_each` for long-lived identities
- choose stable keys from business identity (e.g., `zone-a`, `payments-api`)
- keep identity attributes separate from mutable attributes
- add `moved` blocks before first apply after rename/restructure
## Decision matrix: `count` vs `for_each`
Use `count` only when:
- resource is truly optional singleton (`0` or `1`)
- 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_each` from known input keys, or
- use `count` for conditional/singleton creation when key-stable `for_each` is not possible
## Safe migration playbook (`count` -> `for_each`)
1. define stable key map
2. refactor resource to `for_each`
3. add one `moved` block per old index
4. verify plan reports move operations, not replace
5. apply in lower environment first
Example:
```hcl
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:
```hcl
moved {
from = module.network_core
to = module.network_foundation
}
```
## LLM mistake checklist
Common model mistakes to correct:
- defaults to `count` for every collection
- omits `moved` blocks in refactors
- uses list index as identity key
- suggests `terraform state mv` in automation where `moved` is safer and reviewable
- builds `for_each` keys from computed IDs not known until apply
## Known-at-plan example (`for_each` failure pattern)
Bad (key depends on apply-time value):
```hcl
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:
```hcl
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
```bash
terraform fmt -check
terraform validate
terraform plan -out=plan.bin
terraform show plan.bin | grep -i moved
```
OpenTofu equivalent:
```bash
tofu fmt -check
tofu validate
tofu plan -out=plan.bin
tofu show plan.bin | grep -i moved
```