Terraform State and Backends
Terraform state is a JSON file that knows your RDS instance is db-ABC123 and that the security group depends on the VPC. Lose it, corrupt it, or let two applies run concurrently, and you are in for an afternoon of terraform import and prayer. Remote backends with locking are table stakes; the harder problems are partitioning, secrets in state, and refactoring resources without destroy-and-recreate.
Local vs remote state
Local terraform.tfstate on a laptop works for solo experiments. It fails for teams:
- No locking → concurrent applies corrupt state
- Not shared → teammates recreate resources
- No versioning → no rollback
Remote backend minimum:
terraform {
backend "s3" {
bucket = "org-terraform-state"
key = "prod/network/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Enable S3 versioning on the bucket — previous state versions are your undo button.
Locking mechanics
DynamoDB lock item prevents parallel apply/plan-with-lock. Stuck lock after CI crash:
terraform force-unlock LOCK_ID
Only after confirming no running apply. Force-unlock during active apply causes state corruption.
State partitioning strategy
| Split by | Example key path |
|---|---|
| Environment | prod/eks/terraform.tfstate |
| Layer | prod/network/, prod/apps/ |
| Team | platform/vpc/, data/rds/ |
Anti-pattern: one state for entire prod account — 45-minute plans, everyone blocked.
Cross-stack references:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "org-terraform-state"
key = "prod/network/terraform.tfstate"
region = "eu-west-1"
}
}
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
Terraform 1.5+ terraform stack and Cloud stacks evolve this pattern — same principle: explicit outputs, no duplicate resources.
Sensitive values in state
State contains resource attributes, including secrets marked sensitive in plan output. Treat state bucket as highly confidential:
- Encryption at rest (S3 SSE-KMS)
- IAM least privilege
- No public access
- Audit logging
Prefer referencing Secrets Manager/SSM rather than storing secret strings in Terraform resources when possible. If password must flow through Terraform, rotate after apply.
Import and adoption
Bring existing infrastructure under management:
terraform import aws_instance.web i-0abc123def456
Write matching .tf first, then import. Plan should show no changes after successful import.
Bulk adoption: tools like terraformer generate rough .tf from scan — always refactor into modules afterward.
Refactoring without destroy — moved and import blocks
Terraform 1.1+ moved block retargets state when renaming resources:
moved {
from = aws_security_group.old
to = aws_security_group.web
}
import block (1.5+) declarative import in config:
import {
to = aws_instance.web
id = "i-0abc123"
}
Avoid manual terraform state mv when declarative alternatives exist — they are reviewable in PR.
Recovery playbook
Corrupted state: restore previous S3 version.
Accidental destroy: restore state version; re-apply if resources still exist, or recreate from module.
Resource drifted outside state: import or remove from config.
Splitting state: terraform state pull, carefully edit JSON (expert only), or use terraform state mv -state-out to new backend.
Always backup before manual state surgery.
CI integration
terraform init -backend-config=...terraform planon PR (post comment)terraform applyon merge to main with approval gate- State access via OIDC role, not long-lived keys
State access control and auditing
Limit who can read prod state — it contains secrets and infrastructure topology useful to attackers. Use IAM policies on S3 bucket and DynamoDB lock table. Enable CloudTrail data events on state bucket. Terraform Cloud adds RBAC and audit log of who ran apply — worth it for regulated industries even if S3 backend suffices technically.
Workspace vs directory layout
| Pattern | State files | Use case |
|---|---|---|
| Monorepo workspaces | One per env/stack | Platform team, shared modules |
| Directory per env | env/prod, env/staging |
Simple prod/staging split |
| Terragrunt | Generated backends | DRY backend config |
# environments/prod/backend.tf
terraform {
backend "s3" {
bucket = "tf-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-locks-prod"
encrypt = true
}
}
Never share state files between environments — staging terraform destroy must not touch production resources.
State size management
Large states (>10 MB) slow every plan:
- Split stacks by lifecycle (network rarely changes, apps change weekly)
- Remove resources from state with
terraform state rmafter migration to new stack - Use
-targetsparingly — it doesn't reduce state size, only plan scope
Monitor terraform plan duration — sudden 10× increase often means someone imported thousands of resources into one state file.
Pair with Terraform modules composition when splitting state along module boundaries.
State locking and team workflows
Remote state without locking corrupts on concurrent apply. S3 backend uses DynamoDB for locks; GCS uses native locking; Terraform Cloud provides managed locking. Never disable locking to "speed up" CI — two applies racing produce state corruption that takes hours to untangle. Use separate state files per environment and per blast-radius boundary — one monolithic state for entire org means every apply risks everything.
Encryption and access logging
S3 state buckets require SSE-KMS or SSE-S3, versioning enabled, and public access blocked at account level. CloudTrail data events on state bucket object reads catch unauthorized access. Restrict s3:GetObject on state to CI role and break-glass admin role only — developer laptops do not need direct state download.
Resources
- Terraform backend types
- S3 backend documentation
- State locking
- moved block reference
- Import block (Terraform 1.5+)
terraform state management backends rollout
Field RUM on Android 4G. Rollback documented in the PR. Test back navigation and offline recovery.
terraform state management backends rollout
Field RUM on Android 4G. Rollback documented in the PR. Test back navigation and offline recovery.
terraform state management backends rollout
Field RUM on Android 4G. Rollback documented in the PR. Test back navigation and offline recovery.
terraform state management backends rollout
Field RUM on Android 4G. Rollback documented in the PR. Test back navigation and offline recovery.
terraform state management backends rollout
Field RUM on Android 4G. Rollback documented in the PR. Test back navigation and offline recovery.
State is a database of record
Encrypt and version the backend; require locks on every apply. Split state by blast radius. CI plans broadly and applies narrowly. Never share prod and non-prod state.
Practice restoring a prior state version and planning against it. Treat backend migrations like datastore migrations. Partial applies leave orphans — your runbook should know how to find them in the cloud console.
Verification layer 1 for terraform state management backends
Define an acceptance check for layer 1: failure injection, timeout behavior, and rollback. Keep it next to the code that implements terraform state management backends. Reviewers confirm the check fails when the control is disabled.
Verification layer 2 for terraform state management backends
Define an acceptance check for layer 2: failure injection, timeout behavior, and rollback. Keep it next to the code that implements terraform state management backends. Reviewers confirm the check fails when the control is disabled.
Verification layer 3 for terraform state management backends
Define an acceptance check for layer 3: failure injection, timeout behavior, and rollback. Keep it next to the code that implements terraform state management backends. Reviewers confirm the check fails when the control is disabled.
Verification layer 4 for terraform state management backends
Define an acceptance check for layer 4: failure injection, timeout behavior, and rollback. Keep it next to the code that implements terraform state management backends. Reviewers confirm the check fails when the control is disabled.
Frequently asked questions
Why does Terraform need state?
Terraform state maps resource identifiers in configuration to real cloud resource IDs, tracks metadata for dependency ordering, and records attributes needed for other resources. Without state, Terraform cannot know whether to create or update an existing resource on apply. State is the source of truth for Terraform's view of the world — distinct from but synchronized with actual infrastructure.
What is the recommended remote backend for Terraform?
AWS S3 with DynamoDB table for state locking is the most common open-source pattern: durable, versioned state files and mutual exclusion during apply. Terraform Cloud and HCP Terraform provide managed remote state with RBAC and run history. GCS and Azure Blob with native locking are equivalents on other clouds.
How should you partition Terraform state?
Split by blast radius and team ownership — separate state per environment (dev/staging/prod) and per domain (network, compute, data). Smaller states plan faster and limit damage from bad apply. Use terraform_remote_state data source or stack outputs (Terraform 1.5+ stacks) to pass references between partitions — never duplicate resource management across states.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →