cluster

Infrastructure files for Nordgedanken and Midnightthoughts.
git clone git://archive.git.mtrnord.blog/MTRNord/cluster.git
Log | Files | Refs | README

commit cb522b5ff3926ab9d0d74902f8ee16105ea15303
parent c75978e27b55c00f9ad490847c430c808e2dd075
Author: MTRNord <MTRNord@users.noreply.github.com>
Date:   Sat, 28 Feb 2026 12:08:43 +0100

add a disaster recovery doc

Signed-off-by: MTRNord <MTRNord@users.noreply.github.com>

Diffstat:
ADISASTER_RECOVERY.md | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 367 insertions(+), 0 deletions(-)

diff --git a/DISASTER_RECOVERY.md b/DISASTER_RECOVERY.md @@ -0,0 +1,367 @@ +# Disaster Recovery Runbook — cluster-2025 + +Last updated: 2026-02-28 + +--- + +## Overview + +This cluster has three layers of backup: + +| Layer | Tool | Scope | Frequency | Retention | Storage | +|---|---|---|---|---|---| +| Application + Volumes | Velero (Kopia) | All namespaces + hcloud-volumes | 4× daily (0,6,12,18 UTC) | 14 days | `mtrnord-talos-velero` S3 bucket | +| Longhorn Volumes | Longhorn backup (incremental, full every 14d) | All Longhorn volumes (group: default) | Daily 02:00 UTC | 14 backups | `mtrnord-longhorn-backup` S3 bucket | +| Longhorn Config | Longhorn system-backup | Longhorn settings + metadata | Daily 05:00 UTC | 7 backups | `mtrnord-longhorn-backup` S3 bucket | +| PostgreSQL WAL | CNPG barman-cloud | postgres-cluster | Continuous + scheduled | 30 days | `mtrnord-talos-pg-backup` S3 bucket | + +> **Key constraint**: hcloud-volumes CSI does NOT support snapshots. Velero uses Kopia (file-system copy into S3) for those volumes, which requires applications to be quiesced or tolerant of slightly inconsistent snapshots. + +--- + +## Scenario 1: Single Application Recovery + +Use this when one app's data is corrupt or accidentally deleted. + +```bash +# List available backups +velero backup get + +# See what's in a backup +velero backup describe cluster-backup-0000-20260228000000 --details + +# Restore a single namespace +velero restore create --from-backup cluster-backup-0600-20260228060000 \ + --include-namespaces=<namespace> \ + --wait + +# Restore a single resource +velero restore create --from-backup cluster-backup-0600-20260228060000 \ + --include-namespaces=<namespace> \ + --include-resources=persistentvolumeclaims,persistentvolumes \ + --wait +``` + +To restore to a different namespace (e.g. testing a restore without overwriting live data): +```bash +velero restore create --from-backup cluster-backup-0600-20260228060000 \ + --include-namespaces=myapp \ + --namespace-mappings myapp:myapp-restored \ + --wait +``` + +--- + +## Scenario 2: PostgreSQL Point-in-Time Recovery + +CNPG continuously archives WALs to S3. This allows recovery to any point within the 30-day retention window. + +### 2a. Recover to latest (e.g. after accidental table drop) + +```bash +# Stop services that use postgres (reduce connections) +# Then edit cnpg-cluster.yaml to add a recovery bootstrap: + +bootstrap: + recovery: + source: pg-s3-backup + recoveryTarget: + targetTLI: latest # or use targetTime for PITR + +externalClusters: + - name: pg-s3-backup + plugin: + name: barman-cloud.cloudnative-pg.io + parameters: + barmanObjectName: hetzner-base-backup + serverName: pg-cluster-v2 # original WAL archive path +``` + +Also change `plugins.parameters.serverName` to something new (e.g. `pg-cluster-v2-restored`) to avoid "expected empty archive" error. + +### 2b. Point-in-Time Recovery (PITR) + +```yaml +bootstrap: + recovery: + source: pg-s3-backup + recoveryTarget: + targetTime: "2026-02-27T20:00:00Z" # RFC 3339, adjust as needed +``` + +### 2c. Important lessons from past migrations + +- **DO NOT use `bootstrap.recovery.backup.name`** with the barman-cloud plugin — causes "missing Azure credentials" error. Always use `externalClusters` + `source`. +- **Set a different `serverName` in `plugins`** from the recovery `serverName` — otherwise WAL archiver fails with "expected empty archive". +- **If timeline mismatch error**: add `targetTLI: latest` or use `targetTime` before the timeline switch. +- **Deleting the CNPG Cluster object ALSO deletes the PVCs** by default. Before deleting for migration, verify `deletionPolicy` or set it to `retain`. +- After recovery: remove the `bootstrap` and `externalClusters` sections from `cnpg-cluster.yaml` once the cluster is healthy. + +### 2d. Full postgres recovery procedure + +```bash +# 1. Scale down apps using postgres +flux suspend kustomization apps + +# 2. Delete the existing (broken) cluster +kubectl -n postgres-cluster delete cluster pg-cluster-v2 + +# 3. Edit gitops/infrastructure_talos/configs/cnpg-cluster.yaml: +# - Add bootstrap + externalClusters as above +# - Set storage.storageClass: hcloud-volumes (or longhorn if migrated) +# - Set plugins.parameters.serverName: pg-cluster-v2-restored + +# 4. Commit + push and let Flux apply +git add infrastructure_talos/configs/cnpg-cluster.yaml +git commit -m "recovery: restore pg-cluster-v2 from S3 backup" +git push + +# 5. Watch recovery +kubectl -n postgres-cluster get cluster pg-cluster-v2 -w +kubectl -n postgres-cluster logs -l cnpg.io/cluster=pg-cluster-v2 -f + +# 6. Once Ready: resume apps +flux resume kustomization apps + +# 7. Cleanup: remove bootstrap/externalClusters from cnpg-cluster.yaml and commit +``` + +--- + +## Scenario 3: Longhorn Volume Recovery + +### Longhorn Recurring Jobs + +Backup target: `s3://mtrnord-longhorn-backup` (Hetzner Object Storage HEL1) + +**Current configuration:** + +| Job name | Type | Schedule | Group | Retain | Concurrency | Notes | +|---|---|---|---|---|---|---| +| `volume-backup` | `backup` | `0 2 * * *` (02:00) | default | 14 | 2 | Volume data → S3. Primary recovery source. Incremental, full every 14 days. | +| `post-backup-cleanup` | `snapshot-delete` | `0 3 * * *` (03:00) | default | 2 | 2 | Enforces max 2 snapshots per volume after backup runs. Prevents copy/move failures. | +| `system-backup` | `system-backup` | `0 5 * * *` (05:00) | — | 7 | — | Longhorn config/metadata backup. `volume-backup-policy: if-not-present`. | +| `filesystem-trim` | `filesystem-trim` | `0 4 * * *` (04:00) | default | — | 2 | Reclaim space from deleted files. Runs between backup (02:00) and system-backup (05:00). | + +**Global Longhorn settings:** +- Max snapshots per volume: **5** (hard ceiling, monitoring before raising — `snapshot-delete` retain=2 enforces the soft limit, leaving 3 slots for system snapshots during replica rebuilds) +- Backup target: `s3://mtrnord-longhorn-backup` (Hetzner Object Storage HEL1) + +**Why no `snapshot` or `snapshot-cleanup` job:** +- No `snapshot` job: with a low global snapshot limit, an hourly retain=24 would immediately hit the ceiling. Velero (6h) + Longhorn backup (daily) provide sufficient recovery points without in-cluster snapshots. +- No `snapshot-cleanup`: redundant when `backup` job does pre-backup cleanup and `snapshot-delete` enforces the count hard limit. + +### 3a. Restore a single Longhorn volume from backup + +**Via Longhorn UI (easiest):** +1. Go to Longhorn UI → Backup +2. Find the volume backup +3. Click Restore → enter a name for the restored volume +4. Once restored, create a PVC pointing to the new volume or update the app's PVC + +**Via kubectl:** +```bash +# List available backups +kubectl -n longhorn-system get backups.longhorn.io + +# Restore by creating a Volume CR pointing to the backup +kubectl apply -f - <<EOF +apiVersion: longhorn.io/v1beta2 +kind: Volume +metadata: + name: restored-volume + namespace: longhorn-system +spec: + fromBackup: "s3://your-bucket?backup=backup-name&volume=volume-name" + numberOfReplicas: 2 + size: "10Gi" +EOF + +# Then create a PVC that binds to it (via Longhorn UI or static PV/PVC) +``` + +### 3b. Restore from Longhorn system-backup + +System backups capture the entire Longhorn state (volumes + settings). + +```bash +# List system backups +kubectl -n longhorn-system get systembackups.longhorn.io + +# Restore a system backup (this restores Longhorn settings + volumes) +# Do this in Longhorn UI: Settings → System Backup → Restore +# OR via CR: +kubectl apply -f - <<EOF +apiVersion: longhorn.io/v1beta2 +kind: SystemRestore +metadata: + name: restore-from-system-backup + namespace: longhorn-system +spec: + systemBackup: <system-backup-name> +EOF + +kubectl -n longhorn-system get systemrestores -w +``` + +> **Warning**: System restore overwrites current Longhorn settings. Only use for full Longhorn recovery, not single-volume restore. + +### 3c. Restore Longhorn volume via Velero CSI snapshot + +If Velero captured a CSI snapshot (via the `longhorn-velero-vsc` VolumeSnapshotClass): + +```bash +velero restore create --from-backup cluster-backup-0600-20260228060000 \ + --include-namespaces=<namespace> \ + --wait +``` + +Velero recreates the VolumeSnapshot and Longhorn restores the volume from it automatically. + +### 3d. Which method to use? + +| Situation | Best method | +|---|---| +| Single volume, recent data loss | Longhorn UI restore (3a) | +| Need data from >4 days ago | Velero restore (3c) — 14-day retention | +| Total Longhorn state loss | Longhorn system-backup restore (3b) | +| App namespace fully deleted | Velero namespace restore (Scenario 1) | + +--- + +## Scenario 4: Full Cluster Recovery (Total Loss) + +Use this when the entire cluster is gone and you need to rebuild from scratch. + +### Prerequisites +- Terraform state intact (in Hetzner Cloud or backed up) +- Access to S3 buckets: `mtrnord-talos-velero` and `mtrnord-talos-pg-backup` +- Age private key (stored separately — see below) +- All secrets in gitops repo are SOPS-encrypted — need age key to decrypt + +### Step 1: Rebuild infrastructure + +```bash +cd cluster2025-talos/cloud +terraform apply # recreates cloud VMs, network, firewall, kubeconfig + +# For Proxmox nodes: re-apply Talos machineconfig +cd cluster2025-talos/proxmox +terraform apply -var-file=proxmox.tfvars +``` + +### Step 2: Bootstrap Flux + +```bash +# Flux bootstrap will re-deploy all controllers and apps from gitops repo +flux bootstrap github \ + --owner=MTRNord \ + --repository=gitops \ + --branch=main \ + --path=clusters/talos_cluster +``` + +Or using your existing bootstrap method. + +### Step 3: Provide age key for SOPS decryption + +```bash +# The age private key must exist as a secret in flux-system +kubectl -n flux-system create secret generic sops-age \ + --from-file=age.agekey=/path/to/age.agekey +``` + +Without this, Flux cannot decrypt any SOPS-encrypted secrets (postgres credentials, velero credentials, etc.). + +### Step 4: Wait for core infrastructure + +```bash +# Wait for Velero, Longhorn, cert-manager, CNPG to be Ready +flux get kustomization +flux get helmrelease -A +kubectl -n velero get pods +kubectl -n longhorn-system get pods +``` + +### Step 5: Restore from Velero + +```bash +# Find the most recent backup +velero backup get + +# Full cluster restore (all namespaces) +velero restore create full-restore \ + --from-backup cluster-backup-0000-20260228000000 \ + --exclude-namespaces=velero,longhorn-system,kube-system,flux-system,cert-manager \ + --wait + +# Flux will already manage velero/longhorn/etc — exclude those to avoid conflicts +``` + +### Step 6: Restore PostgreSQL + +After Velero restores the `postgres-cluster` namespace, the CNPG operator will see the Cluster CR but the PVCs may not exist. If so, follow Scenario 2d above to recover from S3 WAL archives. + +If Velero successfully restored the PVCs (hcloud-volumes Kopia backup), CNPG should recover automatically once the operator is running. + +### Step 7: Verify + +```bash +# Check all pods running +kubectl get pods -A | grep -v Running | grep -v Completed + +# Check postgres +kubectl -n postgres-cluster get cluster pg-cluster-v2 + +# Check apps +flux get ks apps +``` + +--- + +## Key Backup Locations + +| What | Where | Path | +|---|---|---| +| Velero backups (all namespaces + volumes) | Hetzner Object Storage HEL1 | `mtrnord-talos-velero` bucket | +| Postgres WAL archives | Hetzner Object Storage HEL1 | `mtrnord-talos-pg-backup/pg-base-backup/pg-cluster-v2/` | +| Postgres scheduled base backups | same bucket | `mtrnord-talos-pg-backup/pg-base-backup/pg-cluster-v2/base/` | +| Longhorn volume backups | Hetzner Object Storage HEL1 | `mtrnord-longhorn-backup` bucket | +| Longhorn system backups | same bucket | `mtrnord-longhorn-backup` bucket | +| GitOps repo | GitHub | MTRNord/gitops | +| Terraform state | Hetzner Cloud S3 / local | cluster2025-talos/cloud/terraform.tfstate | +| Age private key | Local machine | `~/.config/sops/age/keys.txt` or `age.agekey` | + +> **CRITICAL**: The age private key is the master key for all cluster secrets. Store it in a password manager (Bitwarden, etc.) in addition to the local file. Without it you cannot decrypt any secret in the cluster. + +--- + +## Recovery Decision Tree + +``` +Something is broken + │ + ├── Single app data loss / corruption + │ └── Velero restore of that namespace (Scenario 1) + │ + ├── PostgreSQL data loss / corruption + │ ├── Minor (table drop, bad migration) + │ │ └── CNPG PITR to before the event (Scenario 2) + │ └── Major (cluster deleted, storage gone) + │ └── Full CNPG recovery from S3 (Scenario 2d) + │ + ├── Longhorn volume corrupted + │ └── Restore from Longhorn backup or Velero CSI snapshot (Scenario 3) + │ + └── Total cluster loss + └── Rebuild Terraform → Bootstrap Flux → Velero restore → CNPG restore (Scenario 4) +``` + +--- + +## Known Gotchas + +- **hcloud-volumes Kopia backup consistency**: Kopia copies files from live pods. For databases other than postgres (which uses CNPG's own WAL-based backup), backups may be inconsistent if the app is writing during backup. Consider pre-backup hooks or accepting slight inconsistency. +- **Velero Schedule CRDs**: Velero CRDs (incl. `Schedule`) are installed by the HelmRelease (infra-controllers). Schedules themselves live in infra-configs which runs after controllers — this is why they're in `infrastructure_talos/configs/velero-schedules.yaml` not in the velero controller directory. +- **node-agent PodSecurity**: Kopia's `node-agent` DaemonSet requires `hostPath` volumes. The `velero` namespace must have `pod-security.kubernetes.io/enforce: privileged`. +- **Longhorn backup disk space**: Longhorn snapshots are space-expensive during generation. Prefer Velero (Kopia) for volume backups where possible.