railiance-apps/manifests/cnpg-option-a-backup.yaml
tegwick 4a06c0884a
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
RAILIANCE-WP-0016: finish unattended hybrid Option A backups
Core in-cluster CronJobs with cnpg-option-a-backup image + NetworkPolicy;
R01 activity-core schedule enabled; status healthy; evidence recorded.
2026-07-22 20:34:24 +02:00

423 lines
13 KiB
YAML

# Option A — age-encrypted logical dumps → Nextcloud WebDAV (RAILIANCE-WP-0015).
#
# Decided approach (not barman ScheduledBackup). Applies to CoulombCore
# databases namespace clusters: apps-pg, gitea-db, net-kingdom-pg, state-hub-db.
#
# Unattended path (primary):
# - CoulombCore: in-cluster CronJobs using image cnpg-option-a-backup:v1
# (kubectl+age+curl; import via k3s ctr; imagePullPolicy Never)
# - railiance01: activity-core ActivityDefinition daily-cnpg-option-a-backup
#
# Prereqs:
# 1. OpenBao lane CCR-2026-0004 resolvable
# 2. make cnpg-backup-offsite-secret-apply # Secret cnpg-backup-offsite
# 3. kubectl apply -f manifests/cnpg-option-a-backup.yaml
# 4. Install workstation cron (see schedule ConfigMap / docs)
#
# Schedule: daily 02:30 UTC (RPO 24h). Retention target 14 daily + 4 weekly
# is enforced on the Nextcloud side / operator prune.
#
# Health: make cnpg-backup-status
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cnpg-option-a-schedule
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
data:
mode: in-cluster-cron
next_mode: activity-core+in-cluster
schedule: "30 2 * * *"
command: "cd $HOME/railiance-apps && make cnpg-logical-backup"
activity_core_definition: daily-cnpg-option-a-backup
platform_cli: /opt/railiance-platform/tools/cmd/cnpg-option-a-backup
clusters: "apps-pg,gitea-db,net-kingdom-pg,state-hub-db"
rpo_hours: "24"
rto_hours: "4"
retention: "14 daily + 4 weekly (Nextcloud operator prune)"
note: "WP-0016: activity-core path primary when enabled; workstation-cron is stopgap. In-cluster CronJobs remain suspended."
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: cnpg-logical-backup
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cnpg-logical-backup
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "create", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["cnpg-backup-offsite"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cnpg-logical-backup
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cnpg-logical-backup
subjects:
- kind: ServiceAccount
name: cnpg-logical-backup
namespace: databases
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cnpg-option-a-runner
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
data:
run-backup.sh: |
#!/bin/sh
# In-cluster Option A runner. Env: CLUSTER, DATABASES (comma-separated).
set -eu
CLUSTER="${CLUSTER:?CLUSTER required}"
DATABASES="${DATABASES:?DATABASES required}"
NAMESPACE="${NAMESPACE:-databases}"
STATUS_CM="${STATUS_CM:-cnpg-option-a-status}"
WORK=/tmp/backup-work
mkdir -p "$WORK"
# Image cnpg-option-a-backup:v1 ships /usr/local/bin/age (no egress installs).
AGE_BIN="$(command -v age || true)"
if [ -z "$AGE_BIN" ]; then
echo "ERROR: age binary missing from image PATH" >&2
exit 1
fi
NC_TOKEN="$(cat /secrets/offsite/NC_WEBDAV_TOKEN)"
NC_URL="$(cat /secrets/offsite/NC_WEBDAV_URL)"
AGE_PUB="$(cat /secrets/offsite/AGE_PUBLIC_KEY)"
# filesdrop shares often need token-only URL form
case "$NC_URL" in
*/) ;;
*) NC_URL="${NC_URL}/" ;;
esac
POD="$(kubectl get pods -n "$NAMESPACE" \
-l "cnpg.io/cluster=${CLUSTER},role=primary" \
-o jsonpath='{.items[0].metadata.name}')"
if [ -z "$POD" ]; then
echo "ERROR: primary pod not found for ${CLUSTER}" >&2
exit 1
fi
echo "cluster=${CLUSTER} pod=${POD}"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
OLD_IFS=$IFS
IFS=,
for db in $DATABASES; do
IFS=$OLD_IFS
plain="${WORK}/${CLUSTER}-${db}-${TS}.dump"
aged="${plain}.age"
echo "dump ${db}"
kubectl exec -n "$NAMESPACE" "$POD" -c postgres -- \
pg_dump -U postgres -d "$db" -Fc --no-owner --no-acl >"$plain"
"$AGE_BIN" -r "$AGE_PUB" -o "$aged" "$plain"
rm -f "$plain"
remote="${CLUSTER}/${CLUSTER}-${db}-${TS}.dump.age"
dest="${NC_URL}${remote}"
echo "upload ${remote}"
curl -sf -u "${NC_TOKEN}:" -T "$aged" "$dest"
# weekly on Sunday UTC
if [ "$(date -u +%u)" = "7" ]; then
curl -sf -u "${NC_TOKEN}:" -T "$aged" \
"${NC_URL}${CLUSTER}/${CLUSTER}-${db}-weekly-${TS}.dump.age" || true
fi
rm -f "$aged"
done
IFS=$OLD_IFS
# Record last success for cnpg-backup-status
if kubectl get configmap "$STATUS_CM" -n "$NAMESPACE" >/dev/null 2>&1; then
kubectl patch configmap "$STATUS_CM" -n "$NAMESPACE" --type merge \
-p "{\"data\":{\"${CLUSTER}\":\"${TS}\",\"updated\":\"${TS}\",\"lane\":\"option-a\"}}"
else
kubectl create configmap "$STATUS_CM" -n "$NAMESPACE" \
--from-literal="${CLUSTER}=${TS}" \
--from-literal=updated="${TS}" \
--from-literal=lane=option-a
fi
kubectl label configmap "$STATUS_CM" -n "$NAMESPACE" \
railiance.apps/backup-lane=option-a --overwrite >/dev/null
echo "ok: ${CLUSTER} backup ${TS}"
---
# Shared pod template fields are repeated per CronJob for readability (no Kustomize).
apiVersion: batch/v1
kind: CronJob
metadata:
name: cnpg-logical-backup-apps-pg
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: apps-pg
spec:
schedule: "30 2 * * *"
suspend: false
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 7200
template:
metadata:
labels:
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: apps-pg
spec:
serviceAccountName: cnpg-logical-backup
restartPolicy: Never
containers:
- name: backup
image: cnpg-option-a-backup:v1
imagePullPolicy: Never
env:
- name: CLUSTER
value: apps-pg
- name: DATABASES
value: apps_meta,core_hub,core_hub_staging,vergabe_db
- name: NAMESPACE
value: databases
command: ["/bin/sh", "/runner/run-backup.sh"]
volumeMounts:
- name: runner
mountPath: /runner
readOnly: true
- name: offsite
mountPath: /secrets/offsite
readOnly: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumes:
- name: runner
configMap:
name: cnpg-option-a-runner
defaultMode: 0755
- name: offsite
secret:
secretName: cnpg-backup-offsite
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: cnpg-logical-backup-gitea-db
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: gitea-db
spec:
schedule: "35 2 * * *"
suspend: false
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 7200
template:
metadata:
labels:
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: gitea-db
spec:
serviceAccountName: cnpg-logical-backup
restartPolicy: Never
containers:
- name: backup
image: cnpg-option-a-backup:v1
imagePullPolicy: Never
env:
- name: CLUSTER
value: gitea-db
- name: DATABASES
value: gitea
- name: NAMESPACE
value: databases
command: ["/bin/sh", "/runner/run-backup.sh"]
volumeMounts:
- name: runner
mountPath: /runner
readOnly: true
- name: offsite
mountPath: /secrets/offsite
readOnly: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumes:
- name: runner
configMap:
name: cnpg-option-a-runner
defaultMode: 0755
- name: offsite
secret:
secretName: cnpg-backup-offsite
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: cnpg-logical-backup-net-kingdom-pg
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: net-kingdom-pg
spec:
schedule: "40 2 * * *"
suspend: false
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 7200
template:
metadata:
labels:
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: net-kingdom-pg
spec:
serviceAccountName: cnpg-logical-backup
restartPolicy: Never
containers:
- name: backup
image: cnpg-option-a-backup:v1
imagePullPolicy: Never
env:
- name: CLUSTER
value: net-kingdom-pg
- name: DATABASES
value: interhub,privacyidea_db
- name: NAMESPACE
value: databases
command: ["/bin/sh", "/runner/run-backup.sh"]
volumeMounts:
- name: runner
mountPath: /runner
readOnly: true
- name: offsite
mountPath: /secrets/offsite
readOnly: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumes:
- name: runner
configMap:
name: cnpg-option-a-runner
defaultMode: 0755
- name: offsite
secret:
secretName: cnpg-backup-offsite
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: cnpg-logical-backup-state-hub-db
namespace: databases
labels:
app.kubernetes.io/part-of: railiance-apps
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: state-hub-db
spec:
schedule: "45 2 * * *"
suspend: false
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 7200
template:
metadata:
labels:
railiance.apps/backup-lane: option-a
railiance.apps/backup-cluster: state-hub-db
spec:
serviceAccountName: cnpg-logical-backup
restartPolicy: Never
containers:
- name: backup
image: cnpg-option-a-backup:v1
imagePullPolicy: Never
env:
- name: CLUSTER
value: state-hub-db
- name: DATABASES
value: state_hub
- name: NAMESPACE
value: databases
command: ["/bin/sh", "/runner/run-backup.sh"]
volumeMounts:
- name: runner
mountPath: /runner
readOnly: true
- name: offsite
mountPath: /secrets/offsite
readOnly: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumes:
- name: runner
configMap:
name: cnpg-option-a-runner
defaultMode: 0755
- name: offsite
secret:
secretName: cnpg-backup-offsite