Review: P2 off-cluster postgres backup destination (DR, not just resilience)
Verdict: NEEDS_REFINEMENT
TL;DR: Premise is correct (in-cluster MinIO = local resilience, not DR). But the ticket has a wrong file target, a competing active plan (
plan-pal-e-backup Phase 2 scopes this exact problem with a different approach), missing backing notes, and an AC that will block on a "scratch environment" we don't have. Major scope conflict needs a human call before this can advance to todo.Template Completeness
- [x] Type — Feature
- [x] Lineage — discovered during #297 scoping, documented
- [x] Repo —
forgejo_admin/pal-e-platform - [x] User Story — present, names downstream stakeholders (Lucas, Marcus, agents)
- [x] Context — thorough, enumerates destinations and trade-offs
- [x] File Targets — present (but one is wrong; see File Targets section)
- [x] Test Expectations — present
- [x] Constraints — present (cost target, no-data-loss, scratch-env test)
- [x] Acceptance Criteria — 8 items, mostly verifiable (two problematic; see AC section)
- [x] Checklist — deferred to AC, acceptable
- [x] Out of Scope — present, sensible carve-outs
- [x] Environment — present
- [x] Related — present
Traceability
- [x]
story:superuser-recoverlabel — verified onproject-pal-e-platformuser-stories table: "I can recover from failures using documented SOPs. Every failure mode has a runbook." - [x]
arch:cnpglabel — applied to board item - [ ] arch note MISSING —
arch-cnpgdoes not exist in pal-e-docs.[SCOPE]Create architecture notearch-cnpgdocumenting the CNPG operator, both clusters (pal-e-postgres,woodpecker-db), bucket (s3://postgres-wal/), and credentials (cnpg-s3-creds). Needed for future work on this component. - [x] Forgejo issue — #299 open, body populated via template.
- [ ] SOP note MISSING — ticket AC references
sop-postgres-restore, but no such note exists in pal-e-docs.[SCOPE]Createsop-postgres-restoreas part of sibling ticket #298 (restore drill) BEFORE this ticket executes, so the AC "sop-postgres-restore updated with off-cluster steps" has something to update.
File Targets
- [x]
pal-e-services/terraform/cnpg.tf— verified. Lines 123-145 contain thebarmanObjectStoreblock withdestinationPath = "s3://postgres-wal/"andendpointURL = "http://minio.minio.svc.cluster.local:9000". Matches ticket claim exactly. - [x]
pal-e-services/terraform/k3s.tfvars(gitignored) — exists, confirmed not tracked. - [x]
pal-e-services/terraform/k3s.tfvars.example— exists (103 lines). - [ ] ISSUE — woodpecker-db destination is NOT modifiable from
pal-e-services/terraform/cnpg.tf. Ticket says "every other CNPG cluster on the platform (today:woodpecker-db) should likely get the same treatment." But thewoodpecker-dbbarmanObjectStoreblock lives inpal-e-platform/terraform/modules/ci/main.tflines 86-88 (destinationPaths3://postgres-wal/woodpecker/). Touching both requires changes in TWO repos. Either: (a) narrow this ticket to pal-e-postgres only and file a sibling for woodpecker-db, or (b) broaden File Targets to includepal-e-platform/terraform/modules/ci/main.tfand mark both repos in Repo field.[BODY]Fix File Targets to reflect reality; pick (a) or (b). - [ ] MISSING file target —
cnpg-s3-credssecret + verify CronJob. Credentials are provisioned inpal-e-platform/terraform/modules/database/main.tflines 47-56 (notpal-e-servicesas the ticket implies). The ticket says "Newcnpg-s3-creds-offclusterk8s secret (or extend existing)" — the existing secret lives in pal-e-platform, so the off-cluster secret likely belongs there too. Also, lines 84-200 of the same file containcnpg_backup_verifyCronJob that currently checkspal-e-postgresandwoodpeckerprefixes against MinIO — it will need updating when destinations change.[BODY]Addpal-e-platform/terraform/modules/database/main.tfto File Targets with both the secret and CronJob call-outs. - [x]
sop-postgres-restoreandconvention-postgres-backup-destination— new pal-e-docs notes, not code files. Acceptable as deliverables.
Repo Placement
Ticket is correctly filed on
pal-e-platform (platform-level concern: provisions buckets, credentials, verification, operator). But actual file changes cross both pal-e-platform and pal-e-services repos. Either need a sibling ticket on pal-e-services for the cnpg.tf change, or explicitly call out multi-repo execution in the body with coordination notes (which repo lands first to avoid a gap — see Blast Radius).Dependencies
- #297 (P0 tf-state drift) — ticket says "apply through PR #297-style review-fix-Lucas-approve loop (and by then #297 is DONE so the apply is the canonical
sop-platform-tf-changesflow)." This phrasing is aspirational, not a hard dependency. Parallel work is safe if this ticket's code changes are restricted to pal-e-services/cnpg.tf (which is the one file #297 is actively stabilizing). If #297 is still reconciling drift in the same file, a merge collision is likely.[BODY]Explicitly mark "blocked by #297" OR state "can run in parallel; rebase required if #297 movescnpg.tf". - #298 (restore drill) — ticket says sibling. Verified open. AC here depends on the
validation-postgres-restoredrill existing and passing. Hard dependency: #298 must producesop-postgres-restoreand a working drill before #299 can satisfy its AC "validation-postgres-restore re-run against off-cluster backup PASS."[BODY]Mark "blocked by #298 untilsop-postgres-restorepublished." - plan-pal-e-backup (existing active plan) — MAJOR OVERLAP.
plan-pal-e-backupalready exists withproject-pal-e-backupand 7 phases. Phase 2 "Database Backups" is explicitly scoped as dailypg_dumpto Backblaze B2 (decisions section: "Backblaze B2 recommended $6/TB", "Daily pg_dump over CNPG migration", "Unified bucket structure one bucket directory-per-service"). This ticket's approach (CNPG-nativebarmanObjectStorepointed at off-cluster S3) is a DIFFERENT technical approach. These two cannot both execute without a scope-overlap conflict. This needs a human decision before #299 advances: either kill plan-pal-e-backup Phase 2 (or kill #299), OR harmonize them as two complementary layers (pg_dump daily + WAL continuous streaming).[SCOPE]Ava + Lucas must reconcileplan-pal-e-backupPhase 2 vs. #299 before this moves totodo. - No ticket blocked by #299 found — work can be sequenced freely within the dependency graph above.
Acceptance Criteria
AC list is mostly testable. Two problem items:
- AC: "Cluster-loss simulation in a SCRATCH environment." Realistic concern: platform runs on a single archbox node. There is no "scratch environment" today. A realistic scratch target is either (a) a Hetzner VPS spun up for the drill (plan-pal-e-backup Phase 7 also assumes this), (b) a local VM or kind cluster, or (c) a docker-compose CNPG operator standalone. Without naming one, this AC is untestable.
[BODY]Name the scratch environment (even provisionally — "Hetzner VPS per plan-pal-e-backup Phase 7"). Without this, a dev agent cannot satisfy the AC. - AC: "Apply through PR #297-style review-fix-Lucas-approve loop (and by then #297 is DONE)." The parenthetical embeds a sequencing assumption. Either make it a hard dependency ("blocked by #297") or remove the phrase and just reference
sop-platform-tf-changes.[BODY]Tighten. - AC: "Follow-up ticket filed: same treatment for woodpecker-db." Good hygiene. Keep.
Missing AC considerations that Ava flagged and Dottie concurs:
- Encryption at rest: ticket body says "Out of Scope — Backup encryption at rest (separate ticket if not already provider-default)." Given postgres holds PII (journal entries, user identifiers,
feedback_funnel_requires_auth.mdcites 4-hour PII leak), encryption-at-rest is NOT an acceptable "out of scope." Needs an explicit AC: "Chosen provider encrypts at rest by default (documented in convention-postgres-backup-destination) OR WAL/base-backup files are encrypted client-side (e.g., via--encryptionflag or age pre-upload)."[BODY]Add encryption AC. - Data residency: Providers enumerated (B2, AWS S3, R2, DO Spaces) span multiple jurisdictions. No residency requirement is stated. If Lucas wants US-only, say so; if "don't care," say that too.
[BODY]Add a one-line data-residency constraint in Constraints (can be "US-only acceptable, EU acceptable, not PRC/RU" or similar). - Cutover safety: AC "No data loss during the cutover" is stated but the mechanism is hand-wavy ("Existing in-cluster backups must keep working until the off-cluster destination is verified"). Concrete mechanism matters: does this mean dual-destination for N days (which CNPG doesn't trivially support), or swap-with-rollback, or something else? Classic footgun: if the new destination is applied and WAL archive to old destination stops immediately, PITR gap equals time-from-apply to first-successful-new-backup.
[BODY]Spell out cutover: recommend "parallel base backup complete before old destination decommissioned; WAL archive gap tolerance documented."
Blast Radius
- Same bug elsewhere? Yes — every CNPG cluster on the platform has the same "backup-to-same-cluster-MinIO" problem.
woodpecker-dbconfirmed (ci/main.tf). Future CNPG clusters will inherit it unless a convention (convention-postgres-backup-destination) lands to force off-cluster from the start. The convention-note AC is GOOD; it prevents regression. - cnpg_backup_verify CronJob impact:
pal-e-platform/terraform/modules/database/main.tf:84runs daily checking MinIO backup freshness. If destinations move but this CronJob doesn't, alerting goes stale. Must be updated in lockstep. Flagged above in File Targets. - Non-CNPG postgres (plain pods): basketball-api and mcd-tracker run plain Postgres pods, not CNPG. They have no backup today. Out of scope here (plan-pal-e-backup Phase 2 addresses them via pg_dump CronJobs). Note for Ava: the conflict with plan-pal-e-backup is that plan would get ALL databases (including plain pods) to off-site with one approach; #299 only addresses CNPG clusters.
- Cost ceiling realism: $5/month target — paledocs + twitch2kwager DB sizes small (well under 10GB). At Backblaze B2 $6/TB/month that's negligible (fractions of a cent). R2 has no egress but S3-API egress costs are zero on reads anyway. AWS S3 Standard-IA at $0.0125/GB would be ~$0.13/mo for 10GB. All named candidates fit the budget easily. Budget is not a meaningful constraint — decision driver should be operational simplicity and encryption defaults, not cost.
[BODY]Tighten Constraints: replace "under $5/month" with "cost not a driver at current volumes; optimize for operational simplicity + encryption-at-rest default." - Provider-selection sub-scope: Ava asked whether provider selection should be a sibling spike. Dottie view: no. Evaluating 4 providers for a narrow CNPG S3-API target is maybe 2 hours of doc-reading; does not need its own spike. But the first AC ("Provider chosen and documented in convention-postgres-backup-destination with rationale") implicitly IS the decision gate, and it needs Lucas sign-off before execution. Recommend marking the first AC as "review-fix-Lucas-approve gate before other AC start."
Decomposition Assessment
File count: 3 confirmed + 2 notes = 5. AC count: 8. Estimated time if plan-pal-e-backup conflict is resolved: 3-6 hours (provider selection + tofu plan + credential provisioning + apply + 24h wait for scheduled backup + drill re-run + SOP updates + convention note). This is at the upper edge of the 5-minute-agent rule. If the conflict with plan-pal-e-backup is resolved by killing the pg_dump approach, this ticket stays whole. If both approaches coexist, this ticket should decompose into (1) off-cluster pal-e-postgres WAL destination, (2) off-cluster woodpecker-db WAL destination, (3) convention + SOP publication — three sub-tickets. Dottie recommends NOT decomposing yet; resolve the plan-pal-e-backup conflict first, then re-evaluate.
Recommendation
[SCOPE]BLOCKER: Reconcile withplan-pal-e-backupPhase 2. Two active approaches to "off-site postgres backup" cannot both execute. Ava + Lucas decide: (a) kill plan-pal-e-backup Phase 2 and adopt the CNPG-native approach in #299, (b) kill #299 and use the pg_dump approach, or (c) both layers coexist (continuous WAL to off-cluster via CNPG + daily pg_dump to same bucket for extra safety).[SCOPE]Createarch-cnpgnote in pal-e-docs documenting the CNPG operator, both clusters, bucket, credentials.[SCOPE]Create or coordinatesop-postgres-restoreas part of #298 (sibling) BEFORE #299 executes, so #299 has something to update.[BODY]Fix File Targets: woodpecker-db backup config lives inpal-e-platform/terraform/modules/ci/main.tf, notpal-e-services/terraform/cnpg.tf. Either narrow ticket to pal-e-postgres only (sibling for woodpecker) or broaden File Targets + Repo to cover both repos.[BODY]Addpal-e-platform/terraform/modules/database/main.tfto File Targets (thecnpg-s3-credssecret +cnpg_backup_verifyCronJob both live here and need updates).[BODY]Name the scratch environment for cluster-loss simulation (e.g. "Hetzner VPS, provisioned per plan-pal-e-backup Phase 7"). Without this, the AC is untestable.[BODY]Mark dependency on #298 as hard (AC "validation-postgres-restore re-run PASS" requires #298 to have delivered the drill).[BODY]State #297 relationship explicitly: either "blocked by #297" or "parallel OK, rebase if cnpg.tf moves."[BODY]Add encryption-at-rest AC (PII data; "out of scope" is wrong here).[BODY]Add data-residency constraint (one line in Constraints).[BODY]Spell out cutover mechanism (parallel dual-destination OR swap-with-PITR-gap-bounded). Classic footgun: WAL archive gap between old destination stopping and new destination starting.[BODY]Replace "$5/month cost target" with operational-simplicity + encryption-defaults driver.[BODY]Tighten AC about #297 — drop the parenthetical aspiration, just referencesop-platform-tf-changes.
After these refinements (and the plan-pal-e-backup reconciliation in particular), ticket is solid. Dottie expects this returns to READY after one iteration with Ava.