Why State Management Breaks Teams
Terraform state is a JSON file that maps your configuration to real infrastructure. When it's managed poorly, you get:
- Two engineers running
terraform applysimultaneously, corrupting state - A deleted state file with no backup, orphaning 200 cloud resources
- Monolithic state files taking 15 minutes to plan
- No way to promote changes from staging to production safely
This guide covers the patterns used by teams managing 500+ resources across multiple environments.
Remote Backend: The Foundation
Never store state locally. Ever. Use a remote backend with locking.
S3 + DynamoDB (AWS — most common):
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/networking/terraform.tfstate"
region = "ap-south-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
Set up the backend infrastructure first:
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state"
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
State File Structure: Split by Blast Radius
The most important decision: how to split your state files.
Bad: One state file for everything
terraform.tfstate ← 5000 resources, 20-minute plans
Good: Split by layer and environment
states/
├── global/
│ └── iam/terraform.tfstate
├── networking/
│ ├── production/terraform.tfstate
│ └── staging/terraform.tfstate
├── data/
│ ├── production/terraform.tfstate
│ └── staging/terraform.tfstate
└── applications/
├── production/api/terraform.tfstate
├── production/web/terraform.tfstate
└── staging/api/terraform.tfstate
The principle: if one part breaks, it shouldn't take everything down.
Split along these boundaries:
- Global — IAM roles, DNS zones, organization-level resources
- Networking — VPCs, subnets, route tables, NAT gateways
- Data — RDS, ElastiCache, S3 buckets, DynamoDB tables
- Applications — ECS services, Lambda functions, API Gateways
Using Data Sources Across State Files
When your networking state is separate from your application state, use terraform_remote_state or data sources:
# In your application module — reference networking outputs
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "mycompany-terraform-state"
key = "production/networking/terraform.tfstate"
region = "ap-south-1"
}
}
resource "aws_ecs_service" "api" {
# Use outputs from the networking state
network_configuration {
subnets = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
}
Better alternative — use SSM Parameter Store:
# Networking module writes to SSM
resource "aws_ssm_parameter" "private_subnets" {
name = "/infrastructure/production/private-subnet-ids"
type = "StringList"
value = join(",", aws_subnet.private[*].id)
}
# Application module reads from SSM
data "aws_ssm_parameter" "private_subnets" {
name = "/infrastructure/production/private-subnet-ids"
}
SSM is better because it doesn't create hard dependencies between state files.
Environment Promotion Strategy
Use the same Terraform code across environments with tfvars files:
environments/
├── staging.tfvars
├── production.tfvars
└── dr.tfvars
# staging.tfvars
environment = "staging"
instance_type = "t3.medium"
min_capacity = 1
max_capacity = 3
multi_az = false
# production.tfvars
environment = "production"
instance_type = "m5.large"
min_capacity = 3
max_capacity = 10
multi_az = true
Apply with:
terraform plan -var-file=environments/production.tfvars
terraform apply -var-file=environments/production.tfvars
State Disaster Recovery
Your state file gets corrupted or deleted. What do you do?
Prevention (do this now):
Recovery when state is lost:
# Import existing resources back into state
terraform import aws_instance.web i-1234567890abcdef0
terraform import aws_rds_cluster.main my-cluster-id
terraform import aws_vpc.production vpc-abc123
# For many resources, use terraformer to bulk-import
terraformer import aws --resources=vpc,subnet,sg --regions=ap-south-1
State Locking Deep Dive
When two engineers run terraform apply simultaneously without locking, you get state corruption. DynamoDB-based locking prevents this.
What happens with locking:
terraform apply → acquires lockterraform apply → gets "state locked" errorForce-unlocking (use with extreme caution):
# Only use when you're certain no one else is applying
terraform force-unlock <lock-id>
Key Takeaways
- Remote state with locking is non-negotiable for teams
- Split state by blast radius — networking, data, and applications should be separate
- Use SSM or data sources to share outputs between state files
- Same code, different tfvars for environment promotion
- Versioning + cross-region replication for state disaster recovery
- Never run
terraform applywithout reviewing the plan first
Get state management right and your infrastructure deployments become predictable, safe, and fast.
---
Frequently Asked Questions
What is Terraform state and why is it important?
Terraform state is a JSON file that maps your configuration to real cloud resources, tracking resource IDs, attributes, and dependencies. Without state, Terraform cannot determine what exists, what needs updating, or what to destroy. It's the single source of truth for Terraform's knowledge of your infrastructure.
How do I set up remote state with S3 and DynamoDB?
Create an S3 bucket (with versioning enabled) and a DynamoDB table (with LockID as partition key), then configure your backend: terraform { backend "s3" { bucket = "my-tf-state" key = "prod/terraform.tfstate" region = "us-east-1" dynamodb_table = "tf-locks" encrypt = true } }. Run terraform init to migrate existing state to the remote backend.
How do I fix a Terraform state lock error?
State lock errors occur when a previous operation didn't release the lock (crashed or timed out). First verify no one else is actively running Terraform. Then use terraform force-unlock <lock-id> with the lock ID from the error message. Only force-unlock when you're certain no concurrent operations are running — unlocking during an active apply can corrupt state.
What happens if my Terraform state file gets corrupted?
If using S3 with versioning, restore a previous state version from S3 version history. Run terraform plan to verify the restored state matches reality. For unrecoverable corruption, use terraform import to rebuild state from existing resources. This is why enabling versioning and cross-region replication on your state bucket is critical for disaster recovery.
How should I split Terraform state for large environments?
Split state by blast radius and team ownership: separate networking, databases, applications, and monitoring into independent state files. Use data sources and SSM parameters to share values between states (like VPC IDs). This limits the impact of misconfigurations, speeds up plan/apply cycles, and allows different teams to work independently.
---
Related Resources
- Production Reference Architectures — 6 production reference architectures with IaC patterns
- DevOps Prompt Library — 500 DevOps prompts including Terraform templates
- AWS IAM Least Privilege Guide — IAM policies for Terraform roles
- Terraform Modules Best Practices — Structuring reusable Terraform modules
- Terraform Import Existing Resources — Importing existing infra into state