stack.basicstack.de/apps/monitoring
CTO (Paperclip Agent) 7368283d76 retire(monitoring): remove backup-k8s-resources CronJob + restic alerts (DEV-576)
Velero cluster-all now covers cluster-manifest backup with two successive
Completed runs verified and one restore drill (openwebui) green (DEV-575).

- Delete apps/monitoring/backup-k8s-resources-cronjob.yaml
- Drop BackupK8sResourcesStale + BackupK8sResourcesCheckFailed from
  backup-restic-alerts.yaml and matching series/cases from the promtool
  test file (all 9 remaining rules pass `promtool test rules`)
- Update apps/monitoring/README.md + prometheus-backup-cronjob comments
- Runbook lives in BookStack: EN
  https://books.basicstack.de/books/it-operations-manual/page/velero-backup-restore-runbook
  DE
  https://books.basicstack.de/books/it-betriebshandbuch/page/velero-backup-und-restore-runbook

Rollback: revert this commit — Argo restores the CronJob + alerts and
restic k8s-resources runs resume at the next 02:00 UTC schedule.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 18:24:07 +00:00
..
backup-grafana-restic-cronjob.yaml chore(restic): bump image 0.17.3 -> 0.19.1 (DEV-541) 2026-08-23 15:52:15 +00:00
backup-loki-restic-cronjob.yaml chore(restic): bump image 0.17.3 -> 0.19.1 (DEV-541) 2026-08-23 15:52:15 +00:00
backup-restic-alerts.test.yaml retire(monitoring): remove backup-k8s-resources CronJob + restic alerts (DEV-576) 2026-09-12 18:24:07 +00:00
backup-restic-alerts.yaml retire(monitoring): remove backup-k8s-resources CronJob + restic alerts (DEV-576) 2026-09-12 18:24:07 +00:00
loki-deployment.yaml feat(monitoring): retire legacy backup-volumes CronJob + backup-storage PVC (DEV-489) 2026-08-16 15:58:43 +00:00
prometheus-backup-cronjob.yaml retire(monitoring): remove backup-k8s-resources CronJob + restic alerts (DEV-576) 2026-09-12 18:24:07 +00:00
prometheus-backup-sealed.yaml feat(monitoring): seal restic-password into monitoring-s3-backup (DEV-484) 2026-08-16 13:22:10 +00:00
README.md retire(monitoring): remove backup-k8s-resources CronJob + restic alerts (DEV-576) 2026-09-12 18:24:07 +00:00

monitoring — backup CronJobs

Manifests recording the cluster-side monitoring backup CronJobs that were previously applied out-of-band. These files are the authoritative source (kubectl apply -f apps/monitoring/). See DEV-464 for the repair context.

  • backup-loki-restic-cronjob.yaml — daily restic backup of loki-storage-encrypted to hetzner-s3:${BUCKET}/restic/loki. Co-schedules with the Loki pod via podAffinity (RWO permits additional read-only mounts on the same node). Deployed per the DEV-482 Option 4 rollout (DEV-485).
  • backup-grafana-restic-cronjob.yaml — daily restic backup of grafana-storage to hetzner-s3:${BUCKET}/restic/grafana. Pinned to k3s-worker-2 via nodeSelector (the local-path PV anchors the grafana pod there already, no podAffinity needed). Schedule 15 3 * * * — offset from the loki run at 03:00. Deployed per the DEV-482 Option 4 rollout (DEV-486).
  • prometheus-backup-cronjob.yaml + prometheus-backup-sealed.yaml — daily restic backup of prometheus-data-encrypted to hetzner-s3:${BUCKET}/restic/prometheus. Co-schedules with the Prometheus pod via podAffinity so the RWO PVC attaches on the same node. Schedule 30 3 * * * — offset from the loki (03:00) and grafana (03:15) runs. Migrated from the DEV-465 rclone sync job to restic client-side encryption per DEV-492 / DEV-482 Option 4. Compaction-race mitigation: --exclude wal/* + --exclude chunks_head/* + accept restic backup exit code 3 (source file vanished mid-walk) as a warning, not a failure; restore drill re-runs promtool tsdb analyze per block.
  • backup-restic-alerts.yaml + backup-restic-alerts.test.yaml — PrometheusRule with freshness, integrity, and repo-size alerts covering the three restic repos (loki/grafana/prometheus), plus a promtool test rules unit test proving each alert fires against synthetic samples (DEV-490, extended in DEV-492). The k8s-resources restic repo was superseded by Velero cluster-reconstruction backups and retired in DEV-576.

Shared SealedSecret

All four restic CronJobs read Hetzner S3 credentials + the restic repository password from SealedSecret monitoring-s3-backup (namespace monitoring). Keys:

Key Purpose
access-key Hetzner Object Storage access key ID
secret-key Hetzner Object Storage secret access key
endpoint S3 endpoint hostname (e.g. fsn1.your-objectstorage.com)
bucket Bucket name (single bucket, per-prefix repos)
restic-password 32-byte random string sealed at DEV-484; plaintext copy in Passbolt entry restic / monitoring backups. Rotation: restic key add → seal new value → restic key remove old id. Same key protects all three remaining repos (loki/grafana/prometheus) — rotating rewrites the key file on every repo.

Restore

Restore procedure for all four restic repos: docs/monitoring/restic-restore.md. First drill (2026-08-16, loki/k8s-resources) passed — see DEV-488. Prometheus repo added and drilled same day — see DEV-492.

Emitted metrics (textfile-collector format)

Each restic CronJob writes /metrics/backup_<kind>.prom (atomic — .prom.tmp + mv) into a hostPath volume mounted at the node-exporter textfile-collector directory (/var/lib/node_exporter/textfile_collector). The kube-prometheus-stack node-exporter DaemonSet has --collector.textfile.directory=/host/textfile_collector enabled (DEV-494, applied via apps/observability/patches/node-exporter-textfile-collector.yaml) and surfaces those samples in Prometheus.

Metric Emitted by
backup_<kind>_success (0/1) backup-{loki,grafana}-*-cronjob.yaml, prometheus-backup-cronjob.yaml
backup_<kind>_timestamp_seconds same
backup_<kind>_check_status (exit code) same — from restic check --read-data-subset=5%
backup_prometheus_backup_status prometheus-backup-cronjob.yaml — restic backup exit code (3 = accepted compaction race)
restic_repo_size_bytes{repo="<kind>"} same — from restic stats --json --mode raw-data (added in DEV-490)

Cross-node staleness note. Because a backup CronJob may run on a different worker across days (loki/prometheus follow their app pods), a .prom file can linger on a node the job has since left and node-exporter keeps exposing it. The freshness alerts collapse the per-node samples with max() so the freshest sample wins; check/size alerts fire when any node reports a bad value, which is intentional — a recent failure is still a signal until the file is manually cleaned or the job returns to that node.

Alerts

backup-restic-alerts.yaml defines seven alerts (all severity: warning):

  • BackupLokiStale / BackupGrafanaStale / BackupPrometheusStale — time() - max(backup_<kind>_timestamp_seconds) > 28h. Daily schedule + 4 h grace. max() collapses per-node samples so a stale .prom on a node the job has left does not fire.
  • BackupLokiCheckFailed / BackupGrafanaCheckFailed / BackupPrometheusCheckFailed — backup_<kind>_check_status != 0.
  • ResticRepoOversize — restic_repo_size_bytes > 20 GiB. Baseline expected < 5 GiB; catches retention/prune regressions.

All alerts carry a Runbook: docs/monitoring/restic-restore.md annotation. To iterate on the rule file locally:

awk '/^spec:/{f=1;next} f{sub(/^  /,"");print}' \
  apps/monitoring/backup-restic-alerts.yaml > /tmp/backup-restic-rules.yaml
promtool check rules /tmp/backup-restic-rules.yaml
promtool test rules  apps/monitoring/backup-restic-alerts.test.yaml

The legacy backup-volumes CronJob and its 100 Gi local-path backup-storage PVC were retired in DEV-489 once the restic pipeline was proven end-to-end. With the shared destination PVC gone, the DEV-483 bridge nodeSelector pinning Loki to k3s-worker-2 was also removed — backup-loki-restic follows the Loki pod via podAffinity regardless of which node the RWO CSI volume lands on. backup-grafana-restic still nodeSelects k3s-worker-2 because its source PV (grafana-storage, local-path) is anchored there. prometheus-backup uses podAffinity on app=prometheus (RWO PVC on Hetzner CSI, single-node attach) and follows the Prometheus pod between workers. The backup-k8s-resources CronJob and its restic repo were retired in DEV-576 after Velero cluster-all took over cluster-manifest coverage.