Table of Contents
Introduction
When multiple team members or CI/CD pipelines work with Terraform simultaneously, there’s a real risk of state file corruption. If two people run terraform apply at the same time, both operations might read the same state, make different changes, and overwrite each other’s modifications. The result can be orphaned resources, configuration drift, or even infrastructure outages.
Terraform solves this problem with state locking. When using an S3 backend with DynamoDB, Terraform acquires a lock before any operation that could modify the state file. If another process attempts to modify the state while it’s locked, Terraform will display an error and wait or exit, preventing corruption.
How State Locking Works
The locking mechanism follows a simple but effective pattern:
- Before any state-modifying operation (
plan,apply,destroy), Terraform writes a lock record to DynamoDB - The lock record contains a unique ID, the operation being performed, who initiated it, and when
- If another Terraform process tries to acquire the lock, it sees the existing record and fails with a clear error message
- When the operation completes (or fails), Terraform releases the lock by deleting the record
This mechanism ensures that only one Terraform operation can modify the state at any given time, regardless of whether the operations originate from different machines, users, or CI/CD pipelines.
Setting Up the S3 Backend with DynamoDB Locking
Step 1: Create the S3 Bucket for State Storage
# state-infrastructure/main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-company-terraform-state"
lifecycle {
prevent_destroy = true
}
tags = {
Name = "Terraform State"
Environment = "shared"
ManagedBy = "terraform"
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
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"
}
}
}
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
}
Step 2: Create the DynamoDB Table for Locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
point_in_time_recovery {
enabled = true
}
tags = {
Name = "Terraform State Locks"
Environment = "shared"
ManagedBy = "terraform"
}
}
The LockID attribute is the partition key and must be of type S (String). Terraform uses the state file path as the lock ID, which means each state file gets its own independent lock.
Step 3: Configure the Backend
In your main Terraform project, configure the backend:
# backend.tf
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "production/infrastructure/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
After adding the backend configuration, initialize Terraform to migrate the state:
terraform init
Terraform will detect the backend change and ask if you want to migrate the existing state.
Understanding Lock Behavior
Viewing Active Locks
When a lock is active, the DynamoDB table contains a record like this:
{
"LockID": "my-company-terraform-state/production/infrastructure/terraform.tfstate",
"Info": "{\"ID\":\"a1b2c3d4-e5f6-7890\",\"Operation\":\"OperationTypeApply\",\"Info\":\"\",\"Who\":\"user@hostname\",\"Version\":\"1.7.0\",\"Created\":\"2025-01-22T10:30:00.000Z\",\"Path\":\"my-company-terraform-state/production/infrastructure/terraform.tfstate\"}"
}
What Happens During a Lock Conflict
If another user tries to run a Terraform command while the state is locked, they’ll see an error like:
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: a1b2c3d4-e5f6-7890
Path: my-company-terraform-state/production/infrastructure/terraform.tfstate
Operation: OperationTypeApply
Who: user@hostname
Version: 1.7.0
Created: 2025-01-22 10:30:00.000000 +0000 UTC
Terraform acquires a state lock to protect the state from being
written by multiple users at the same time. Please resolve the
issue above and try again.
Force Unlocking State
Sometimes a lock gets stuck — for example, if a CI/CD pipeline crashes mid-apply or a developer’s machine loses network connectivity. In these cases, you can force-unlock the state:
terraform force-unlock a1b2c3d4-e5f6-7890
⚠️ Warning: Only use force-unlock when you are absolutely certain no other Terraform operation is running. Force-unlocking while another process is actively modifying state will lead to corruption.
Safer Alternative: Check DynamoDB Directly
Before force-unlocking, verify the lock status in the AWS Console or CLI:
aws dynamodb get-item \
--table-name terraform-state-locks \
--key '{"LockID":{"S":"my-company-terraform-state/production/terraform.tfstate"}}' \
--query 'Item.Info.S' \
--output text | python3 -m json.tool
This shows you who created the lock and when, helping you determine if it’s a stale lock or an active operation.
Multi-Environment Lock Strategy
For organizations with multiple environments, use separate state files to enable parallel operations across environments:
# environments/dev/backend.tf
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "dev/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
# environments/prod/backend.tf
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
With this setup, a developer can apply changes to the dev environment while another applies to staging — each has its own lock.
Cost Considerations
The DynamoDB table uses PAY_PER_REQUEST billing mode, which means you only pay for actual lock operations. Typical Terraform usage generates minimal DynamoDB requests:
- Lock acquire: 1 write request per operation
- Lock release: 1 delete request per operation
- Lock check: 1 read request per operation
For most teams, this costs less than $1 per month — a negligible cost for the protection it provides.
Best Practices
- Always enable versioning on the S3 bucket — This allows you to recover previous state versions if something goes wrong
- Enable encryption at rest — Use KMS encryption for the S3 bucket and enable point-in-time recovery for DynamoDB
- Restrict access with IAM policies — Only CI/CD roles and authorized developers should have write access to the state bucket and lock table
- Use one DynamoDB table for all projects — The lock ID includes the full state path, so a single table can safely handle locks for all your projects
- Monitor for stale locks — Set up CloudWatch alarms for DynamoDB items older than a threshold (e.g., 2 hours) to catch stuck locks early
Conclusion
State locking is a non-negotiable requirement for any team using Terraform collaboratively. The S3 + DynamoDB combination provides a reliable, cost-effective, and AWS-native solution that prevents state corruption and ensures safe concurrent operations. Setting it up takes only a few minutes but saves hours of debugging and potential infrastructure issues down the road.

