Files
home-network-k3s-impl/platform/velero
jakman85andClaude Opus 5 161430d915 feat(backup): the overlay picks the credential — BACKUP_CREDENTIALS_SUFFIX
The previous commit created the two halves. This one wires the choice, so dev renders MinIO
credentials and prod renders R2 credentials from the SAME base manifests (R-ENV-02: the
overlay patch is the entire difference between environments; no base manifest is forked).

    cluster-vars  BACKUP_CREDENTIALS_SUFFIX   base ""  |  dev ""  |  prod "-prod"

    velero HelmRelease  credentials.existingSecret: velero-cloud-credentials${BACKUP_CREDENTIALS_SUFFIX}
    3x CNPG Cluster     s3Credentials.*.name:      <db>-backup-credentials${BACKUP_CREDENTIALS_SUFFIX}

ONE SUFFIX, NOT FOUR SECRET-NAME KNOBS. Four independent knobs can be updated three-quarters
of the way, leaving one consumer reading the OTHER environment's key with nothing rendering
wrong — and StrictPostBuildSubstitutions cannot catch that: strict mode fails on an UNDEFINED
variable, never on a defined-but-stale one. With one suffix the four move together or not at
all. The cost is that the rendered Secret name is not greppable from the consumer, so both
kustomizations spell the two names out in full beside the resources.

THE BASE DEFAULT IS "" AND THAT IS LOAD-BEARING. It renders the UNSUFFIXED names — objects
that exist in every render, and that dev is running today. The alternative failure mode is
strictly worse than the shared-Secret state this replaces: velero's pod hangs in
ContainerCreating forever when `existingSecret` names a missing Secret (the volume never
mounts), and a CNPG Cluster whose s3Credentials Secret is absent stops archiving WAL — which
under archive_mode=on is a filling pgdata PVC, not a missing backup. So an unresolvable
default breaks the referencing cluster HARDER than a wrong key does. Both overlays state the
value explicitly anyway, so neither inherits it silently.

Prod's overlay change is a cluster-vars op only. The velero Flux Kustomization patch is
untouched (it stays decryption-only, and velero stays suspended on prod).

VERIFIED — `kubectl kustomize --load-restrictor LoadRestrictionsNone` on clusters/dev and
clusters/prod for the cluster-vars values, then on each of the four affected tiers (platform,
platform-config, platform/velero, apps/wave-0) piped through `flux envsubst --strict` with
that environment's variables:

  strict envsubst              PASSES for all 8 (env x tier) combinations
  dev   velero existingSecret  velero-cloud-credentials
  dev   keycloak/nextcloud/semaphore-db  <db>-backup-credentials         (both key refs each)
  prod  velero existingSecret  velero-cloud-credentials-prod
  prod  keycloak/nextcloud/semaphore-db  <db>-backup-credentials-prod    (both key refs each)
  both  all EIGHT Secret objects present in the rendered output (dev and prod alike)
  no `${...}` survives in any rendered tier except gitea's three DELIBERATE `$${...}` shell
    escapes in apps/wave-0, which are pre-existing and unrelated.

Read alongside the endpoints the same overlays already set, the two environments are now
isolated on credential AND target:
  dev   velero s3://velero (root) + cnpg s3://cnpg      @ http://192.168.1.9:9000  region us-east-1
  prod  velero k3s-prod-backup/velero + barman prefix   @ https://<acct>.r2.cloudflarestorage.com  region auto

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 01:41:56 -05:00
..

platform/velero

Velero — off-cluster backup and DR of Kubernetes resource state + PVC data.

What this is (LIVE)

Flux HelmRelease (Velero chart 12.x / Velero 1.18.x) wired to the on-Proxmox MinIO S3 target:

  • Endpoint: http://192.168.1.9:9000 (plain HTTP, path-style, region us-east-1 placeholder)
  • BucketStorageLocation default: provider aws, bucket velero, s3ForcePathStyle=true.
  • AWS plugin (velero/velero-plugin-for-aws) loaded as an init container.
  • File System Backup (kopia): deployNodeAgent=true, uploaderType=kopia, snapshotsEnabled=false — storage-agnostic PVC backup, no CSI snapshot / data-mover.
  • Credentials: velero-cloud-credentials — a SOPS Secret (velero-cloud.sops.yaml) holding the cloud INI key, the scoped MinIO key limited to the velero+cnpg buckets. Kept in sync with the staging source of truth platform/backup/minio-credentials.sops.yaml and the Proxmox host ansible-vault.

Reconciled by its own non-gating Flux Kustomization clusters/base/backup.yaml (dependsOn: platform-config, wait:false) — a backup controller must never gate the platform graph.

Backup scope (what's included vs excluded)

Two daily Schedules (cadence ${VELERO_BACKUP_SCHEDULE}, retention ${VELERO_RETENTION}):

Schedule Namespaces Volumes (kopia FSB)
daily ALL except monitoring, unifi all PVC data (defaultVolumesToFsBackup:true, opt-out)
daily-resources-only monitoring, unifi none (defaultVolumesToFsBackup:false) — resources only

Excluded volume data (reconstructable, per the sizing decision):

  • monitoring — Prometheus TSDB PVC (ephemeral) + Loki 72h-log PVC. Grafana/Alertmanager are emptyDir (no PVC); Grafana dashboards are GitOps-provisioned.
  • unifi — the ~7.4 GiB KubeVirt VM disk (huge; UniFi is protected by its own config export).

Their Kubernetes resources are still captured (by daily-resources-only) to aid DR.

Included (the irreplaceable ~6 GiB): every other namespace's PVC data via kopia — Nextcloud data, Gitea, Plex/*arr config, PiHole, and the CNPG Postgres PVCs. (CNPG DBs additionally have native Barman PITR to the cnpg bucket — see the DB manifests.)

Post-deploy (PM)

  • velero backup-location get → default Available.
  • velero backup create --from-schedule daily → objects land under s3://velero/.
  • DR drill (R-STATE-03, Phase-4 gate): velero restore create --from-backup <name> into a scratch namespace and verify PVC data restores via the node-agent.