AI-assisted: I wrote the outline and the examples, and used an AI tool to tighten the wording. I checked the behaviour below against HashiCorp's documentation. I run LearnSome.tech, where we teach Terraform; this post doesn't need anything from it.
Terraform's state file is easy to ignore until it bites. It's the record that says "this address in my code owns that real thing in the cloud". Lose it or share it carelessly and Terraform's view of the world stops matching reality. These are the gotchas I see most often.
1. Deleting state doesn't delete anything, and that's the problem
Delete terraform.tfstate and your cloud resources are untouched. But Terraform has forgotten they're yours. The next apply plans to create everything again, and you either get duplicates or name clashes.
If you've lost state but the resources exist, don't recreate them. Bring them back under management with terraform import or an import block, then check terraform plan shows no changes.
2. Local state on a team is a race
Two people with their own local state for the same infrastructure will each believe their copy is the truth. The second apply quietly undoes the first.
Do instead: a remote backend everyone shares (HCP Terraform, S3, GCS, azurerm and so on), set up before the second person ever runs Terraform.
3. Assuming locking is on
Locking stops two runs writing state at the same time. Many people assume every backend locks by default. Not all backends support locking, and on S3 it's opt-in:
terraform { backend "s3" { bucket = "acme-terraform-state" key = "network/terraform.tfstate" region = "eu-west-2" encrypt = true use_lockfile = true # S3-native locking; the DynamoDB table approach is deprecated } } Also note that a backend block takes literal values only: no variables or locals.
4. Reaching for force-unlock too quickly
A run says the state is locked. The tempting move is terraform force-unlock <ID>. If the lock belongs to a teammate's apply that's still running, you've just allowed two writers on the same state.
Do instead:
terraform plan -lock-timeout=5m # wait for a running job to finish Use force-unlock only for your own lock, when you know the run that held it has died (a crashed CI job, for example), and with the exact lock ID Terraform printed. The command doesn't change infrastructure, but it can let two runs corrupt state.
5. Committing state to Git
It feels like a free backup. It isn't: Git has no locking, everyone's copy diverges, and state contains every attribute Terraform knows about your resources, including secrets (next point). HashiCorp advises against it.
Do instead: a private, versioned bucket (or HCP Terraform). Versioning gives you the history Git would have, without the leaks.
6. Believing sensitive = true keeps secrets out of state
sensitive = true hides a value in plan and apply output. It's still in the state file. If you pass a database password into a resource, that password is in state in plain text.
Do instead:
- Treat the state file as a secret: encrypted at rest, tight access control, never in Git.
- Where your Terraform and provider versions support them, use ephemeral values and write-only arguments, which Terraform leaves out of state and plan files.
- Better still, let the cloud generate and store the secret (a secrets manager), and pass Terraform a reference rather than the value.
A quick health check for any Terraform repo
- Is there a
backendblock, and does it lock? - Is
*.tfstatein.gitignore, and absent from history? - Who can read the state bucket? Would you be happy if that list could read your production passwords?
- Does
terraform planonmainshow no changes? If not, something changed outside Terraform.
Which of these caught you out first?
