Initializing Enclave...

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 PutItem conditional write because a lock record for your state file already exists — left behind by a crashed, OOM-killed, or network-interrupted terraform apply.
  • How to fix it: Retrieve the LockID from the DynamoDB table directly, then run terraform 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 LockID partition 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.

Related Diagnostics

"Part of the Performance Utility Matrix."

View all 219 Performance Tools →