[Infra][MinIO] Uploads-tree coordinated batch cutover — 7 services off /mnt/fusionio (epic #413) #529

Open
opened 2026-07-13 00:40:41 +00:00 by spikerj · 5 comments
Owner

Parent epic: #413. The coordinated batch — per the ordering constraint in docs/minio-storage-migration.md, the shared uploads tree hands files between services by path, so these cut over together after the standalone dirs prove the pattern:

backend API (staging writers: FileProcessingService, LessonVideoStagingService, UploadBookCommandHandler, CacheExternalUrlCommandHandler), upload-coordinator (StagingMirror — dual-write EXISTS), security-scanner (S3StagedObjectStore dual-read EXISTS, #493), metadata-extractor (dual-read EXISTS), file-movement (mirror EXISTS), blog-media-processor + lesson-video-processor (S3 code from their standalone tickets).

Cutover plan: flip Storage:UseS3=true everywhere in one deploy window → final mc mirror re-run → watch Seq/Jaeger → remove ALL /mnt/fusionio/spikersoft/uploads* binds from the seven stack files → relax placements to node.labels.spikersoft-app == true → verify tasks can land on 4090 (burst runbook) → retire the host path after a soak.

Blocked by: blogs, lesson-videos, dlls, quarantine standalone tickets + #493 completion.

Parent epic: #413. **The coordinated batch** — per the ordering constraint in `docs/minio-storage-migration.md`, the shared `uploads` tree hands files between services **by path**, so these cut over together after the standalone dirs prove the pattern: backend API (staging writers: `FileProcessingService`, `LessonVideoStagingService`, `UploadBookCommandHandler`, `CacheExternalUrlCommandHandler`), upload-coordinator (StagingMirror — dual-write EXISTS), security-scanner (S3StagedObjectStore dual-read EXISTS, #493), metadata-extractor (dual-read EXISTS), file-movement (mirror EXISTS), blog-media-processor + lesson-video-processor (S3 code from their standalone tickets). **Cutover plan:** flip `Storage:UseS3=true` everywhere in one deploy window → final `mc mirror` re-run → watch Seq/Jaeger → remove ALL `/mnt/fusionio/spikersoft/uploads*` binds from the seven stack files → relax placements to `node.labels.spikersoft-app == true` → verify tasks can land on 4090 (burst runbook) → retire the host path after a soak. Blocked by: blogs, lesson-videos, dlls, quarantine standalone tickets + #493 completion.
Author
Owner

Cutover prep landed as two PRs: backend #264 (deploy-secret guards for blog-media/lesson-video — the only batch members missing them) + infra #60 GATED (Storage__ env for those two stacks + docs/uploads-batch-cutover-runbook.md with the full phase 0-2 choreography, verification and rollback). All seven services' S3 code paths are now merged or in review; the remaining #529 work is the runbook's ops phases + a phase-2 bind-removal PR after the soak. Note from the audit: file-movement reuses metadata-svc — widen that policy to its four new buckets or issue a dedicated movement-svc key.

Cutover prep landed as two PRs: backend #264 (deploy-secret guards for blog-media/lesson-video — the only batch members missing them) + infra #60 GATED (Storage__ env for those two stacks + docs/uploads-batch-cutover-runbook.md with the full phase 0-2 choreography, verification and rollback). All seven services' S3 code paths are now merged or in review; the remaining #529 work is the runbook's ops phases + a phase-2 bind-removal PR after the soak. Note from the audit: file-movement reuses metadata-svc — widen that policy to its four new buckets or issue a dedicated movement-svc key.
Author
Owner

Epic #413 accuracy pass (2026-07-18) — this is now the SINGLE open ops ticket for the batch cutover. Consolidated state + remaining checklist.

Per-service tickets #493/#522/#523/#524/#525/#526/#528 are closed into here: every code path is merged on backend master, every one of the 10 stacks dual-runs (Storage__UseS3=true, verified in the infra stack files), and phase 0 ran 2026-07-14 (buckets created; ebooks 1.6 GiB ✓, blogs ✓; the empty buckets match empty disk trees). Docs corrected in infra PR #125.

Remaining checklist (in order)

Phase 0 stragglers — key policies (a 403 is as silent as the missing buckets were):

  • #613 — file-movement's metadata-svc key needs rw on quarantine/dlls/assets/blogs (or a dedicated movement-svc); Access Denied proven in Seq 2026-07-16
  • blogs-svc: confirm write on blogs (from #522)
  • uploads-svc: add read on ebooks (for BookImageLocator, #598 — from #528)
  • confirm lesson-video-processor actually deployed post-#581 (docker service ls) — first-ever deploy unblocked 2026-07-15

Phase 1 — soak:

  • one real upload of each kind lands in its bucket (blog media, lesson video when one exists, EICAR → quarantine after #613)
  • Seq watch for mirror warnings; note: best-effort mirrors swallow errors — the QA ask for a mirror-failure metric/alert stands

Phase 2 — the cutover PR:

  • final mc mirror re-run → remove all /mnt/fusionio binds from the 7 stacks → relax node.hostname == SERVERnode.labels.spikersoft-app == true → verify a task lands on 4090

Scope note — ebooks slice now rides #696

#696 (2026-07-18) supersedes the dual-run model for ebooks specifically: sole-store, write-primary with real error handling, internal MinIO route, timeouts (partially landed — backend #391). The ebooks bind-drop choreography should follow #696's phases rather than this runbook's generic phase 2; this ticket keeps the other six trees. Also note nginx no longer serves /spikersoft/ebooks at all (denied since #618 — book serving is authz'd through the API), which simplifies this batch's serving story to blogs + lesson-videos (#531).

**Epic #413 accuracy pass (2026-07-18) — this is now the SINGLE open ops ticket for the batch cutover. Consolidated state + remaining checklist.** Per-service tickets #493/#522/#523/#524/#525/#526/#528 are closed into here: every code path is merged on backend master, every one of the 10 stacks dual-runs (`Storage__UseS3=true`, verified in the infra stack files), and phase 0 ran 2026-07-14 (buckets created; ebooks 1.6 GiB ✓, blogs ✓; the empty buckets match empty disk trees). Docs corrected in infra PR #125. ## Remaining checklist (in order) **Phase 0 stragglers — key policies (a 403 is as silent as the missing buckets were):** - [ ] #613 — file-movement's `metadata-svc` key needs rw on quarantine/dlls/assets/blogs (or a dedicated `movement-svc`); Access Denied proven in Seq 2026-07-16 - [ ] `blogs-svc`: confirm **write** on `blogs` (from #522) - [ ] `uploads-svc`: add **read** on `ebooks` (for `BookImageLocator`, #598 — from #528) - [ ] confirm lesson-video-processor actually deployed post-#581 (`docker service ls`) — first-ever deploy unblocked 2026-07-15 **Phase 1 — soak:** - [ ] one real upload of each kind lands in its bucket (blog media, lesson video when one exists, EICAR → quarantine after #613) - [ ] Seq watch for mirror warnings; note: best-effort mirrors swallow errors — the QA ask for a mirror-failure metric/alert stands **Phase 2 — the cutover PR:** - [ ] final `mc mirror` re-run → remove all `/mnt/fusionio` binds from the 7 stacks → relax `node.hostname == SERVER` → `node.labels.spikersoft-app == true` → verify a task lands on 4090 ## Scope note — ebooks slice now rides #696 #696 (2026-07-18) supersedes the dual-run model for **ebooks specifically**: sole-store, write-primary with real error handling, internal MinIO route, timeouts (partially landed — backend #391). The ebooks bind-drop choreography should follow #696's phases rather than this runbook's generic phase 2; this ticket keeps the other six trees. Also note nginx no longer serves `/spikersoft/ebooks` at all (denied since #618 — book serving is authz'd through the API), which simplifies this batch's serving story to blogs + lesson-videos (#531).
Author
Owner

Checklist progress (2026-07-18): the entire "phase 0 stragglers — key policies" block now has a single executable fix: infra PR #126 + backend PR #415 (see #613 for the sequence). One script run then covers:

  • #613 movement-svc (file-movement quarantine/dlls/assets/blogs writes)
  • uploads-svc + ebooks-ro (the #598 BookImageLocator gate)
  • blogs-svc write confirmation — the script's new closing policy audit prints accessKey → attached policies for all svc users, which settles the #522 question with evidence instead of assumption

After the script run + backend #415 merge, remaining before phase 2: lesson-video-processor deploy check (docker service ls | grep lesson), the phase-1 soak verifications (blog upload → bucket; EICAR → quarantine), then the bind-drop PR. I'll prepare the phase-2 bind-drop PR (GATED) so it's ready to merge the moment the soak is clean.

**Checklist progress (2026-07-18):** the entire "phase 0 stragglers — key policies" block now has a single executable fix: **infra PR #126 + backend PR #415** (see #613 for the sequence). One script run then covers: - ✅→ #613 `movement-svc` (file-movement quarantine/dlls/assets/blogs writes) - ✅→ `uploads-svc` + `ebooks-ro` (the #598 BookImageLocator gate) - ✅→ `blogs-svc` write **confirmation** — the script's new closing policy audit prints accessKey → attached policies for all svc users, which settles the #522 question with evidence instead of assumption After the script run + backend #415 merge, remaining before phase 2: lesson-video-processor deploy check (`docker service ls | grep lesson`), the phase-1 soak verifications (blog upload → bucket; EICAR → quarantine), then the bind-drop PR. I'll prepare the phase-2 bind-drop PR (GATED) so it's ready to merge the moment the soak is clean.
Author
Owner

Phase-0 checklist update after the 2026-07-18 provision run + merges (infra #125/#126/#127, backend #415):

  • #613 movement-svc — created in MinIO with movement-svc-storage; one recovery step left: the Bao write 403'd (low-privilege token) → run ONLY_SVCS="movement" sudo -E bash rotate-minio-ci-keys.sh with an admin token, then a file-movement deploy + EICAR verify closes it (details on #613)
  • blogs-svc write CONFIRMED — audit shows blogs-svc-storage attached (rw blogs+quarantine). The #522 question is settled with evidence.
  • uploads-svc read-ebooks DONE — audit shows ebooks-ro,uploads-rw.
  • lesson-video-processor deploy check — still pending (docker service ls | grep lesson; note infra #123 fixed its 63-char service-name issue on 07-17, so post-#581+#123 it should finally exist)

Also now in play (merged #127): the minio internal overlay. ⚠️ Time-sensitive ops step: on a manager, docker network create -d overlay --attachable minio, then redeploy the minio stack (needs MINIO_ROOT_USER/PASSWORD exported) — before the next worker CI deploy, since all 9 worker stacks now reference the external network and will fail fast with network minio not found until it exists. Loud, not silent, but blocks deploys.

Then: phase-1 soak (blog upload → bucket; EICAR → quarantine) → phase-2 bind-drop PR (I'll have it prepared GATED).

**Phase-0 checklist update after the 2026-07-18 provision run + merges (infra #125/#126/#127, backend #415):** - [x] ~~#613 movement-svc~~ — created in MinIO with `movement-svc-storage`; **one recovery step left**: the Bao write 403'd (low-privilege token) → run `ONLY_SVCS="movement" sudo -E bash rotate-minio-ci-keys.sh` with an admin token, then a file-movement deploy + EICAR verify closes it (details on #613) - [x] `blogs-svc` write **CONFIRMED** — audit shows `blogs-svc-storage` attached (rw blogs+quarantine). The #522 question is settled with evidence. - [x] `uploads-svc` read-ebooks **DONE** — audit shows `ebooks-ro,uploads-rw`. - [ ] lesson-video-processor deploy check — still pending (`docker service ls | grep lesson`; note infra #123 fixed its 63-char service-name issue on 07-17, so post-#581+#123 it should finally exist) **Also now in play (merged #127):** the `minio` internal overlay. ⚠️ **Time-sensitive ops step:** on a manager, `docker network create -d overlay --attachable minio`, then redeploy the `minio` stack (needs `MINIO_ROOT_USER/PASSWORD` exported) — **before the next worker CI deploy**, since all 9 worker stacks now reference the external network and will fail fast with `network minio not found` until it exists. Loud, not silent, but blocks deploys. Then: phase-1 soak (blog upload → bucket; EICAR → quarantine) → phase-2 bind-drop PR (I'll have it prepared GATED).
Author
Owner

Audited against origin/masterphase 2 has not started on any of the 7 services. Per-service state, so the remaining work is concrete rather than "the cutover":

Storage__UseS3=true is set on all seven (backend :30, upload-coordinator :44, security-scanner :50, metadata-extractor :57, file-movement :26, blog-media-processor :25, lesson-video-processor :24), so dual-run is genuinely enabled. But every one still binds /mnt/fusionio and is still pinned to node.hostname == SERVER:

service fusionio binds placement
spikersoft-backend :67-75 (uploads, ebooks, blogs, lesson-videos, ai, photographs) :91 SERVER
upload-coordinator :90-96 (7 binds) :103 SERVER
security-scanner :103 uploads:ro :110 SERVER
metadata-extractor :97 uploads:ro, :103 ebooks:rw :110 SERVER
file-movement :52-58 (7 binds) :64 SERVER
blog-media-processor :51,52,55 :61 SERVER
lesson-video-processor :52,53 :59 SERVER

So the payoff this ticket exists for — letting these services schedule off SERVER — is not available yet on any of them.

Remaining:

  1. Final mc mirror re-run before the switch.
  2. Remove the fusionio binds from all 7 stacks.
  3. Relax placement from node.hostname == SERVER to node.labels.spikersoft-app == true.
  4. Verify a task actually lands on the 4090 node.

Plus the phase-0/1 items from this ticket that are still outstanding and unrecorded anywhere in repo: the movement-svc Bao rotate (#613), the lesson-video-processor deploy check, and the EICAR + blog-upload soak.

The ebooks bind is deferred to #696 by this ticket's own scope note, so I haven't double-counted it — though note #696 is also still at phase 0 there, so ebooks blocks metadata-extractor's bind removal in practice.

Audited against `origin/master` — **phase 2 has not started on any of the 7 services.** Per-service state, so the remaining work is concrete rather than "the cutover": `Storage__UseS3=true` **is** set on all seven (backend `:30`, upload-coordinator `:44`, security-scanner `:50`, metadata-extractor `:57`, file-movement `:26`, blog-media-processor `:25`, lesson-video-processor `:24`), so dual-run is genuinely enabled. But every one still binds `/mnt/fusionio` **and** is still pinned to `node.hostname == SERVER`: | service | fusionio binds | placement | |---|---|---| | spikersoft-backend | `:67-75` (uploads, ebooks, blogs, lesson-videos, ai, photographs) | `:91` SERVER | | upload-coordinator | `:90-96` (7 binds) | `:103` SERVER | | security-scanner | `:103` uploads:ro | `:110` SERVER | | metadata-extractor | `:97` uploads:ro, `:103` ebooks:rw | `:110` SERVER | | file-movement | `:52-58` (7 binds) | `:64` SERVER | | blog-media-processor | `:51,52,55` | `:61` SERVER | | lesson-video-processor | `:52,53` | `:59` SERVER | So the payoff this ticket exists for — letting these services schedule off SERVER — is not available yet on any of them. **Remaining:** 1. Final `mc mirror` re-run before the switch. 2. Remove the fusionio binds from all 7 stacks. 3. Relax placement from `node.hostname == SERVER` to `node.labels.spikersoft-app == true`. 4. Verify a task actually lands on the 4090 node. Plus the phase-0/1 items from this ticket that are still outstanding and unrecorded anywhere in repo: the movement-svc Bao rotate (#613), the lesson-video-processor deploy check, and the EICAR + blog-upload soak. The ebooks bind is deferred to #696 by this ticket's own scope note, so I haven't double-counted it — though note #696 is also still at phase 0 there, so ebooks blocks metadata-extractor's bind removal in practice.
Sign in to join this conversation.