Home > Docs > Runbooks > DR Drill
DR Drill Runbook (CSA-0073)¶
Note
Quick Summary: Quarterly disaster-recovery drill that exercises Cosmos PITR restore, ADLS soft-delete restore, and Key Vault secret recovery against the LIVE Loom estate (canary-only, scratch restore targets), plus the scratch-subscription Storage-failover and Bicep rollback rehearsals. Automated via .github/workflows/dr-drill.yml on the first day of each quarter with on-demand workflow_dispatch.
This runbook operationalises docs/DR.md §4 ("Failover Readiness — Quarterly Drill"). The parent DR document is the authoritative reference for RPO/RTO targets, region pairing, and the real failover procedure; this document covers how we rehearse recoverability safely so the runbook stays honest instead of rotting between real incidents.
loom-next-level WS-DR (2026-07): DR1–DR3 deepened this framework from echo-stubs into real drills with validators that assert real restored state (document counts, byte hashes, secret values) and emit per-scenario RPO/RTO reports (
test-results/dr/<scenario>-<id>.json, uploaded as run artifacts). DR4 (Phase 2) adds the run-level summary, Blob persistence for the Health & Reliability hub's DR-drills tab, and action-group alerting.
📑 Table of Contents¶
- 🎯 1. Objectives
- 📅 2. Cadence
- 🔐 3. Required Azure Permissions
- 🧪 4. Scenarios
- 🚀 5. Triggering a Drill
- 📊 6. Results & Reporting
- 📐 7. RPO / RTO Expectations
- 🧰 8. Follow-up Work
- 🔗 9. Related Documentation
🎯 1. Objectives¶
A DR drill is successful when:
- Every scenario executes end-to-end without human intervention during the automated portion.
- Observed RTO per scenario is within the target documented in §7 (and cross-referenced to
docs/DR.md§1). - Each deepened scenario's validator report (
test-results/dr/<scenario>-<drillId>.json) showsok: true— the validators assert real restored state (restored document counts vs a live snapshot, byte-for-byte canary hashes, recovered secret values), never exit-code-only az calls. - Any deviation, flake, or gap is captured as a follow-up GitHub issue labelled
dr-drill-followupso the runbook can be updated before the next drill.
Drills never mutate resources that real workloads depend on. The deepened scenarios touch the live estate only through:
- read-only restore sources (Cosmos PITR restores never modify the source account),
- namespaced canaries (
drdrill-<id>filesystems / secrets) that are swept inalways()steps, and - per-run scratch resource groups (
rg-csa-loom-drdrill-<cloud>-<id>) that are deleted — and asserted deleted — inalways().
📅 2. Cadence¶
- Scheduled:
cron: "0 10 1 1,4,7,10 *"— 10:00 UTC on the 1st day of Jan, Apr, Jul, and Oct. - On-demand: via GitHub → Actions →
dr-drill→ Run workflow. Anyone withwriteaccess to the repository can trigger a drill. - Ad hoc post-incident: after any real DR event, re-run the relevant scenario within five business days to confirm the post-incident patch is effective.
🔐 3. Required Azure Permissions¶
Two credential sets are in play:
Deepened live-estate scenarios (cosmos-pitr-restore, adls-softdelete-restore, keyvault-restore) authenticate with the same secret-based creds block the proven loom-roll-and-validate.yml uses — AZURE_CLIENT_ID / AZURE_CLIENT_SECRET / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID (Gov: the AZURE_GOV_* set + the cloud: gov and live_rg dispatch inputs). That SP needs:
| Scope | Role / permission | Why |
|---|---|---|
| live subscription(s) | CosmosRestoreOperator + restorableDatabaseAccounts/*/read (Contributor covers both) | Trigger PITR restores of the live accounts |
| live subscription(s) | RG create/delete (Contributor) | Per-run scratch RGs rg-csa-loom-drdrill-* |
lake storage account (saloom*) | Storage Blob Data Contributor (data-plane) + Contributor (firewall window) | adls-softdelete-restore canary drill |
admin-plane vault (kv-loom-*) | Key Vault Secrets Officer (RBAC) + Contributor (firewall window) | keyvault-restore canary drill (set/delete/recover/purge-attempt) |
| Microsoft Graph | ability to az ad sp show --id <its own appId> (default for most SPs) | Resolve its object id to self-grant Cosmos data-plane on scratch accts |
The Cosmos data-plane role on the restored scratch accounts is granted by the drill itself (Cosmos DB Built-in Data Contributor, scope /) — no standing data-plane grant on the live Loom store is needed or used.
Legacy scratch scenarios (storage-failover, bicep-rollback) keep the original scratch-subscription OIDC login: the SP behind AZURE_CLIENT_ID needs Contributor on AZURE_SUBSCRIPTION_ID_SCRATCH. The scratch scenarios must not use the production subscription secret.
Warning
Real storage-account failovers drop the account to LRS and the replication seed can take hours to rebuild. That is fine on a scratch account; do not run the storage-failover scenario against staging or prod. (The new adls-softdelete-restore scenario is canary-only and IS safe on the live lake.)
PE-only resources. The lake and vault are publicNetworkAccess: Disabled + default-action Deny. The drill opens a single-IP firewall window for the runner (default-action stays Deny — never Allow), polls for data-plane reachability, and re-locks in always() — the same pattern csa-loom-post-deploy-bootstrap.yml uses. Restored scratch Cosmos accounts are created with public access Enabled (they hold PITR copies of Loom metadata for minutes, are AAD-data-plane-only, and are deleted in always() with an az group exists → false receipt).
🧪 4. Scenarios¶
Each scenario maps to one job in .github/workflows/dr-drill.yml. Scenarios can be run individually via the scenarios input (see §5).
4.1 cosmos-pitr-restore¶
(formerly cosmos-failover — the alias is still accepted by the scenarios input. Renamed because the shipped Loom store is a single-region, PE-only account — see docs/fiab/runbooks/cosmos-pitr-restore.md — so a failover-priority swap is not its real recoverability primitive; point-in-time restore is. The multi-region failover rehearsal remains documented in docs/DR.md §3 for the reference architecture.)
Purpose: Prove the Loom metadata store — and the landing-zone graph/vector Cosmos pair, which was previously in no drill's validation set — is actually restorable inside the PITR window (Continuous30Days on the admin store per DR0; Continuous7Days on the landing-zone accounts).
Behaviour (implemented; per docs/fiab/runbooks/cosmos-pitr-restore.md):
- Discover the admin-plane account carrying the
loomdatabase, plus thecosmos-loom-vec-*/cosmos-loom-gremlin-*landing-zone accounts (Resource Graph; skipped with a note if the estate has none). - Snapshot live per-collection counts via the Azure Monitor
DocumentCountmetric (the live account is PE-only; the metric is the reachable live signal from a hosted runner). az cosmosdb restoreeach account tonow − 10 mininto the per-run scratch RG (rg-csa-loom-drdrill-<cloud>-<id>, same subscription as each source — PITR cannot cross subscriptions), restores running in parallel.- Grant the drill SP
Cosmos DB Built-in Data Contributoron the restored SQL accounts. - Run
scripts/csa-loom/dr/validate-cosmos-restore.mjs. always(): delete the scratch RG(s) and assertaz group exists → false(cost control receipt).
Verification (the validator's real-state checks):
- Restored
loomdatabase lists its containers; every container is counted with cross-partition data-plane reads. - Floors:
loom-workspaces ≥ 1,env-config ≥ 1(overridable viaFLOORS_JSON). - Restored counts within a tolerance band (±max(25, 10%)) of the live metric snapshot, where datapoints exist.
- Schema probes: a sampled doc from
loom-workspaces(id,tenantId) andenv-config(id) deserializes with its identity fields. - Vector account: restored
loom-vectors/docs-veccontainer readable + counted. Gremlin account: restoredloom-graphdatabase +defaultgraph present (structure-level; gremlin data-plane needs an in-VNet client — documented limitation until DR4). rpoEvidence: restore timestamp + restore-point age; end-to-end duration = observed RTO.
Rollback path: none needed on the live side — restore never touches the source account. If teardown fails, delete rg-csa-loom-drdrill-<cloud>-<id> manually (it is tagged purpose=dr-drill autodelete=true).
4.2 storage-failover¶
Purpose: Validate customer-initiated failover on an RA-GRS storage account (az storage account failover) and the subsequent re-enablement of geo-replication. Still a stub (scratch subscription only) — see §8. Distinct from §4.5: this rehearses regional failover on a scratch RA-GRS account; §4.5 proves restore on the live (ZRS/HNS) lake.
Expected behaviour / verification / rollback: unchanged from the original CSA-0073 design — provision scratch Standard_RAGRS account, failover, verify sentinel blob, reset SKU. Do not run against staging or prod.
4.3 keyvault-restore¶
Purpose: Validate soft-delete + purge-protection recovery on the live admin-plane vault (kv-loom-* — enableSoftDelete, 90-day retention, enablePurgeProtection per keyvault.bicep).
Behaviour (implemented — scripts/csa-loom/dr/validate-kv-recovery.mjs):
- Posture asserts: soft delete ON, retention ≥ 7d, purge protection ON.
- Canary 1 (
drdrill-canary-<id>): set (random value) → delete → appears inlist-deletedwith ascheduledPurgeDate→recover→ readable again → value byte-for-byte equals the original. - Canary 2 (
drdrill-purge-<id>): set → delete →az keyvault secret purge→ must be rejected by purge protection (the expected error is captured in the report). It stays soft-deleted and auto-purges at the end of the retention window — harmless, namespaced. - Canary 1 is re-deleted so the vault stays swept.
Verification: recovered value equality + the purge rejection are the two hard gates; recovery duration is recorded as RTO evidence (target < 15 min per §7).
Rollback path: none required — recovery is the happy path. If recovery fails, the canary remains in soft-delete state and is purged automatically at the end of the retention window.
4.4 bicep-rollback¶
Purpose: Validate that a previous main.bicep can be redeployed and that az deployment group what-if produces a clean diff. Still a stub — see §8. This scenario is also the rollback story that new Azure Functions reference (loom-next-level master Function standard).
Expected behaviour / verification / rollback: unchanged from the original CSA-0073 design (deploy HEAD~1, what-if, deploy, return to HEAD, assert idempotent noChange).
4.5 adls-softdelete-restore¶
Redesign note (binding). The original WS-DR spec named this scenario
adls-versioning-restore(canary + prior-version promotion). That premise was corrected by DR0 (#2414, Learn-grounded): blob versioning and blob point-in-time restore (restorePolicy) are both unsupported on HNS-enabled (ADLS Gen2) accounts, and the Loom lake is HNS by design (storage.bicepisHnsEnabled: true, guarded byhnsSupportsVersioning). DR0 shipped the corrected posture — blob + container soft delete (recycleRetentionDayswindow) + change feed — so this drill validates the restore path that actually exists: soft-delete undelete, not version promotion.
Purpose: Prove lake data is recoverable via the shipped soft-delete posture, on the live lake, canary-only.
Behaviour (implemented — scripts/csa-loom/dr/validate-adls-restore.mjs):
- Posture asserts (live ARM):
isHnsEnabled: true; blob soft delete ON with its retention window; container soft delete ON; change feed ON; versioning correctly OFF (it being on would be posture drift — the combination is ARM-invalid on HNS). - Canary drill in a namespaced filesystem
drdrill-<id>: uploadcanary.txt(sha256 recorded) → overwrite v2 → delete →az storage fs list-deleted-path→az storage fs undelete-path→ download → byte-for-byte hash match. Restore duration recorded. - Container-level safety net: the drill filesystem is deleted and asserted to appear in the soft-deleted container list (
az storage container list --include-deleted). always(): the canary filesystem is removed and the lake firewall window is re-locked.
Verification: the hash match after undelete is the hard gate; the posture block ensures a mis-provisioned estate (soft delete off) fails loudly rather than silently losing its restore window.
Rollback path: none — the drill only ever touches its namespaced canary filesystem. If cleanup fails, delete the drdrill-<id> filesystem manually; it contains only canary text.
🚀 5. Triggering a Drill¶
Scheduled run¶
No action required. The cron entry in dr-drill.yml fires on the 1st of each quarter at 10:00 UTC (Commercial estate, all scenarios). The report job posts results even if one or more scenarios fail.
Manual run (all scenarios)¶
Manual run (single scenario)¶
Use the scenarios input to restrict to one of: cosmos-pitr-restore (alias cosmos-failover), adls-softdelete-restore, keyvault-restore, storage-failover, bicep-rollback. Comma-separated values are accepted.
Running against Azure Government¶
The deepened scenarios accept a cloud input. Gov requires the AZURE_GOV_* secrets (same SP as gov-console-roll.yml, csa-loom-gov-deploy) plus the Gov admin-plane RG name:
gh workflow run dr-drill.yml \
-f scenarios=keyvault-restore \
-f cloud=gov \
-f live_rg=<gov admin-plane RG>
az cosmosdb restore, storage soft-delete, and KV recovery are all supported in Azure Government (.us endpoints are resolved from the resources themselves, never hard-coded). The Gov SP needs the same §3 grants in the Gov tenant.
Running against staging¶
The environment input maps to the GitHub environment (approval gate). staging is allowed only for bicep-rollback and keyvault-restore and only with explicit sign-off from the on-call lead.
📊 6. Results & Reporting¶
- Each deepened scenario writes
test-results/dr/<scenario>-<drillId>.json— schema:{ drillId, scenario, cloud, startedAt, finishedAt, durationMs, ok, rpoEvidence, checks: [{ name, ok, ms, detail }] }— and uploads it as thedr-report-<scenario>artifact on the run. - The
reportjob aggregates per-scenarioneeds.<job>.resultand prints a summary line per scenario. - Any scenario returning
failuremust open a GitHub issue labelleddr-drill-followupwithin one business day. - DR4 (Phase 2) persists the reports to the
dr-drillsBlob container, renders them on the Health & Reliability hub's DR-drills tab, and wires failure alerting through the shared action group (LOOM_ALERT_ACTION_GROUP_ID).
📐 7. RPO / RTO Expectations¶
These expectations must stay aligned with docs/DR.md §1. If they drift, update both files in the same PR.
| Scenario | RPO (data-loss window) | RTO (recovery time) | Source of truth |
|---|---|---|---|
cosmos-pitr-restore | restore-point age (drill uses now − 10 min; window = backup tier: 30d admin store / 7d landing-zone) | < 2 h (restore + validate; observed duration in the report) | cosmos-pitr-restore.md + DR.md §1 |
adls-softdelete-restore | soft-delete window (recycleRetentionDays, default 30d) | < 15 min (undelete) | DR0 posture (storage.bicep) |
storage-failover | < 1 h | < 1 h | DR.md §1 (Data Lake Storage Silver + Gold, Critical tier) |
keyvault-restore | N/A (recovery from soft-delete, not replication) | < 15 min | DR.md §1 (Key Vault, Critical tier) |
bicep-rollback | N/A (IaC redeploy, no runtime data) | < 30 min | docs/ROLLBACK.md |
If a drill's observed RTO exceeds the target by > 25%, treat it as a finding and open a follow-up issue — do not silently accept drift.
🧰 8. Follow-up Work¶
Implemented by loom-next-level WS-DR (2026-07, DR1–DR3):
- Cosmos scenario wired end-to-end (PITR restore + validator —
scripts/csa-loom/dr/validate-cosmos-restore.mjs), including the landing-zone graph/vector accounts. - Key Vault scenario wired end-to-end (
scripts/csa-loom/dr/validate-kv-recovery.mjs). - ADLS soft-delete restore scenario added (
scripts/csa-loom/dr/validate-adls-restore.mjs) — redesigned from the unsupported versioning premise per DR0. - Per-scenario report artifacts with per-step durations (RPO/RTO evidence).
Still open:
- Implement
scripts/drill/storage-failover.sh(scratch RA-GRS failover rehearsal — §4.2). - Implement
scripts/drill/bicep-rollback.sh(§4.4). - DR4 (Phase 2): run-level
dr-summary-<id>.json, Blob persistence (dr-drillscontainer), the Health & Reliability hub DR-drills tab (+ "Run drill now"), failure alerting viaLOOM_ALERT_ACTION_GROUP_ID, dedup GitHub issues, and thesvc-dr-drillfreshness gate row. Thedr-drill-rbac.biceprole-assignment module for a dedicated drill principal also lands with DR4 (today the drill documents its grants in §3). - Publish per-step duration metrics to Log Analytics for cross-quarter trend analysis.
🔗 9. Related Documentation¶
- Disaster Recovery — Authoritative DR runbook (RPO/RTO, region pairing, real failover procedure)
- Loom shipped DR posture — per-component posture of the actual shipped platform
- Cosmos PITR restore runbook — the manual restore procedure the cosmos scenario automates a rehearsal of
- Rollback — Deployment rollback procedure
- Security Incident — Sibling operations runbook
- Multi-Region Deployment — Multi-region deployment guide
.github/workflows/dr-drill.yml— The drill workflow itself