TFLAB26-State Locking: Preventing Concurrent State Corruption

State Locking: Preventing Concurrent State Corruption

🔧 Terraform Core ⭐ Intermediate State Locking DynamoDB force-unlock LockID

Scenario

Two engineers on your team ran terraform apply at the same time. Both wrote to the same state file. The state is now corrupted — resources are duplicated, some are orphaned. You need state locking to prevent this. With DynamoDB, Terraform acquires a lock before modifying state and releases it after. If someone else is applying, you get a clear error instead of corruption.

Your Objectives
  • Create a DynamoDB table for state locking with a LockID hash key (case-sensitive).
  • Add dynamodb_table to the S3 backend config.
  • Observe the lock being acquired and released during terraform apply.
  • Understand terraform force-unlock for stuck locks.

Additional Context

The DynamoDB table for locking requires exactly one attribute: LockID (type String) as the partition key. The name is case-sensitive — lockid or lock_id will NOT work.

When Terraform starts an operation that modifies state, it writes a lock record to DynamoDB. When done, it deletes the record. If a previous apply crashed and left a stale lock, use terraform force-unlock LOCK_ID — but only if you are certain nobody else is actively applying.

bootstrap/main.tf (create lock table first)

# bootstrap/main.tf — run with local state FIRST
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

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

# DynamoDB table for state locking
# CRITICAL: hash_key MUST be "LockID" (case-sensitive)
resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"   # no capacity planning needed
  hash_key     = "LockID"            # exactly this name, case-sensitive

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

  tags = { Purpose = "terraform-state-lock", ManagedBy = "terraform" }
}

output "lock_table_name" { value = aws_dynamodb_table.terraform_lock.name }

versions.tf (main config with locking)

# versions.tf — S3 backend WITH DynamoDB locking
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }

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

Workflow Commands

# Step 1: Create the lock table
cd bootstrap && terraform init && terraform apply && cd ..

# Step 2: Init main config with locking enabled
terraform init

# Step 3: Apply — watch for lock messages
terraform apply
# Output includes: "Acquiring state lock. This may take a few moments..."
# and: "Releasing state lock. This may take a few moments..."

# If a lock is stuck (crashed apply left the lock):
# First, find the Lock ID from the error message
# ERROR: Error acquiring the state lock
# Lock Info:
#   ID:  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# Force unlock (DANGEROUS — only if you're sure nobody is applying)
terraform force-unlock xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

terraform destroy
✓ Lock Acquisition During Apply
Acquiring state lock. This may take a few moments...

aws_s3_bucket.demo: Creating...
aws_s3_bucket.demo: Creation complete after 2s

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Releasing state lock. This may take a few moments...