#221: offline update path incl. migrations, rehearsed with rollback #291

Merged
fable-5 merged 1 commits from issue-221-offline-update into main 2026-07-31 17:43:25 +02:00
6 changed files with 183 additions and 6 deletions

View File

@ -62,6 +62,26 @@ documented mirror or update path.
offline (`CI=1` skips the fetch, as in CI); for an offline ZIP build offline (`CI=1` skips the fetch, as in CI); for an offline ZIP build
the tarball is pre-seeded into `packages/plugins/drawio/vendor/`. the tarball is pre-seeded into `packages/plugins/drawio/vendor/`.
## Decisions taken in #220 and #221
- **The airgap claim is proven** (decision 5 executed): a full deployment
ran in an environment with no egress path at all; the complete capture
shows the application makes exactly one outbound attempt — SMTP — which
fails contained in the outbox while the instance stays functional.
Evidence: `docs/vs-nfd/97-isolationslauf-protokoll.md`. The run
surfaced and fixed one real defect (#288, restore on partitioned
tables).
- **The update path is defined and rehearsed** (decision 6 executed):
procedure in `docs/operations/update-runbook.md`; rehearsed in the same
isolated environment including one deliberate failed-update rollback.
Migration irreversibility is stated explicitly: failed migrations roll
back their own transaction but block every further `migrate deploy`
(P3009) until `migrate resolve --rolled-back`; successfully applied,
semantically irreversible migrations have exactly one way back — the
pre-update backup set. No rolling updates on a compose stage; updates
happen in a short maintenance window. Evidence:
`docs/vs-nfd/98-update-rollback-protokoll.md`.
## Consequences ## Consequences
- Digest pinning creates recurring maintenance: security updates now - Digest pinning creates recurring maintenance: security updates now

View File

@ -28,6 +28,10 @@ Consistency model (ADR 0015): the volume archive is taken minutes after the
dump — a page referencing a file uploaded in between shows a missing image, dump — a page referencing a file uploaded in between shows a missing image,
never corruption. never corruption.
Restore is also the designated way back from an update whose migrations
succeeded but must be undone — the pre-update backup set is step 3 of
`docs/operations/update-runbook.md` (#221).
**Relocation to a new host:** provision the stage directory (compose + **Relocation to a new host:** provision the stage directory (compose +
`.env`, deploy/stages.md), start only `db` and `backup` `.env`, deploy/stages.md), start only `db` and `backup`
(`docker compose up -d db backup`), copy the set into the backups volume (`docker compose up -d db backup`), copy the set into the backups volume

View File

@ -0,0 +1,69 @@
# Update runbook (ADR 0024, issue #221)
How to update a Dorfteich stage — connected or airgapped — and how to get
back when an update fails. Rehearsed end-to-end (including one deliberate
failed-update rollback) in the isolated environment of #220; evidence:
`docs/vs-nfd/98-update-rollback-protokoll.md`.
## Procedure
On the stage host, from the stage directory (`/srv/DOCKER/dorfteich-<stage>/`):
1. **Obtain the update.** Connected: `docker compose pull` after step 3.
Airgapped: mirror the release images into the internal registry —
generated image list + digest-preserving copy per `deploy/stages.md`
§5b (procedure evidence: `docs/vs-nfd/95-mirror-protokoll.md`).
2. **Verify it.** Third-party images are digest-pinned in the compose
file — the pin **is** the verification. Own images: compare the
mirrored digest against the release digest before switching `TAG`.
Release notes state manual steps and carry the `migration` label when
a release contains migrations.
3. **Back up first.** Trigger a backup run (admin UI or
`POST /api/v1/admin/system/backup/run`) and confirm the new set id in
the sets list. The pre-update set is the guaranteed way back.
4. **Apply.** Set the new `TAG` in the stage `.env`, then
`docker compose up -d`. All services are replaced together — there is
**no rolling update** on a compose stage; plan a short maintenance
window (seconds to low minutes). The api applies migrations on start
(`MIGRATE_ON_START`, `prisma migrate deploy`); web/api/collab version
skew therefore lasts only for the container replacement itself and is
not a supported operating state.
5. **Verify health.** `/readyz` fully green; spot-check a page, a file
download, and one export.
6. **Roll back if needed** — see below, the path depends on what failed.
## Migration behaviour (explicit)
`prisma migrate deploy` applies pending migrations in order, each in its
own transaction, and stops at the first failure:
- **The failing migration itself is rolled back** — its partial DDL/DML
does not persist.
- **It is recorded as `failed` in `_prisma_migrations`** and from then on
every `migrate deploy` aborts with error P3009 — including the one in a
re-deployed **old** image. A plain image rollback alone therefore does
not recover a failed migration; clear the record first:
```sh
docker compose run --rm --no-deps api \
node node_modules/prisma/build/index.js migrate resolve \
--rolled-back <migration-name>
```
- **Migrations that succeeded are not undone by an image rollback.** The
supported downgrade window is one minor release
(`docs/architecture/operations.md` §Update strategy): schema additions
tolerate the previous minor. Anything older, or a migration that is
semantically irreversible (dropped/rewritten data), has exactly one way
back: **restore the pre-update backup set**
(`docs/operations/restore-runbook.md`) — which is why step 3 is not
optional. `prisma migrate reset` is not part of any procedure here.
## Rollback paths
| Failure | Way back |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| App misbehaves, no migration in the release | previous `TAG` in `.env`, `docker compose up -d` |
| Migration failed (api restarts on P3009) | `migrate resolve --rolled-back <name>` (see above), then previous `TAG` + `up -d`; verify `/readyz` and content |
| Migration succeeded but must be undone | restore the pre-update set (`restore-runbook.md`), then previous `TAG` + `up -d` |
| Update bundle broken / images fail to start | previous `TAG` + `up -d` — the db was never touched before the api booted |

View File

@ -75,7 +75,8 @@ gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung
Prebuilt-Images) · 23 AT · #219 Prebuilt-Images) · 23 AT · #219
- [x] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220 - [x] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220
`97-isolationslauf-protokoll.md` `97-isolationslauf-protokoll.md`
- [ ] Offline-Update-Pfad inkl. Migrationen · 23 AT · #221 - [x] Offline-Update-Pfad inkl. Migrationen · 23 AT · #221
`docs/operations/update-runbook.md` + `98-update-rollback-protokoll.md`
--- ---

View File

@ -71,14 +71,18 @@ dokumentiert die realen Instanzen).
Egress bleibt die Instanz voll funktionsfähig, nur die Egress bleibt die Instanz voll funktionsfähig, nur die
Mail-Zustellung scheitert kontrolliert (Outbox, 5 Versuche, dann Mail-Zustellung scheitert kontrolliert (Outbox, 5 Versuche, dann
FAILED). Nachweis: `97-isolationslauf-protokoll.md`. FAILED). Nachweis: `97-isolationslauf-protokoll.md`.
- ⏳ offen: Offline-Update-Pfad (#221); Meilenstein M28. Bereits - **Offline-Update-Pfad (#221): ✅ live verifiziert.** Prozedur
vorhanden als Grundlage: alle Dritt-Images digest-gepinnt (#203), ein `docs/operations/update-runbook.md`; in der isolierten Umgebung
geprobt inkl. eines absichtlichen Migrations-Fehlschlags mit
Rollback. Nachweis: `98-update-rollback-protokoll.md`. Grundlage
weiterhin: alle Dritt-Images digest-gepinnt (#203), ein
authoritativer Node-Pin (#236), SBOMs je Release (#202). authoritativer Node-Pin (#236), SBOMs je Release (#202).
## 2 Update und Rollback ## 2 Update und Rollback
Referenz: `docs/architecture/operations.md` §Update strategy, Referenz: `docs/operations/update-runbook.md` (maßgeblich, inkl.
`deploy/stages.md`. explizitem Migrationsverhalten und Rollback-Pfaden),
`docs/architecture/operations.md` §Update strategy, `deploy/stages.md`.
- **Stages:** Merge auf `main` → CD baut Images, deployt Test, führt - **Stages:** Merge auf `main` → CD baut Images, deployt Test, führt
Smoke-Tests aus, promotet Int. CD synct **keine** Compose-Dateien — Smoke-Tests aus, promotet Int. CD synct **keine** Compose-Dateien —
@ -101,7 +105,9 @@ Referenz: `docs/architecture/operations.md` §Update strategy,
(imagetools inspect → Compose-Referenz ändern → CI bestätigt → Stage- (imagetools inspect → Compose-Referenz ändern → CI bestätigt → Stage-
Composes von Hand nachziehen → `docker inspect` verifiziert). Composes von Hand nachziehen → `docker inspect` verifiziert).
Belegstufe: ✅ erprobt (Rollout #203 am 31.07.2026). Belegstufe: ✅ erprobt (Rollout #203 am 31.07.2026).
- **Offline-Update:** ⏳ offen (#221, M28). - **Offline-Update:** ✅ geprobt (#221) — Bundle-Beschaffung über das
Mirror-Verfahren (§1), dann identische Prozedur; Fehlschlag-Rollback
einmal durchgespielt (`98-update-rollback-protokoll.md`).
## 3 Backup und Restore ## 3 Backup und Restore

View File

@ -0,0 +1,77 @@
# Update-/Rollback-Protokoll — Probe in der isolierten Umgebung (Issue #221)
Nachweis für Maßnahmenplan P1-3 (ADR 0024): der Update-Pfad inklusive
Migrationen und geprobtem Fehlschlag-Rollback, ausgeführt in derselben
netzisolierten Umgebung wie der Isolationslauf
(`97-isolationslauf-protokoll.md` — Compose-Projekt `dorfteich-isolated`,
alle Netze `internal: true`, kein Egress-Pfad). Belegstufe: **live
verifiziert**. Die daraus destillierte Prozedur steht in
`docs/operations/update-runbook.md`.
- **Datum:** 2026-07-31
- **Ausgangsstand („v1"):** Images `dorfteich-iso-*:isolated`
(`main@a758c9d`, backup mit #288-Fix aus `4f6596e`), Instanz mit
Inhalt aus dem Isolationslauf.
- **Update-Ziel („v2"):** Images `dorfteich-iso-*:isolated-v2`, gebaut
aus dem Kettenstand `ccffcaa` (#288 + #220) — als Stellvertreter eines
regulären Release-Bundles; die Beschaffung/Digest-Prüfung eines echten
Bundles ist das Mirror-Verfahren (#218, `95-mirror-protokoll.md`) und
wurde dort bereits nachgewiesen.
- **Fehlschlag-Kandidat („bad"):** api-Image mit einer absichtlich
fehlschlagenden Migration
(`20260731230000_deliberately_failing_update`: `ALTER TABLE` +
`SELECT 1/0`), nur für die Probe gebaut, nie committet.
## Phase A — reguläres Update
| Schritt | Ergebnis |
| ------------------- | --------------------------------------------------------------------------------------------- |
| Pre-Update-Backup | ✅ Set `20260731-150833` über die Admin-API erzeugt und in der Set-Liste bestätigt |
| `TAG` → v2, `up -d` | ✅ alle vier eigenen Services zusammen ersetzt (Ausfallfenster Sekunden, kein Rolling Update) |
| Migrationen | ✅ migrate-on-start läuft durch (dieses „Release" enthält keine neuen Migrationen) |
| `/readyz` | ✅ vollständig grün |
| Inhalt | ✅ Seite „Isolationstest Seite" unverändert vorhanden |
## Phase B — geprobter Fehlschlag + Rollback
1. **Bad-Update einspielen** (`TAG` → bad, `up -d`): api scheitert beim
Boot in `migrate deploy`**P3018**, `ERROR: division by zero` bei
`20260731230000_deliberately_failing_update`; Container geht in den
Restart-Loop, jeder Folge-Boot meldet **P3009** („migrate found
failed migrations").
2. **Datenbank-Zustand geprüft:** die Migration steht als `failed` in
`_prisma_migrations` (Logs erfasst), aber `broken_column` existiert
**nicht** — die per-Migration-Transaktion hat den Schema-Eingriff
zurückgerollt. Genau das dokumentierte Verhalten.
3. **Image-Rollback allein reicht nicht** (bewusst vorgeführt): `TAG`
zurück auf v2, `up -d` — der alte Stand scheitert weiter mit
**P3009** („The 20260731230000_deliberately_failing_update migration
started at 2026-07-31 15:10:50 UTC failed").
4. **Recovery wie im Runbook:**
`migrate resolve --rolled-back 20260731230000_deliberately_failing_update`
(im api-Image ausgeführt) → „marked as rolled back" → api-Neustart:
`/readyz` vollständig grün, Inhalt unverändert, Migrations-Row trägt
`rolled_back_at`.
5. **Alternativer Weg zurück** (für semantisch irreversible, aber
erfolgreich gelaufene Migrationen): Restore des Pre-Update-Sets — der
Restore-Pfad selbst ist im Isolationslauf verifiziert
(`97-isolationslauf-protokoll.md`, Fix #288).
## Versions-Skew (Akzeptanzkriterium)
Ein Compose-Stage kennt **kein Rolling Update**: `docker compose up -d`
ersetzt web/api/collab/backup zusammen; Mischbetrieb alter und neuer
Versionen ist kein unterstützter Betriebszustand und dauert nur die
Container-Ersetzung selbst (Sekunden). Für VS-Zonen heißt das: Update im
angekündigten Wartungsfenster; die Migrationen wendet ausschließlich der
neue api-Container beim Start an (`MIGRATE_ON_START`).
## Rückfluss
- **`docs/operations/update-runbook.md`** — die Prozedur (Beschaffen,
Prüfen, Sichern, Einspielen, Verifizieren, Rollback) inkl. explizitem
Migrationsverhalten; aus dieser Probe destilliert.
- **`docs/operations/restore-runbook.md`** — Querverweis: Restore als
einziger Weg zurück nach irreversiblen Migrationen.
- **#229** — Betriebshandbuch §1 (Airgap) und §2 (Update/Rollback)
verweisen auf Runbook und dieses Protokoll.