Terraform State Locking
State locking prevents concurrent Terraform operations from corrupting the state file. When two engineers run apply simultaneously, one gets the lock and runs; the other gets an error message and waits. Without locking, simultaneous applies can interleave writes and produce a corrupt, inconsistent state.
How Locking Works
terraform planorterraform applystarts → Terraform writes a lock record to the locking backend- If the lock is already held → Terraform prints an error and exits
- Operation completes (or fails) → Terraform releases the lock
The lock contains metadata about who holds it:
{ "ID": "f7a8b2c1-1234-5678-abcd-9876543210ab", "Operation": "OperationTypeApply", "Info": "", "Who": "alice@mycompany.com", "Version": "1.9.5", "Created": "2025-03-15T14:32:11.123456789Z", "Path": "production/terraform.tfstate"}DynamoDB Locking (AWS S3 Backend)
DynamoDB stores the lock record as a row with hash key LockID:
terraform { backend "s3" { bucket = "mycompany-terraform-state" key = "production/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "terraform-state-lock" # Enables locking }}# Required DynamoDB table structureresource "aws_dynamodb_table" "state_lock" { name = "terraform-state-lock" billing_mode = "PAY_PER_REQUEST" hash_key = "LockID"
attribute { name = "LockID" type = "S" }
tags = { Purpose = "Terraform state locking" }}When a lock is held, you’ll see in DynamoDB:
LockID: "mycompany-terraform-state/production/terraform.tfstate"Info: {"ID":"f7a8b2c1-...","Operation":"OperationTypeApply","Who":"alice@mycompany.com",...}GCS Locking (GCP Backend)
GCS uses native object locking — no separate resource needed:
terraform { backend "gcs" { bucket = "mycompany-terraform-state" prefix = "production" # Locking is automatic via GCS object conditional updates }}Azure Blob Locking
Azure Blob Storage uses blob leases for locking — also automatic:
terraform { backend "azurerm" { resource_group_name = "terraform-state-rg" storage_account_name = "mycompanytfstate" container_name = "tfstate" key = "production.terraform.tfstate" # Locking is automatic via Blob Storage leases }}When a Lock Gets Stuck
A lock becomes stuck when a Terraform process is interrupted before it can release the lock (e.g., killed terminal, lost network connection, CI runner timeout):
Error: Error locking state: Error acquiring the state lock: ConditionalCheckFailedException
Lock Info: ID: f7a8b2c1-1234-5678-abcd-9876543210ab Path: mycompany-terraform-state/production/terraform.tfstate Operation: OperationTypeApply Who: alice@mycompany.com Version: 1.9.5 Created: 2025-03-15T14:32:11.123456789Z Info:Do not immediately force-unlock. First verify:
- Is the original Terraform process actually still running?
- Check CI/CD — is a pipeline job still in progress?
- Look at the
WhoandCreatedfields — is this lock recent?
Force Unlocking (Use Carefully)
Only force-unlock after confirming no Terraform process is actively running:
# Get the lock ID from the error messageterraform force-unlock f7a8b2c1-1234-5678-abcd-9876543210ab
# Confirmation prompt — type "yes"Do you really want to force-unlock? Terraform will remove the lock on the remote state. This will allow local Terraform commands to modify this state, even though it may still be in use.
Lock ID: f7a8b2c1-1234-5678-abcd-9876543210ab
Do you want to unlock? Only "yes" will be accepted to confirm. Enter a value: yesForce-unlock directly in DynamoDB when the CLI isn’t working:
# Delete the lock record directly from DynamoDBaws dynamodb delete-item \ --table-name terraform-state-lock \ --key '{"LockID": {"S": "mycompany-terraform-state/production/terraform.tfstate"}}'Disabling Locking (Not Recommended)
For operations where you’re sure no one else is running:
# Skip locking for a single operationterraform apply -lock=false
# Skip locking with timeout overrideterraform apply -lock-timeout=60s # Wait up to 60s for lock to clearNever disable locking in CI/CD pipelines — the whole point is to serialize concurrent runs.
Best Practices for Team Locking
# In CI/CD: use a lock timeout instead of disabling locking# If another pipeline is running, wait up to 5 minutes for it to finish# terraform apply -lock-timeout=5m
# Split state by team/service to reduce lock contention# networking team's state is independent of app team's statebackend "s3" { key = "teams/networking/production/terraform.tfstate"}Design principles:
- One state per bounded change scope — networking state, database state, and app state are independent, so teams don’t block each other
- Short-lived applies — keep apply time under 10 minutes to minimize lock hold time
- CI/CD serialization — use pipeline concurrency settings to prevent two jobs targeting the same state from running simultaneously