Skip to main content
Terraform·8 min read

Terraform State Locked? Corrupted? Two People Applied at Once — Production Patterns That Prevent This

How to structure Terraform state for multi-team, multi-environment infrastructure. Remote backends, state locking, workspace strategies, and disaster recovery.

DT

DevOps Engineer & Technical Writer

Why State Management Breaks Teams

TERRAFORM REMOTE STATE ARCHITECTURE ENVIRONMENT Production ENVIRONMENT Staging ENVIRONMENT Development EXECUTION Terraform CLI plan / apply S3 BACKEND (VERSIONED) terraform.tfstate ✓ Encrypted (KMS) ✓ Versioned (rollback) ✓ Per-env keys ✓ Access logging DYNAMODB LOCK TABLE State Locking Prevents concurrent applies KEY PATTERN Separate state files per environment → s3://bucket/prod/service/terraform.tfstate

Terraform state is a JSON file that maps your configuration to real infrastructure. When it's managed poorly, you get:

  • Two engineers running terraform apply simultaneously, 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):

  • S3 versioning enabled (roll back to previous version)
  • Enable MFA delete on the state bucket
  • Cross-region replication to a backup bucket
  • Daily state backups to a separate account
  • 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:

  • Engineer A runs terraform apply → acquires lock
  • Engineer B runs terraform apply → gets "state locked" error
  • Engineer A's apply finishes → releases lock
  • Engineer B retries and succeeds
  • Force-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 apply without 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.

    ---