Kilolock 8-minute demo script
The narrative is one sentence: parallel apply on shared state, with history, rollback, and zero lock-in. Everything below supports that sentence; cut whatever doesn't.
Target length: 8 minutes. Each section's headline number is the approximate elapsed-time mark. Times include narration, not just the command. The commands themselves are short by design — every command on screen should be a one-liner an operator can read at a glance.
The script is written to be recorded with asciinema. Suggested record command:
asciinema rec \
--title 'Kilolock: parallel apply, history, rollback' \
--idle-time-limit 2 \
--command 'bash' \
kl-demo.cast
Output that gets too noisy for a recording (Terraform's progress spam, in particular) is intentionally not narrated below. Pipe through grep -E '^(Plan|Apply complete|Error)' when desired.
0:00 — Setup (do this BEFORE you hit record)
make demo-warmto restore the pre-bakedbig-statefixture(10k resources, ~5 seconds).
make demo-statusto confirm it's there.- Open two terminals side-by-side. Set
KL_DATABASE_URLin both, plus
KL_LOG_LEVEL=warnso the recordingisn't drowned in info logs.
cd examples/big-statein both.- Have
bin/klin$PATH(or usemake buildand./bin/kl).
The recording starts AFTER the warm restore. The audience should not see a 30-second pg_restore.
0:00 — The problem (45s)
Narrate over an empty terminal:
"This is what a Terraform module with 10,000 resources looks like in our state. Vanilla
terraform applyon this state takes about 45 minutes — even though I only want to change two of those resources. And while it's running, no other engineer on my team can run apply on this module, because Terraform holds a whole-state lock for the entire duration."
Run, in one terminal only:
cat examples/big-state/main.tf | head -30
Just enough to show the time_sleep.slow_a / slow_b resources.
0:45 — Parallel apply, the elevator pitch (2:00)
The headline beat: **plain terraform apply runs concurrently on the same state**. No kl CLI involvement on the operator side — the backend's optimistic commit-time merge is what makes the two POSTs succeed.
Before recording, make sure the demo state is in optimistic lock mode:
curl -sS -X POST "http://localhost:8090/v1/api/states/operator/default/config" \
-H "Authorization: Bearer $KL_CONTROL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"big-state","exclusive_locks":false}'
Run, simultaneously (start them within ~2 seconds of each other so they overlap; each time_sleep is 30s, which is the visible overlap window):
# Terminal 1
make parallel-tf-a
# Terminal 2
make parallel-tf-b
Each is shorthand for:
terraform apply -auto-approve -refresh=false \
-var=slow_a_version=$NEW -var=slow_b_version=$CURRENT
There is **no -target= flag**: each terminal runs from its own working directory (the make targets prepare /tmp/parallel-tf-{a,b}/ on first invocation) and passes both vars — its own as the new value, the other pinned to the trunk value observed at startup. Terraform's plan in each directory naturally produces a single-resource write set, and the backend's optimistic merge stitches the two POSTs into one new state version.
Quick FAQ if someone in the audience asks "why two directories":
"In a real workflow, two engineers have two checkouts of the module on different branches; the OS naturally gives each one its own
.terraform/cache. We're mimicking that here so the twoterraformprocesses don't fight over local state files in a single directory. The shared piece — and the whole point of this demo — is the backend state: both terminals POST to the samebig-stateand the backend merges them."
While the applies run, narrate:
"Two engineers, same state, different resources. Both running vanilla
terraform apply— no kl CLI, no plan-spec file, no reservations. Kilolock's backend lets multiple operators hold the HTTP-backend lock at the same time, then arbitrates at commit time: if the two write sets are disjoint, both POSTs merge and both commits succeed. If they touch the same address, the second one gets a 409 with the exact address in conflict. Vanilla Terraform on this same 10,000-resource state with the default S3 backend would have made me wait for my colleague's whole-state lock to release."
Both finish in ~30s total wall clock (vs ~60s serialized). Show the elapsed times.
If the audience asks how the backend knew which addresses each operator wrote, point at:
kl history big-state --limit=5
"Each lock records the trunk serial the operator read at lock time. At POST the backend diffs
state at source_serialvs the incoming state — that's the write set. If that intersects the diff ofstate at source_serialvs current trunk, conflict. Otherwise we 3-way-merge and commit."
Advanced beat — explicit reservations (optional, swap out
the plain-terraform beat above with this if the audience is
infra-platform-engineer-y and cares about the wait UI)
# Terminal 1
make parallel-demo-a
# Terminal 2
make parallel-demo-b
Same disjoint-write-set outcome, but goes through kl apply and the pessimistic reservation matrix instead of the optimistic POST merge. The reservation matrix is what makes the wait-feedback demo (bonus beat below) work.
2:45 — History (1:00)
kl history big-state --limit=5
Point out:
- The
*marker on the latest version. - Two new versions appeared, source=
apply, different actors. - The serial monotonically increased; no serial conflict.
"Every write goes through the same append-only history. Nothing ever gets overwritten. Every state version is recoverable, by any operator, at any time."
3:45 — Rollback (2:00)
kl rollback big-state --to=@2
Pause on the output. Walk through it:
- "It's a dry run by default. It hasn't changed anything yet."
- "It tells me which addresses would be added back, removed,
or have their attributes change."
- "Most importantly: it warns me that rolling back the state
is not the same as rolling back the infrastructure. If I
rolled this back without reverting the HCL, I'd orphan
resources in the cloud."
Now actually do it:
kl rollback big-state --to=@2 --apply --yes
Run history again to show the new source=rollback row at the top.
kl history big-state --limit=3
"The rollback is itself just another append. The old current version is still in history. Nothing destructive."
5:45 — Coexistence with vanilla Terraform (1:30)
This is the "no lock-in" beat. Critical for the trust story.
# Show the backend config — it's a plain HTTP backend.
cat examples/big-state/backend.tf
# A vanilla terraform plan still works.
terraform plan -refresh=false | tail -5
"Kilolock exposes Terraform's standard HTTP backend protocol. Vanilla
terraform planandterraform applywork against it with zero changes. You're not locked in — the day you decide Kilolock isn't for you, point the backend back at S3 withterraform init -migrate-stateand you're out. Your state is still vanilla Terraform JSON, exactly the way Terraform wrote it."
7:15 — Close (45s)
"What you saw: parallel apply on shared state, append-only history, dry-run rollback, full compatibility with vanilla Terraform. Built on Postgres so you can SQL-query your infrastructure the same way you query everything else. Open source. Self-hosted today. Hosted offering coming. Try it:
git clone github.com/kilolockio/kilolock && make db-up && make build && make demo-warm."
Bonus beat — reservation wait feedback (optional, ~60 s)
If the parallel-apply demo lands and you have time, follow it with the wait-feedback variant: two terminals, both bumping the SAME address. The second terminal will not fail — it will sit in a wait loop and stream a live "blocked by 1 reservation(s)" block until the first one commits, then proceed.
# Terminal 1 (the blocker):
make wait-demo-blocker
# Terminal 2 (the waiter), within ~5 seconds:
make wait-demo-waiter
What the audience sees on terminal 2:
[apply: waiting 4s/2m] blocked by 1 reservation(s) on "big-state":
time_sleep.slow_a write held by davidkubec@cli (apply 7c3a91…, lease 14m23s)
retrying in 2s
The block refreshes every ~5 s (and immediately whenever the conflict set CHANGES — e.g. when the blocker releases). Once the blocker commits, terminal 2 logs "reservation conflict cleared" and runs its own apply.
If you want the wait visible in a single terminal (e.g. for an asciinema cut), use make wait-demo instead — it backgrounds the blocker to /tmp/wait-demo-blocker.log and foregrounds the waiter so the wait block streams live.
The same --wait-timeout flag accepts 0 for fail-fast (CI's preference). Mention this if the audience asks "what about non-interactive runs".
Recovery moves
If parallel apply demos hang or conflict (state drift, leaked locks, etc.):
# Drop the v1 HTTP-backend lock if a previous terraform run
# leaked it.
psql "$KL_DATABASE_URL" \
-c "DELETE FROM state_locks WHERE state_id = (SELECT id FROM states WHERE name = 'big-state');"
# Drop any leaked reservations / apply runs.
psql "$KL_DATABASE_URL" \
-c "DELETE FROM resource_reservations rr USING apply_runs ar
WHERE rr.apply_run_id = ar.id AND ar.state_id = (SELECT id FROM states WHERE name = 'big-state');"
psql "$KL_DATABASE_URL" \
-c "DELETE FROM apply_runs WHERE state_id = (SELECT id FROM states WHERE name = 'big-state');"
If big-state itself gets wiped, restore with make demo-warm. The integration test suite preserves it by default (KL_TEST_PROTECT_STATES=big-state) but a manual db-reset or DROP SCHEMA will still take it out.
Cuts (if you're over time)
In priority order — cut from the top:
- The
cat backend.tfandterraform plansegment (saves 90s).Move the lock-in promise into the close.
- The history walk (saves 60s). Move it into a single "and
yes there's history too" beat between parallel apply and
rollback.
- The setup narration (saves 45s). Walk in mid-state.
Cuts you cannot make
The orphan warning in the rollback dry-run. That single sentence is the most-likely-misused feature of the whole project surface; if you cut it, somebody will deploy rollback to prod without reading the manual.