Lock Terraform State with DynamoDB to Block Concurrent Runs
Use an S3 backend with a DynamoDB lock table so Terraform automatically serializes concurrent plan and apply operations.
13 Nov 2025, 15:37 UTC

The problem: two applies, one state file
Two engineers run terraform apply against the same environment at almost the same time. Both read the current state file, both calculate changes, and both write back. The second write can overwrite the first, leaving infrastructure partially updated and the state file out of sync with reality.
The practical fix is to store state in Amazon S3 and enable automatic locking with an Amazon DynamoDB table. Terraform writes a lock item to DynamoDB before reading or writing state and removes it when the run finishes. A second run that starts while the lock exists fails immediately instead of corrupting state.
What the lock table does
A backend is where Terraform stores the state file. With the s3 backend, adding dynamodb_table turns on locking. Terraform uses an item whose primary key is named LockID. Before a run touches state, it attempts to create that item. If the item already exists, another run holds the lock and Terraform aborts. When the run succeeds or fails, Terraform deletes the item.
This is automatic. You do not run a separate lock command; standard terraform init, terraform plan, and terraform apply trigger it.
Create the table and backend configuration
The DynamoDB table must exist before you initialize the backend, and it must be in the same AWS region as the S3 bucket. A mismatched region can prevent locking from working as expected.
Create the table with the AWS CLI, run from your workstation or CI runner:
aws dynamodb create-table --table-name terraform-state-locks --attribute-definitions AttributeName=LockID,AttributeType=S --key-schema AttributeName=LockID,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 --region us-east-1
Required IAM permissions for the principal running Terraform: dynamodb:GetItem, dynamodb:PutItem, and dynamodb:DeleteItem on the lock table. You also need standard S3 permissions on the state bucket. Missing DynamoDB permissions often surface as access-denied errors that look like state problems.
Add the backend block to your Terraform configuration. Replace the placeholders with your own values:
terraform {
backend "s3" {
bucket = "STATE_BUCKET_NAME"
key = "environments/prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
Run terraform init in the directory containing your configuration. Terraform will detect the backend change and ask whether to migrate state. Copying state to the new backend is usually safe, but confirm you have a backup before proceeding.
Verify that locking is active
- Open two terminal sessions in the same working directory.
- In terminal one, run
terraform apply. The run starts and holds the lock. - In terminal two, run
terraform applywhile the first run is still active. Expect an error such asError acquiring the state lockand a quick exit, because the default lock timeout is zero seconds. - Let terminal one finish. Terraform deletes the lock item automatically.
- Repeat the apply in terminal two. It should now acquire the lock and proceed.
If the second command does not fail, check that dynamodb_table is set, that the table and bucket share a region, and that the IAM principal has the required DynamoDB permissions.
Limitations and recovery
DynamoDB reads are eventually consistent by default. In theory, two runs could read an absent lock at nearly the same instant and both proceed, though Terraform's write-after-read pattern makes this rare. For most teams, the protection is sufficient.
If a run is killed forcibly, the lock item can remain. Do not delete it manually unless you are certain no other Terraform process is active. Use terraform force-unlock LOCK-ID only after confirming the original run is dead. This command changes state by removing the lock, so treat it as a recovery step, not a routine command.
To disable locking later, remove dynamodb_table from the backend block and run terraform init again. This returns you to unprotected concurrent access.
Closing checklist
- DynamoDB table exists with a String primary key named
LockID. - Table region matches the S3 bucket region.
- IAM principal has
dynamodb:GetItem,dynamodb:PutItem, anddynamodb:DeleteItemon the table. - Backend block includes
dynamodb_table. - You have verified two concurrent applies do not both write state.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.