Terraform apply fails with state lock after CI job is canceled
Terraform apply fails with state lock after CI job is canceled
A canceled CI job in the middle of terraform apply leaves the workspace locked. The next pipeline run dies in seconds with Error acquiring the state lock, prints a lock ID and creation timestamp, and blocks every teammate touching the same state. If two engineers retry, the second failure looks like a recurring outage. Clearing the Terraform state lock canceled CI jobs leave behind is usually a five-minute fix — but only if you can prove the original process is gone.
What the lock record actually is
Terraform's S3 backend writes a lock item to DynamoDB under LockID; the GCS backend writes a .tflock object; the Postgres backend takes an advisory lock; Terraform Cloud uses its own run lock [link needed]. Canceling a job sends SIGTERM, then SIGKILL after a grace period, and a killed process never runs its deferred unlock [link needed]. Those records have no built-in expiry [link needed]. The lock error's Lock Info block includes the ID, state path, operation, who created it, and when — copy those fields before touching anything.
Recovering from a Terraform state lock canceled CI left behind
1. Confirm the holder is dead. Check the runner or pod, the Terraform Cloud run, and any local terraform apply a teammate started. A "canceling" job can still be alive.
2. Copy the lock ID from the error output. Do not reconstruct or guess it.
3. Inspect the backend for the matching record: the DynamoDB LockID item, the .tflock object in the bucket, or a pg_locks row.
4. Force-unlock from the same directory and backend config: terraform force-unlock <LOCK_ID> [link needed]. Confirm the prompt only after step 3 matches.
5. Re-run terraform plan before apply. A killed apply can leave infrastructure half-created; the plan shows the delta so you know what the next apply will change.
Use terraform force-unlock -force <LOCK_ID> only in automation, where no human reads the prompt.
Preventing the next Terraform state lock canceled CI incident
- Make Terraform wait instead of fail: set
TF_CLI_ARGS_apply=-lock-timeout=10min CI [link needed]. A waiter drains the lock when the previous run finishes. Set the timeout above your longest apply, not the average. - Stop canceling applies. GitHub Actions
concurrencywithcancel-in-progress: true[link needed] kills in-flight applies. Split plan and apply into separate jobs so only plans are cancelable. - Forward the signal. Terraform unlocks on
SIGINT, not always on CI's defaultSIGTERM; wrap the CLI withtrap 'kill -INT $TF_PID' TERM[link needed]. - Keep plan locking on.
terraform plantakes the lock by default; leave it and rely on the timeout.-lock=falsehides contention rather than solving it. - Shorten the window. Smaller states and shorter applies mean shorter lock lifetimes.
When NOT to do this
Do not force-unlock when you cannot verify the original process is dead. A draining autoscaler, a network partition between runner and backend, or a job marked "canceled" in the UI can still have a live Terraform process. Two applies writing one state can interleave writes and corrupt it [link needed]. For Terraform Cloud, if the lock belongs to another run, cancel or discard that run first; CLI force-unlock may not clear a remote run lock [link needed]. Skip the whole procedure if the lock was placed intentionally for a change freeze and no CI job is involved.
Checklist before force-unlocking a Terraform state lock canceled CI left behind
- [ ] Runner or pod confirmed stopped, not just "canceling"
- [ ] No teammate or second pipeline running
apply - [ ] Backend record inspected and lock ID matches the error
- [ ] Same backend config and working directory used
- [ ]
terraform planrun after unlock - [ ]
TF_CLI_ARGS_apply=-lock-timeoutset in CI