Docs / Automation & IaC / Implement Terraform State Locking with S3 and DynamoDB

Implement Terraform State Locking with S3 and DynamoDB

By Admin · Mar 15, 2026 · Updated Apr 23, 2026 · 226 views · 4 min read

Terraform state files track the current state of your infrastructure. When teams collaborate, concurrent state modifications can corrupt your state file and cause infrastructure drift. State locking with S3 and DynamoDB prevents simultaneous modifications, while remote state storage enables team collaboration. This guide covers setting up a production-ready Terraform backend.

Why Remote State with Locking?

  • Team collaboration: Multiple engineers can work on the same infrastructure safely
  • State locking: DynamoDB prevents concurrent terraform apply operations
  • Versioning: S3 versioning provides state file history and recovery
  • Encryption: State files often contain secrets — encrypt at rest with KMS
  • Durability: S3 provides 99.999999999% durability vs local files

Create the S3 Backend Infrastructure

# bootstrap/main.tf — Create the state backend (run once with local state)
terraform {
  required_version = ">= 1.7.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

# S3 bucket for state storage
resource "aws_s3_bucket" "terraform_state" {
  bucket = "myorg-terraform-state"

  lifecycle {
    prevent_destroy = true
  }

  tags = {
    Name        = "Terraform State"
    Environment = "shared"
    ManagedBy   = "terraform"
  }
}

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

# Enable server-side 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     = "aws:kms"
      kms_master_key_id = aws_kms_key.terraform.arn
    }
    bucket_key_enabled = true
  }
}

# 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
}

# KMS key for encryption
resource "aws_kms_key" "terraform" {
  description             = "KMS key for Terraform state encryption"
  deletion_window_in_days = 30
  enable_key_rotation     = true
}

resource "aws_kms_alias" "terraform" {
  name          = "alias/terraform-state"
  target_key_id = aws_kms_key.terraform.key_id
}

# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }

  tags = {
    Name      = "Terraform State Locks"
    ManagedBy = "terraform"
  }
}

output "state_bucket" {
  value = aws_s3_bucket.terraform_state.id
}

output "dynamodb_table" {
  value = aws_dynamodb_table.terraform_locks.name
}

Configure Projects to Use Remote State

# projects/production/backend.tf
terraform {
  backend "s3" {
    bucket         = "myorg-terraform-state"
    key            = "production/infrastructure/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    kms_key_id     = "alias/terraform-state"
    dynamodb_table = "terraform-state-locks"

    # Optional: assume role for cross-account access
    # role_arn     = "arn:aws:iam::123456789:role/TerraformStateAccess"
  }
}

# Initialize with the backend
# terraform init

How Locking Works

# When you run terraform plan or apply:
# 1. Terraform writes a lock entry to DynamoDB with:
#    - LockID: bucket/key (your state file path)
#    - Info: who holds the lock, operation, timestamp

# 2. If another process tries to acquire the lock, it gets:
#    Error: Error acquiring the state lock
#    Lock Info:
#      ID:        12345678-abcd-efgh-ijkl-123456789012
#      Path:      myorg-terraform-state/production/terraform.tfstate
#      Operation: OperationTypeApply
#      Who:       user@hostname
#      Created:   2026-03-15 10:30:00 UTC

# 3. After the operation completes, the lock is released

# Force-unlock (use with extreme caution!)
terraform force-unlock 12345678-abcd-efgh-ijkl-123456789012

State File Organization

# Organize state files by environment and component
# S3 key structure:
# ├── production/
# │   ├── networking/terraform.tfstate
# │   ├── compute/terraform.tfstate
# │   ├── database/terraform.tfstate
# │   └── monitoring/terraform.tfstate
# ├── staging/
# │   ├── networking/terraform.tfstate
# │   └── compute/terraform.tfstate
# └── shared/
#     ├── dns/terraform.tfstate
#     └── iam/terraform.tfstate

# Reference other states with data source
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "myorg-terraform-state"
    key    = "production/networking/terraform.tfstate"
    region = "us-east-1"
  }
}

# Use outputs from the networking state
resource "aws_instance" "web" {
  subnet_id = data.terraform_remote_state.networking.outputs.public_subnet_ids[0]
}

State Recovery

# List state file versions in S3
aws s3api list-object-versions \
  --bucket myorg-terraform-state \
  --prefix production/infrastructure/terraform.tfstate \
  --query 'Versions[*].{VersionId:VersionId,LastModified:LastModified,Size:Size}' \
  --output table

# Restore a previous state version
aws s3api get-object \
  --bucket myorg-terraform-state \
  --key production/infrastructure/terraform.tfstate \
  --version-id "VERSION_ID_HERE" \
  terraform.tfstate.backup

# Review and restore
terraform show terraform.tfstate.backup
cp terraform.tfstate.backup terraform.tfstate
terraform state push terraform.tfstate

Best Practices

  • Enable S3 versioning — it's your safety net for state file corruption or accidental deletion
  • Use KMS encryption — state files contain sensitive data like passwords and API keys
  • Never manually edit state files — use terraform state commands instead
  • Separate state by environment and component to limit blast radius
  • Use prevent_destroy on the state bucket to avoid accidental deletion
  • Set up lifecycle rules to transition old state versions to cheaper storage classes
  • Avoid force-unlock unless you're certain no other operation is running

Was this article helpful?