Configure Terraform State Locking with an S3 Backend and DynamoDB Lock Table
Step‑by‑step guide to enable Terraform state locking using an S3 backend and a DynamoDB lock table, including setup, verification, and recovery procedures.
29 May 2026, 22:17 UTC

Desired Outcome
Enable Terraform state locking so that concurrent runs of terraform init, plan, apply, or destroy cannot interfere with each other when using an Amazon S3 bucket for remote state storage. The solution uses a DynamoDB table as a lock manager, preventing race conditions and ensuring that only one Terraform operation modifies the state at a time.
Prerequisites
- Terraform CLI version 0.9 or newer (any recent 1.x release works).
- An AWS account with permissions to create S3 buckets, DynamoDB tables, and manage IAM policies.
- The AWS CLI installed and configured with a profile that has sufficient rights to create the resources described below.
- A working Terraform configuration that currently uses an S3 backend without locking (or no backend at all).
Procedure
1. Create the S3 bucket for state storage
Choose a globally unique bucket name and the AWS region where you want the state to reside.
# Replace with your chosen bucket name and region
BUCKET_NAME=my-terraform-state-2025
REGION=us-east-1
aws s3api create-bucket \
--bucket $BUCKET_NAME \
--region $REGION \
--create-bucket-configuration LocationConstraint=$REGION
# Enable versioning (recommended for state recovery)
aws s3api put-bucket-versioning \
--bucket $BUCKET_NAME \
--versioning-configuration Status=Enabled
2. Create the DynamoDB lock table
The table must have a primary key named LockID of type String. No other attributes are required; Terraform will add its own fields when acquiring a lock.
# Replace with your preferred table name
TABLE_NAME=terraform-lock-table
aws dynamodb create-table \
--table-name $TABLE_NAME \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST # on-demand capacity; adjust if you prefer provisioned
# Optional: add a description for clarity
aws dynamodb tag-resource \
--resource-arn arn:aws:dynamodb:$REGION:$(aws sts get-caller-identity --query Account --output text):table/$TABLE_NAME \
--tags Key=Description,Value="Terraform state lock table"
3. Configure the Terraform backend
Add or modify the backend "s3" block in your Terraform configuration (typically in backend.tf or directly inside the terraform block).
terraform {
backend "s3" {
bucket = "my-terraform-state-2025"
key = "prod/network/terraform.tfstate" # path inside the bucket
region = "us-east-1"
dynamodb_table = "terraform-lock-table"
encrypt = true # optional, enables SSE-S3 for the state file
}
}
After saving the backend configuration, run terraform init to initialize the backend and verify that Terraform can access both the S3 bucket and the DynamoDB table.
terraform init
Expected output includes a line similar to:
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
4. Verify locking behavior
To observe the lock in action, start a long‑running apply in one terminal and attempt a second apply in another.
- In Terminal A, add a temporary
null_resourcethat sleeps for 60 seconds to any module, then run:
terraform apply -auto-approve
While the apply is still running (you should see the sleep progress), switch to Terminal B and run:
terraform apply -auto-approve
Terminal B should display a message like:
Acquiring state lock... (this may take a few moments)
After the apply in Terminal A finishes (the lock is released automatically), Terminal B will proceed and show the usual plan/apply output.
5. Inspect the lock item (optional)
You can query the DynamoDB table to see the lock attributes while a run holds the lock.
aws dynamodb scan \
--table-name terraform-lock-table \
--filter-expression "attribute_exists(LockID)" \
--output json
The item will contain fields such as LockID, Info, Who, Version, and CreatedAt. When no lock is held, the scan returns zero items.
Expected Checks
terraform initcompletes without errors and reports the backend as configured.- During a concurrent run, the second command shows the "Acquiring state lock..." message and does not proceed until the lock is released.
- After a successful run, a scan of the DynamoDB table shows no lock items.
- If you manually insert a lock item (e.g., via AWS CLI), subsequent Terraform commands block until the item is removed or you run
terraform force-unlockwith the correct lock ID.
Recovery Options (if a lock becomes stale)
If a Terraform run crashes or is interrupted, the lock item may remain in DynamoDB, blocking future operations.
- Confirm that no Terraform process is still active (check your CI system, local terminals, or any automation).
- Retrieve the lock ID from the stale item (you can get it from the earlier scan output or from the error message that Terraform prints when it fails to acquire the lock).
- Run the force‑unlock command:
terraform force-unlock <LOCK_ID>
Replace <LOCK_ID> with the exact value (a UUID‑like string). Terraform will delete the item and return a confirmation.
Risk: Forcing an unlock while another operation is truly in progress can corrupt the state. Always verify that no other Terraform process is running before executing force-unlock.
Limitations and Considerations
- The S3 bucket and DynamoDB table must be in the same AWS region as the provider configuration; cross‑region setups are not supported.
- Using on-demand billing for DynamoDB avoids capacity planning but may incur higher per‑request costs under very high concurrency. Monitor consumption via CloudWatch.
- Locking does not protect against manual edits to the state file outside of Terraform; treat the state as immutable.
- If the DynamoDB table is accidentally deleted or its key schema altered, Terraform will be unable to acquire locks. Treat the table as part of your immutable infrastructure and protect it with appropriate IAM policies and backup strategies.
Verification Checklist
- Create S3 bucket with versioning enabled.
- Create DynamoDB table with
LockID(String) primary key. - Add
backend "s3"block with correctbucket,key,region, anddynamodb_table. - Run
terraform initand confirm success. - Run a long‑running apply in one terminal and a second apply in another; observe the lock acquisition message and subsequent progression after the first run finishes.
- After the first apply completes, verify that the DynamoDB table contains no lock items.
- Optionally, manually insert a lock item and confirm that a subsequent apply blocks until you run
terraform force-unlockwith the correct ID.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.