Infrastructure as Code

Terraform Remote State: Locking with S3 and DynamoDB

NT

Naveen Teja

3/2/2026

Terraform Remote State: Locking with S3 and DynamoDB

When multiple engineers or CI/CD pipelines run Terraform simultaneously against the same infrastructure, state file corruption becomes a serious risk. The default local state file offers no protection against concurrent runs — two engineers could apply conflicting changes simultaneously, resulting in a corrupted state and unrecoverable infrastructure drift.

The production-grade solution is storing Terraform state remotely in Amazon S3 with state locking provided by a DynamoDB table. S3 provides durable, versioned storage for the state file, while DynamoDB implements a pessimistic locking mechanism. Before any plan or apply operation, Terraform writes a lock entry to DynamoDB. Any subsequent run that detects an existing lock will immediately halt, preventing concurrent modifications.

Enabling S3 versioning is critical — it allows you to roll back to a previous known-good state if a bad apply corrupts the current one. You should also enable server-side encryption on the bucket since the state file contains plaintext sensitive values like passwords and API keys. The Terraform configuration below provisions the complete remote state backend including bucket versioning, encryption, public access blocking, and the DynamoDB lock table.

terraform-remote-state.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket = "your-org-terraform-state"
  
  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "state_versioning" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "state_enc" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "state_block" {
  bucket                  = aws_s3_bucket.terraform_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

# Add this to your backend.tf
# terraform {
#   backend "s3" {
#     bucket         = "your-org-terraform-state"
#     key            = "prod/terraform.tfstate"
#     region         = "us-east-1"
#     dynamodb_table = "terraform-state-locks"
#     encrypt        = true
#   }
# }