How Deployment Locking Works
A plain-language walkthrough of strata’s deployment state locking — what it protects, how it behaves across different backends, and how to manage stuck locks.
Why locking exists
Running strata deploy run mutates live infrastructure. If two pipelines target the
same deployment at the same time — a hotfix and a scheduled release, for example — they
will race to write Terraform state and you will end up with corrupted state or a
partially-applied change.
Locking serialises those runs. The first caller acquires the lock; the second waits (or fails fast) until it is released.
How the lock fits into deploy run
strata deploy run
│
├─ _should_lock() ← is locking enabled? is strategy != "delegate"?
│
├─ _resolve_lock_backend()← find the Terraform backend for the stages being run
│
├─ _acquire_lock() ← write the lock record, start polling if already held
│ │
│ └─ (blocks up to wait_timeout, then exits with code 3)
│
├─ [ stage 1 … stage N ] ← all stages run inside a try/finally
│
└─ _release_lock() ← always runs, even on failure or Ctrl-C
The lock is held for the entire pipeline — from the first stage until the last one finishes or fails.
Configuring locking
Add a locking block to your deployment’s spec:
spec:
locking:
enabled: true
strategy: wrap # "wrap" | "delegate"
wait_timeout: "5m" # how long to poll before giving up
Field |
Default |
Description |
|---|---|---|
|
|
Set to |
|
|
|
|
|
Duration to wait for a held lock before aborting. Accepts |
Backend selection
strata selects the lock backend automatically from the Terraform backend configured on the provisioner your stages reference. No extra config is needed.
Terraform backend |
Lock backend |
Where the lock is stored |
|---|---|---|
|
Azure Blob lease |
A blob named |
|
Terraform Cloud workspace lock |
TFC’s native lock API |
|
Consul KV session |
|
|
File lock |
|
|
(Phase 3 — not yet implemented) |
— |
End-to-end example — azurerm backend
1. Deployment file
apiVersion: strata.huybrechts.xyz/v1
kind: deployment
meta:
name: xyz-production
spec:
locking:
enabled: true
strategy: wrap
wait_timeout: "2m"
stages:
- name: production
provisioner: xyz-infra
The provisioner xyz-infra uses an azurerm Terraform backend:
backend:
type: azurerm
configuration:
resource_group_name: rg-strata-state
storage_account_name: strastate001
container_name: tfstate
key: xyz-production.tfstate
2. Lock acquired
$ strata deploy run --deployment xyz-production
[INFO] Acquiring deployment lock for 'xyz-production' (azurerm) ...
[INFO] Lock acquired: a3f2c1d0-… held by ci-bot@runner-07
strata:
Obtains an Azure bearer token via
az account get-access-tokenCreates (or opens) the blob
strata-lock/xyz-production.lockAcquires an infinite lease on the blob — no other process can write or delete it
Writes the lock record as blob content:
{
"lock_id": "a3f2c1d0-...",
"deployment": "xyz-production",
"holder": "ci-bot@runner-07",
"hostname": "runner-07",
"pid": 12345,
"acquired_at": "2026-06-16T08:31:00Z",
"reason": "deploy run"
}
Appends the same record to the local history file:
.strata/locks/azurerm-xyz-production.lock.history
3. Concurrent run is blocked
A second pipeline runs at the same time:
$ strata deploy run --deployment xyz-production
[INFO] Acquiring deployment lock for 'xyz-production' (azurerm) ...
[WARN] Lock held by 'ci-bot@runner-07' — retrying (45s / 120s) ...
[WARN] Lock held by 'ci-bot@runner-07' — retrying (55s / 120s) ...
Azure returns 409 Conflict on every lease attempt. strata reads the blob to show who
holds the lock, then sleeps and retries until wait_timeout elapses. If the timeout
expires, it exits with code 3 (validation/lock failure).
4. Stages execute
All stages run while the lease is held. The manifest records lock_ref.lock_id so you
can correlate a run record with its lock history.
5. Lock released
[INFO] Deployment stages complete.
[INFO] Lock released: a3f2c1d0-…
strata breaks the lease and deletes the blob. A released_at timestamp is appended to
the history file.
What happens when a run crashes mid-deploy
The _release_lock() call is inside a finally block — it runs even if a stage fails
or the process receives a signal.
If the process is killed hard (OOM kill, node reboot):
azurerm — the infinite lease remains.
strata deploy lock release --forcebreaks it via the management API.TFC — TFC’s own lock record stays.
--forceunlocks via the Runs API.Consul — the session TTL is 8 hours. If strata does not renew it, Consul automatically deletes the key when the TTL expires.
--forcedestroys the session immediately.local — the OS releases the
flockwhen the process dies.
Manual lock management
Check who holds the lock
strata deploy lock status --deployment xyz-production
Deployment : xyz-production
Lock ID : a3f2c1d0-...
Holder : ci-bot@runner-07
Hostname : runner-07
PID : 12345
Acquired : 2026-06-16T08:31:00Z
Reason : deploy run
Returns exit code 0 if locked, 3 if not.
Force-release a stuck lock
strata deploy lock release --deployment xyz-production --force
Use this when a pipeline died and left the lock held. The command writes an audit entry to the history file before breaking the lock.
View the audit trail
strata deploy lock history --deployment xyz-production --last 10
Reads .strata/locks/{backend}-{deployment}.lock.history (NDJSON) and prints the most
recent events in reverse chronological order.
The delegate strategy
Some backends handle their own concurrency:
Terraform Cloud — the TFC run queue serialises applies natively.
Atlantis — holds a per-repo lock while a plan/apply is in progress.
Set strategy: delegate to skip strata’s lock entirely and rely on the backend’s
built-in mechanism:
spec:
locking:
enabled: true
strategy: delegate
strata will not acquire, poll, or release any lock. strata deploy lock status will
return “not locked” even when a TFC run is in progress — use the TFC UI or API to check
run queue status in that case.
Dry runs never lock
strata deploy run --dry-run skips locking unconditionally. Dry runs are read-only
planning operations and should never block or be blocked by an in-flight apply.