Terraform State Management

Status: Active
Last Updated: 2026-01-30
Category: Infrastructure - Infrastructure as Code
Prerequisites: terraform-basics
Time: 2-3 hours
Tags: terraform, state, backend, locking, collaboration

Summary

Master Terraform state management for team collaboration and production deployments. Learn state backends, locking mechanisms, state manipulation, and best practices for managing infrastructure state safely across teams.

🎯 What You'll Learn

By the end of this article, you'll be able to:

πŸ€” What is State?

The State Problem

Terraform needs to know:

terraform.tfstate answers these questions.


State File Example

terraform.tfstate:

{
  "version": 4,
  "terraform_version": "1.7.0",
  "serial": 3,
  "lineage": "abc123...",
  "outputs": {
    "instance_id": {
      "value": "i-0123456789abcdef",
      "type": "string"
    }
  },
  "resources": [
    {
      "mode": "managed",
      "type": "aws_instance",
      "name": "web",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "id": "i-0123456789abcdef",
            "ami": "ami-0c55b159cbfafe1f0",
            "instance_type": "t3.micro",
            "public_ip": "54.123.45.67"
          }
        }
      ]
    }
  ]
}

Why State Matters

State enables:


🏠 Local State (Default)

How Local State Works

terraform-project/
β”œβ”€β”€ main.tf
β”œβ”€β”€ terraform.tfstate        # Current state
└── terraform.tfstate.backup # Previous state

Commands:

# Apply creates/updates state
terraform apply

# State stored locally
cat terraform.tfstate

# Backup created automatically
cat terraform.tfstate.backup

Local State Problems

❌ Single point of failure:

# Laptop dies = state lost!
rm terraform.tfstate
# Now Terraform has no idea what exists

❌ No team collaboration:

# Developer A applies
terraform apply

# Developer B doesn't see changes
terraform apply  # Conflicts!

❌ No locking:

# Two people run apply simultaneously
# Race conditions, corrupted state!

❌ Secrets in plaintext:

{
  "resources": [{
    "attributes": {
      "password": "super_secret_password"  // Visible!
    }
  }]
}

☁️ Remote State Backends

Why Remote State?

βœ… Shared state: Team collaboration
βœ… Locking: Prevent conflicts
βœ… Backup: Cloud storage durability
βœ… Encryption: Secrets protected
βœ… Version history: State snapshots


S3 Backend (AWS)

backend.tf:

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

Setup S3 bucket:

# setup/main.tf (run once)
provider "aws" {
  region = "us-east-1"
}

# S3 bucket for state
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state"
  
  lifecycle {
    prevent_destroy = true
  }
}

# Enable versioning
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

# Enable encryption
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 = "AES256"
    }
  }
}

# Block public access
resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# DynamoDB table for locking
resource "aws_dynamodb_table" "terraform_state_lock" {
  name           = "terraform-state-lock"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "LockID"
  
  attribute {
    name = "LockID"
    type = "S"
  }
}

Apply setup:

cd setup
terraform init
terraform apply

# Now configure backend in main project
cd ../main-project
# Add backend.tf (shown above)
terraform init  # Migrate to S3

Azure Blob Storage Backend

backend.tf:

terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "tfstateaccount"
    container_name       = "tfstate"
    key                  = "production.terraform.tfstate"
  }
}

Setup Azure storage:

# Create resource group
az group create --name terraform-state-rg --location eastus

# Create storage account
az storage account create \
  --name tfstateaccount \
  --resource-group terraform-state-rg \
  --location eastus \
  --sku Standard_LRS \
  --encryption-services blob

# Create container
az storage container create \
  --name tfstate \
  --account-name tfstateaccount

Terraform Cloud Backend

backend.tf:

terraform {
  cloud {
    organization = "my-company"
    
    workspaces {
      name = "production"
    }
  }
}

Benefits:

Setup:

# Login
terraform login

# Initialize
terraform init

HTTP Backend (Generic)

backend.tf:

terraform {
  backend "http" {
    address        = "https://terraform.example.com/state/prod"
    lock_address   = "https://terraform.example.com/state/prod/lock"
    unlock_address = "https://terraform.example.com/state/prod/lock"
    username       = "terraform"
    password       = "secret"  # Use env var!
  }
}

PostgreSQL Backend

backend.tf:

terraform {
  backend "pg" {
    conn_str = "postgres://user:pass@db.example.com/terraform_backend"
    schema_name = "terraform_remote_state"
  }
}

πŸ”’ State Locking

Why Locking?

Without locking:

Developer A:                Developer B:
terraform apply            terraform apply
  ↓                          ↓
Reading state...           Reading state...
  ↓                          ↓
Planning changes...        Planning changes...
  ↓                          ↓
Applying...                Applying...
  ↓                          ↓
Writing state...           Writing state... (CORRUPTED!)

With locking:

Developer A:                Developer B:
terraform apply            terraform apply
  ↓                          ↓
Acquiring lock... βœ…        Acquiring lock... ❌
  ↓                          ↓
Reading state...           "State locked by Developer A"
  ↓                          (waits...)
Applying...
  ↓
Releasing lock... βœ…
                             ↓
                           Acquiring lock... βœ…
                             ↓
                           Reading state...

Lock Providers

Backend Lock Support Lock Method
S3 βœ… DynamoDB
Azure βœ… Blob lease
Terraform Cloud βœ… Built-in
PostgreSQL βœ… pg_advisory_lock
Consul βœ… KV store
etcd βœ… Distributed lock
Local ❌ None

Manual Lock Management

# Force unlock (DANGEROUS!)
terraform force-unlock LOCK_ID

# Example: Unlock if teammate's laptop died
terraform force-unlock abc123-def456-789012

⚠️ Only use if you're SURE no one else is applying!


πŸ› οΈ State Commands

List Resources

# List all resources in state
terraform state list

# Output:
# aws_vpc.main
# aws_subnet.public
# aws_instance.web

Show Resource

# Show resource details
terraform state show aws_instance.web

# Output:
# resource "aws_instance" "web" {
#     id            = "i-0123456789abcdef"
#     ami           = "ami-0c55b159cbfafe1f0"
#     instance_type = "t3.micro"
#     public_ip     = "54.123.45.67"
# }

Move Resource

# Rename resource in state
terraform state mv aws_instance.old aws_instance.new

# Move to module
terraform state mv aws_instance.web module.webserver.aws_instance.web

# Move from module
terraform state mv module.old.aws_instance.web aws_instance.web

Remove Resource

# Remove from state (doesn't destroy resource!)
terraform state rm aws_instance.web

# Resource still exists in AWS!
# Terraform just forgets about it

# Useful for:
# - Moving resource to different state file
# - Importing into another Terraform project
# - Manually managing resource

Import Resource

# Import existing AWS instance
terraform import aws_instance.web i-0123456789abcdef

# Import with module
terraform import module.webserver.aws_instance.web i-0123456789abcdef

# Terraform adds to state, but doesn't have config yet!
# You still need to write matching configuration

Pull/Push State

# Download current state
terraform state pull > terraform.tfstate.backup

# Upload state (DANGEROUS!)
terraform state push terraform.tfstate.backup

πŸ”„ Migrating State

Local to Remote

Step 1: Configure backend:

# backend.tf
terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}

Step 2: Initialize:

terraform init

# Prompt:
# Do you want to copy existing state to the new backend?
# yes

# State migrated! βœ…

Step 3: Verify:

# Check S3
aws s3 ls s3://my-terraform-state/prod/

# Local state is now backup
ls terraform.tfstate*

Remote to Remote

Change backend:

# OLD backend
terraform {
  backend "s3" {
    bucket = "old-state-bucket"
    key    = "terraform.tfstate"
    region = "us-east-1"
  }
}

# NEW backend
terraform {
  backend "s3" {
    bucket = "new-state-bucket"
    key    = "terraform.tfstate"
    region = "us-west-2"
  }
}

Migrate:

terraform init -migrate-state

🚨 State Disasters

Disaster 1: State Lost

Symptoms:

terraform apply
# Error: state file not found

Recovery options:

Option A: Restore from backup

# S3 versioning enabled?
aws s3api list-object-versions --bucket my-terraform-state --prefix terraform.tfstate

# Restore previous version
aws s3api get-object --bucket my-terraform-state --key terraform.tfstate --version-id VERSION_ID terraform.tfstate

Option B: Rebuild state

# Import every resource (tedious!)
terraform import aws_instance.web i-0123456789abcdef
terraform import aws_vpc.main vpc-abc123
# ... repeat for all resources

Option C: Nuclear option

# Destroy all resources manually
# Delete state
# Start fresh
terraform apply

Disaster 2: State Corrupted

Symptoms:

terraform plan
# Error: state data is corrupted

Recovery:

# Restore from backup
mv terraform.tfstate terraform.tfstate.broken
cp terraform.tfstate.backup terraform.tfstate

# Or from S3 versioning
aws s3api list-object-versions --bucket my-terraform-state
aws s3api get-object --bucket my-terraform-state --key terraform.tfstate --version-id GOOD_VERSION terraform.tfstate

Disaster 3: Drift Detected

Symptoms:

terraform plan
# ~ aws_instance.web will be updated in-place
#   ~ instance_type: "t3.small" -> "t3.micro"
# 
# Someone changed the instance outside Terraform!

Solutions:

Option A: Accept changes

# Refresh state to match reality
terraform apply -refresh-only

Option B: Revert changes

# Apply to force back to config
terraform apply

Option C: Update config

# Update config to match reality
resource "aws_instance" "web" {
  instance_type = "t3.small"  # Match what exists
}

πŸ” State Security

Encrypt State

S3 with encryption:

terraform {
  backend "s3" {
    bucket  = "my-state"
    key     = "terraform.tfstate"
    encrypt = true  # Server-side encryption
    
    # Or use KMS
    kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abc123"
  }
}

Sensitive Data in State

Problem: Passwords stored in plaintext!

{
  "resources": [{
    "attributes": {
      "password": "super_secret"  // Anyone with state access sees this!
    }
  }]
}

Solutions:

1. Mark as sensitive (hides in plan/apply output):

variable "db_password" {
  sensitive = true
}

output "db_password" {
  value     = var.db_password
  sensitive = true
}

2. Use secret manager:

# Don't store in Terraform at all
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "production/db/password"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db.secret_string
}

3. Restrict state access:

# S3 bucket policy
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"AWS": "arn:aws:iam::123456789012:user/terraform"},
    "Action": "s3:*",
    "Resource": "arn:aws:s3:::my-state/*"
  }]
}

πŸ’‘ Best Practices

1. Never Edit State Manually

# DON'T
vim terraform.tfstate  # ❌

# DO
terraform state mv ...  # βœ…
terraform state rm ...  # βœ…

2. Always Use Remote State

# Development
terraform {
  backend "s3" {
    bucket = "dev-terraform-state"
    key    = "terraform.tfstate"
  }
}

# Production
terraform {
  backend "s3" {
    bucket = "prod-terraform-state"
    key    = "terraform.tfstate"
  }
}

3. Enable State Locking

# S3 + DynamoDB for locking
terraform {
  backend "s3" {
    bucket         = "my-state"
    key            = "terraform.tfstate"
    dynamodb_table = "terraform-locks"  # Required for locking!
  }
}

4. Use State Versioning

# S3 versioning enabled
resource "aws_s3_bucket_versioning" "state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

5. Separate State Per Environment

s3://terraform-state/
β”œβ”€β”€ dev/terraform.tfstate
β”œβ”€β”€ staging/terraform.tfstate
└── production/terraform.tfstate

6. Never Commit State to Git

.gitignore:

# Local state
*.tfstate
*.tfstate.*

# Backup files
*.backup

# Terraform directory
.terraform/
.terraform.lock.hcl

πŸ”— What's Next?

Providers:

Modules:

Workspaces:


πŸ“š Resources

Official Docs:

Backend Types:


πŸ“ Change Log

2026-01-30


Next Article: terraform-providers - Multi-cloud infrastructure!

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse