R-ENV-02 promises dev and prod differ ONLY by scoped cluster-vars overrides. In fact
dev LAN addresses were hardcoded into base manifests, so clusters/prod would have come
up ANNOUNCING DEV ADDRESSES (192.168.1.x) on the prod LAN (192.168.2.x).
New cluster-vars knobs (base default = dev literal, per the file's existing convention):
K3S_API_HOST platform/cilium/helmrelease.yaml (+ the values.yaml mirror)
CILIUM_LB_POOL_START platform-config/cilium/l2-ip-pool.yaml
CILIUM_LB_POOL_STOP "
UNIFI_L4_VIP apps/vms/unifi/50-service-l4.yaml
CRAFTY_L4_VIP platform/crafty/service-l4.yaml
S3_BACKUP_ENDPOINT platform/keycloak/postgres-cluster.yaml, apps/wave-0/{nextcloud,
semaphore}-db.yaml, and platform/velero/helmrelease.yaml
Also fixes GATEWAY_VIP's base default, which was still the RETIRED libvirt address
10.73.0.240, and adds the GATEWAY_VIP patch that clusters/prod was missing entirely.
clusters/base/apps-vms.yaml gains postBuild.substituteFrom (it was the one consuming
Kustomization without it, so a ${TOKEN} there would have shipped as a literal string).
The envsubst contract is documented there: ${VAR} substitutes and escapes as $${VAR};
$VAR and $(cmd) are left alone; ${ARR[0]} hard-errors. The three UOS_URL expansions in
the unifi cloud-init are escaped accordingly.
PROD ADDRESS PLAN (operator, 2026-08-31):
nodes .10/.11/.12 · control-plane VIP .9 (Ansible-side) · GATEWAY_VIP .20
Cilium LB pool .20-.30
CONTAINMENT INVARIANT, now documented at the pool and in the prod overlay: GATEWAY_VIP
MUST equal CILIUM_LB_POOL_START. The shared Gateway pins no address, so Cilium LB-IPAM
gives it the first free address of a matching pool block. An initial prod proposal put
.20 outside a .21-.30 pool; the Gateway would have come up on .21 while split-horizon
coredns-custom kept answering *.home.keithjackson.dev with .20 — an address nothing
announces — blackholing in-cluster OIDC hairpin traffic while reporting healthy. The
pool was widened to .20-.30. The control-plane VIP (.9) stays outside the pool so
kube-vip and LB-IPAM can never contend.
Values prod still cannot know stay INVALID `TBD-PROD-*` placeholders on purpose: base
defaults are dev values, so an omitted patch means prod silently inherits a dev address
— far worse than a loud failed apply.
DEV IS BYTE-IDENTICAL. Verified by kustomize build over 13 paths before/after, then
flux envsubst with the dev cluster-vars values applied to both sides: zero drift. The
only deltas are the intended additive keys in the overlay ConfigMap itself. No
unresolved ${TOKEN} survives. render-golden green (10 goldens); yamllint clean.
Serves: R-ENV-02. Task: T-7.1 (prerequisite). Gate: MERGE GATE: PASS.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
79 lines
3.4 KiB
YAML
79 lines
3.4 KiB
YAML
---
|
|
# Keycloak's PostgreSQL — a CloudNativePG Cluster (CR of the CNPG CRDs installed by platform/cnpg).
|
|
# CARRIED BY platform-config (dependsOn platform) so the postgresql.cnpg.io CRDs exist first (F-MON1).
|
|
#
|
|
# DR-critical (R-IDP-05): storage on the 3-replica `longhorn` StorageClass (ADR-0003) — the data
|
|
# survives node loss. CNPG creates the keycloak-db-app Secret (keys username/password) that the
|
|
# Keycloak CR consumes, plus the keycloak-db-rw/-ro/-r Services. Keycloak is stateless and rebuilds
|
|
# from this DB + the git-managed realm import (docs/15).
|
|
apiVersion: postgresql.cnpg.io/v1
|
|
kind: Cluster
|
|
metadata:
|
|
name: keycloak-db
|
|
namespace: keycloak
|
|
spec:
|
|
# Single Postgres instance on 3-replica Longhorn is acceptable for the home scale (ADR-0014:
|
|
# "single replica with DB on Longhorn + backups is acceptable"). Raise to 3 for Postgres-level HA
|
|
# as a future hardening (prod) — the storage is already 3-way replicated by Longhorn.
|
|
instances: 1
|
|
imageName: ghcr.io/cloudnative-pg/postgresql:16.4
|
|
storage:
|
|
storageClass: longhorn
|
|
size: 5Gi
|
|
bootstrap:
|
|
initdb:
|
|
database: keycloak
|
|
owner: keycloak
|
|
# Emit Postgres metrics so Prometheus scrapes DB health (R-OBS-01). The CNPG operator creates the
|
|
# PodMonitor at runtime (CRD already present from Phase-4 monitoring) — no kustomize CRD-ordering hazard.
|
|
monitoring:
|
|
enablePodMonitor: true
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 256Mi
|
|
limits:
|
|
memory: 512Mi
|
|
# --- Off-cluster DR backup (R-IDP-05) — CNPG-native Barman → the on-Proxmox MinIO `cnpg` bucket ---
|
|
# Continuous WAL archiving + periodic base backups give proper Postgres PITR (superior to a Velero
|
|
# PVC snapshot for a live DB). destinationPath is the BUCKET ROOT; CNPG appends serverName (defaults
|
|
# to the Cluster name), so objects land under s3://cnpg/keycloak-db/. Path-style HTTP MinIO endpoint.
|
|
# Credentials: the scoped key from the in-namespace SOPS Secret keycloak-db-backup-credentials
|
|
# (a Pod only reads same-ns Secrets), kept in sync with platform/backup/minio-credentials.sops.yaml.
|
|
# endpointURL is ${S3_BACKUP_ENDPOINT} (cluster-vars, R-ENV-02; dev http://192.168.1.9:9000) — prod
|
|
# MUST point at its own off-cluster target. Substituted by the `platform-config` Kustomization.
|
|
backup:
|
|
retentionPolicy: "30d" # keep 30 days of base backups + WAL (PITR window); prunes older
|
|
barmanObjectStore:
|
|
destinationPath: s3://cnpg
|
|
endpointURL: ${S3_BACKUP_ENDPOINT}
|
|
s3Credentials:
|
|
accessKeyId:
|
|
name: keycloak-db-backup-credentials
|
|
key: ACCESS_KEY_ID
|
|
secretAccessKey:
|
|
name: keycloak-db-backup-credentials
|
|
key: ACCESS_SECRET_KEY
|
|
wal:
|
|
compression: gzip
|
|
maxParallel: 2
|
|
data:
|
|
compression: gzip
|
|
---
|
|
# Daily base backup of keycloak-db (WAL is archived continuously by the barmanObjectStore above).
|
|
# CNPG cron is 6-field (sec min hour dom mon dow). immediate:true takes a first base backup as soon
|
|
# as the schedule is created so the PITR chain has a starting point. Staggered 01:00 (nextcloud 02:00,
|
|
# semaphore 04:00, Velero 03:00) to avoid concurrent base backups hammering MinIO.
|
|
apiVersion: postgresql.cnpg.io/v1
|
|
kind: ScheduledBackup
|
|
metadata:
|
|
name: keycloak-db-daily
|
|
namespace: keycloak
|
|
spec:
|
|
immediate: true
|
|
schedule: "0 0 1 * * *"
|
|
backupOwnerReference: self
|
|
cluster:
|
|
name: keycloak-db
|
|
method: barmanObjectStore
|