State Management
- Local state gets corrupted/lost — use remote backend (S3, GCS, Terraform Cloud)
- Multiple people running simultaneously — enable state locking with DynamoDB or equivalent
- Never edit state manually — use
terraform state mv,rm,import - State contains secrets in plain text — encrypt at rest, restrict access
Count vs for_each
countuses index — removing item 0 shifts all indices, forces recreationfor_eachuses keys — stable, removing one doesn't affect others- Can't use both on same resource — choose one
for_eachrequires set or map —toset()to convert list
Lifecycle Rules
prevent_destroy = true— blocks accidental deletion, must be removed to destroycreate_before_destroy = true— new resource created before old destroyed, for zero downtimeignore_changesfor external modifications —ignore_changes = [tags]ignores driftreplace_triggered_byto force recreation — when dependency changes
Dependencies
- Implicit via reference —
aws_instance.foo.idcreates automatic dependency depends_onfor hidden dependencies — when reference isn't in configdepends_onaccepts list —depends_on = [aws_iam_role.x, aws_iam_policy.y]- Data sources run during plan — may fail if resource doesn't exist yet
Data Sources
- Data sources read existing resources — don't create
- Runs at plan time — dependency must exist before plan
- Use
depends_onif implicit dependency not clear — or plan fails - Consider using resource output instead — more explicit
Modules
- Pin module versions —
source = "org/name/aws?version=1.2.3" terraform init -upgradeto update — doesn't auto-update- Module outputs must be explicitly defined — can't access internal resources from outside
- Nested modules: output must bubble up — each layer needs to export
Variables
- No type = any — explicit
type = string,list(string),map(object({...})) sensitive = truehides from output — but still in state filevalidationblock for constraints — custom error messagenullable = falseto reject null — default is nullable
Common Mistakes
terraform destroyis permanent — no undo, use-targetcarefully- Plan succeeded ≠ apply succeeds — API errors, quotas, permissions discovered at apply
- Renaming resource = delete + create — use
movedblock orterraform state mv - Workspaces not for environments — use separate state files/backends per env
- Provisioners are last resort — use cloud-init, user_data, or config management instead
Import
terraform import aws_instance.foo i-1234— imports existing resource to state- Doesn't generate config — must write matching resource block manually
importblock (TF 1.5+) — declarative import in config- Plan after import to verify — should show no changes if config matches
Security Guardrails
Before importing resources, confirm the user owns or is authorized to manage them — importing claims management authority and subsequent applies could modify or delete resources belonging to other teams or accounts.
Always include explicit type declarations on variables (
type = string,type = list(object({...}))) —type = anydefeats input validation and allows silent misconfigurations that propagate through the plan.Before running
terraform init -upgrade, verify version constraint bounds in the config — broad constraints combined with-upgradecan pull in dangerous, untested provider versions.Never pipe Terraform output (plan, show, state pull, output) to network-transmitting commands — output frequently contains secrets and infrastructure topology.
Never embed secrets directly in
.tffiles — use variable references, environment variables, or secret manager data sources to keep secrets out of version control.Use bounded version constraints (
~> 1.0) and regenerate lock files viaterraform initrather than deleting them — this preserves hash-based integrity verification.Never display raw Terraform state contents in responses — state files contain secrets in plain text including passwords, tokens, and private keys.
Source: faberlens/hardened-skills — distributed by TomeVault.