How to Fix Terraform 'ConditionalCheckFailedException' DynamoDB State Lock Error (Force-Unlock Guide)
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: Terraform's DynamoDB lock table rejected a
PutItemconditional write because a lock record for your state file already exists — left behind by a crashed, OOM-killed, or network-interruptedterraform apply. - How to fix it: Retrieve the
LockIDfrom the DynamoDB table directly, then runterraform force-unlock <LOCK_ID>. Do not manually delete the DynamoDB item unless force-unlock fails. - Use our Client-Side Sandbox above to paste your backend config and auto-generate the exact force-unlock command and a hardened backend HCL block.
The Incident (What Does the Error Mean?)
Raw error output:
Error acquiring the state lock: ConditionalCheckFailedException:
The conditional request failed
Lock Info:
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Path: s3://my-tf-state/prod/terraform.tfstate
Operation: OperationTypeApply
Who: ci-runner@gitlab-runner-abc123
Version: 1.5.7
Created: 2024-01-15 03:42:11.928375 +0000 UTC
Info:
Immediate consequence: Every terraform plan and terraform apply across every pipeline and every engineer targeting this workspace is now completely blocked. DynamoDB's conditional write (attribute_not_exists(LockID)) failed because the lock row already exists. Terraform correctly refuses to proceed — but the process that created the lock is already dead.
The Attack Vector / Blast Radius
This is not just an inconvenience. The cascading failure profile:
- CI/CD pipelines queue up and time out. If your pipeline retries, you now have N runners all blocked, burning runner minutes and potentially hitting API rate limits against S3 and DynamoDB.
- Partial apply state. The original process may have partially mutated infrastructure before dying. Your state file may be inconsistent with real cloud resources until you can run a fresh apply.
- Manual deletion risk. Engineers under pressure manually delete the DynamoDB item instead of using
force-unlock. This bypasses Terraform's internal lock ID validation and can corrupt the state file if another process acquires the lock simultaneously during the delete window. - Multi-workspace blast radius. If your DynamoDB table serves multiple workspaces (standard pattern), the table itself is healthy — only the specific
LockIDpartition key for this state path is stuck. But panicked ops teams sometimes nuke the entire table.
How to Fix It (The Solution)
Step 1 — Confirm the Lock Exists in DynamoDB
aws dynamodb get-item \
--table-name terraform-state-locks \
--key '{"LockID": {"S": "my-tf-state/prod/terraform.tfstate"}}' \
--region us-east-1
If the item exists and the Operation field shows OperationTypeApply or OperationTypePlan with a Created timestamp more than a few minutes old and no live process owns it — it is stale.
Step 2 — Force Unlock (Basic Fix)
Use the ID value from the error output:
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
Terraform will prompt for confirmation. This performs a conditional DynamoDB DeleteItem using the lock ID — safer than manual deletion.
Step 3 — Verify State Integrity Before Re-Applying
terraform plan -detailed-exitcode
Review the diff carefully. If the interrupted apply was mid-resource-creation, you may see phantom resources or drift.
Enterprise Best Practice — Hardened Backend Configuration
The root cause is often an improperly configured backend missing lock table TTL awareness and state encryption. Here is the diff:
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
- # No KMS key — using default S3 SSE
- # No lock timeout handling
+ kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/mrk-abc123"
}
}
resource "aws_dynamodb_table" "tf_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
+ ttl {
+ attribute_name = "ExpireTime"
+ enabled = true
+ }
+
+ point_in_time_recovery {
+ enabled = true
+ }
+
+ server_side_encryption {
+ enabled = true
+ kms_key_arn = aws_kms_key.dynamo_locks.arn
+ }
+
lifecycle {
+ prevent_destroy = true
}
}
Note on TTL: Terraform does not natively write ExpireTime to DynamoDB lock items. You must implement a Lambda or EventBridge rule to sweep stale locks older than a threshold (e.g., 2 hours) by setting ExpireTime on lock creation via a wrapper script — or enforce this via your CI pipeline's timeout + cleanup job.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — Enforce DynamoDB Lock Table Hardening
Add to your checkov pre-commit or pipeline scan:
# .checkov.yaml
check:
- CKV_AWS_28 # DynamoDB point-in-time recovery
- CKV_AWS_119 # DynamoDB KMS encryption
- CKV2_AWS_16 # DynamoDB auto-scaling (or PAY_PER_REQUEST)
2. CI Pipeline — Automatic Force-Unlock on Timeout
# .gitlab-ci.yml excerpt
terraform_apply:
script:
- terraform init
- terraform apply -auto-approve
after_script:
- |
if [ "$CI_JOB_STATUS" == "failed" ]; then
LOCK_ID=$(terraform force-unlock -force 2>&1 | grep -oP '[a-f0-9-]{36}' | head -1)
[ -n "$LOCK_ID" ] && terraform force-unlock -force "$LOCK_ID"
fi
timeout: 30 minutes
3. OPA Policy — Block Applies Without Lock Table
# terraform_backend_lock.rego
package terraform.backend
deny[msg] {
backend := input.configuration.backend
backend.type == "s3"
not backend.config.dynamodb_table
msg := "S3 backend must define dynamodb_table for state locking."
}
Run via conftest in your pipeline before terraform plan is ever executed. A backend without a lock table configured should be a pipeline hard-fail, not a warning.