Ein Backup-System, das nur funktioniert, wenn man Zugangsdaten quer durch die Konfiguration verteilt, hat schon verloren. In meinem Lab sichern die Anwendungen ihre Daten mit VolSync nach restic — und das Schöne daran: Weder die Adresse des Remote-Repos noch sein Passwort stehen jemals im Git, und der eigentliche Anwendungs-Pod bekommt sie nie zu Gesicht.
Ein Template, viele Anwendungen#
Backup soll kein Sonderfall pro App sein, sondern ein Schalter, den man umlegt. Deshalb liegt die gesamte Mechanik in einem geteilten Template unter k8s/templates/volsync/ und besteht aus vier Bausteinen:
- einer
ReplicationSource— der geplante Backup-Job, - einer
ReplicationDestination— der Restore-Pfad, - einem
PersistentVolumeClaim, der sich aus genau dieser Destination speist, - und einem
ExternalSecret, das die restic-Zugangsdaten aus Vault holt.
Eine Anwendung „abonniert“ Backups, indem sie das Template in ihre Kustomization aufnimmt und eine Handvoll Variablen stempelt. Mehr nicht. Namespaces mit strengen Pod-Security-Standards — bei mir etwa Vault, ZNC oder ZeroClaw — geben ihren kurzlebigen Movern zusätzlich das Label volsync.backube/privileged-movers: "true" mit.
Das Geheimnis bleibt im Tresor#
Hier liegt der Kern. Das ExternalSecret zieht Repo-Adresse und Passwort aus Vault und baut daraus erst zur Laufzeit das Secret zusammen, das der VolSync-Mover nutzt:
1apiVersion: external-secrets.io/v1
2kind: ExternalSecret
3metadata:
4 name: "${APP}-volsync"
5spec:
6 secretStoreRef:
7 kind: ClusterSecretStore
8 name: vault-backend
9 target:
10 name: "${APP}-volsync-secret"
11 template:
12 data:
13 RESTIC_REPOSITORY: "{{ .RESTIC_REPOSITORY }}/${APP}"
14 RESTIC_PASSWORD: "{{ .RESTIC_PASSWORD }}"
15 data:
16 - secretKey: RESTIC_REPOSITORY
17 remoteRef:
18 key: "${CLUSTER_NAME}/volsync/restic/borgbase"
19 property: repo
20 - secretKey: RESTIC_PASSWORD
21 remoteRef:
22 key: "${CLUSTER_NAME}/volsync/restic/borgbase"
23 property: key
Drei Dinge fallen auf:
- Im Git steht nichts Sensibles — nur Pfadangaben nach Vault. Repo und Passwort leben ausschließlich im Tresor.
- Pro App ein eigener Repo-Unterpfad (
{{ .RESTIC_REPOSITORY }}/${APP}). Alle teilen sich dasselbe restic-Remote bei BorgBase , aber jede App sichert in ihre eigene Ecke. - Der App-Pod sieht die Credentials nie. Nur der kurzlebige VolSync-Mover-Pod bekommt das
${APP}-volsync-secretgemountet — die Anwendung selbst hat damit nichts zu tun.
Sichern und Wiederherstellen#
Die ReplicationSource ist der eigentliche Backup-Job. Sie zieht per CSI einen konsistenten Snapshot des PVCs (copyMethod: Snapshot), übergibt ihn an restic und hält eine gestaffelte Aufbewahrung vor:
1spec:
2 sourcePVC: "${APP}"
3 trigger:
4 schedule: "${VOLSYNC_SCHEDULE}" # Standard: "0 3 * * 0" — sonntags 03:00
5 restic:
6 copyMethod: Snapshot
7 pruneIntervalDays: 7
8 repository: "${APP}-volsync-secret"
9 retain:
10 hourly: 24
11 daily: 7
12 weekly: 5
Zwei Feinheiten stecken hier drin. copyMethod: Snapshot liest über einen CSI-VolumeSnapshot einen konsistenten Stand — der Mover fasst nie das live gemountete Volume an, was bei schreibenden Diensten wie PostgreSQL oder Vault über brauchbares oder korruptes Backup entscheidet. Voraussetzung dafür ist ein installierter CSI-Snapshot-Controller mit passender VolumeSnapshotClass; fehlt der, läuft copyMethod: Snapshot wirkungslos ins Leere. Und der Schedule steht bewusst in Cron-Syntax (0 3 * * 0) statt als @weekly — YAML 1.1 deutet ein führendes @ als reserviertes Zeichen, und der Mover bekäme nie einen gültigen Plan.
Der Restore-Pfad ist der elegante Teil: Der PVC der Anwendung wird nicht leer angelegt, sondern referenziert über dataSourceRef die ReplicationDestination. Beim ersten Anlegen kann VolSync den PVC damit direkt aus dem letzten restic-Snapshot befüllen — genau das, was man bei einem Cluster-Neuaufbau braucht:
1spec:
2 dataSourceRef:
3 kind: ReplicationDestination
4 apiGroup: volsync.backube
5 name: "${APP}-dst"
Das greift sauber beim ersten Anlegen des PVC — beim Cluster-Neuaufbau also genau richtig. Einen bereits laufenden Dienst auf einen alten Stand zurückzuholen ist dagegen kein Live-Cutover: Das verlangt einen Ziel-PVC, angepasste claimName-Referenzen und einen geplanten Neustart — ein Pfad, den man einmal im Lab geübt haben sollte, bevor man im Ernstfall darauf angewiesen ist.
Das Zusammenspiel im Überblick:
Ein handfestes Beispiel: snac#
Meine snac-Instanz hält ihren gesamten Zustand in einem Verzeichnisbaum auf einem einzigen PVC — ein idealer Kandidat. Das Abonnement besteht aus zwei winzigen Stellen. In der App-Kustomization wird das Template eingebunden:
1resources:
2 - ../../../../../../templates/volsync
3 - deployment.yaml
4 - service.yaml
5 - httproute.yaml
Und in der Flux-Kustomization werden die Variablen gestempelt — inklusive dependsOn: volsync, damit der Operator vorher steht:
1 dependsOn:
2 - name: volsync
3 postBuild:
4 substitute:
5 APP: snac
6 VOLSYNC_CAPACITY: 5Gi
7 VOLSYNC_CACHE_CAPACITY: 5Gi
8 VOLSYNC_STORAGECLASS: "ceph-block"
9 VOLSYNC_SNAPSHOTCLASS: "ceph-block"
10 VOLSYNC_CACHE_SNAPSHOTCLASS: "ceph-block"
Im Deployment mountet snac dann einfach den PVC namens snac, den VolSync bereitstellt — fertig. Ab da sichert sich snac jeden Sonntag um drei selbst.
Die Bedienung läuft über ein paar Tasks, die Repo-Pfad und Schlüssel im Hintergrund aus Vault ziehen:
1task backup:list NS=fediverse-system APP=snac # Snapshots auflisten
2task backup:snapshot NS=fediverse-system APP=snac # sofort sichern
3task backup:restore NS=fediverse-system APP=snac # neuesten Stand zurückspielen
Fazit#
VolSync verlagert Backups dorthin, wo sie hingehören: in die Plattform, deklarativ, pro Anwendung mit einem Dreizeiler aktivierbar. Die wirklich wichtige Eigenschaft ist aber die Trennung der Geheimnisse — Repo-Adresse und Passwort wohnen im Vault, materialisieren sich nur im flüchtigen Mover-Pod und tauchen weder im Repository noch in der Anwendung auf. So fühlt sich ein Backup-System an, dem man auch im Ernstfall vertraut.
Nachtrag (25. Juni 2026): Wenn der Restore mehr Rechte braucht#
Eine Stelle, an der mich VolSync zuletzt überraschte: Der Restore selbst kann an den Berechtigungen scheitern. Restic stellt beim Zurückspielen die ursprünglichen Datei-Eigentümer wieder her — und ruft dafür lchown auf, was CAP_CHOWN voraussetzt. Der Default-Mover läuft aber bewusst unprivilegiert (drop: ["ALL"]), also kippt der Restore mit operation not permitted, sobald die Daten nicht alle dem Mover-User gehören.
Aufgefallen ist das beim Build-Namespace meines CI-Brokers
: Dort liegen der Radicle-Store und das /run/containers von rootless buildah, beide mit Datei-Eigentümern, die der Restore exakt rekonstruieren muss. Die Lösung ist eine Namespace-Annotation, die VolSync in den privilegierten Mover schaltet:
1metadata:
2 annotations:
3 # Restic-Restore stellt Original-Ownership per lchown wieder her -> CAP_CHOWN.
4 volsync.backube/privileged-movers: "true"
Der privilegierte Mover läuft als root mit CHOWN/FOWNER/DAC_OVERRIDE/SETUID/SETGID. Das gebe ich nicht leichtfertig her — nur in Namespaces, die ohnehin unter privilegiertem Pod-Security-Standard stehen, wie eben der Build-Namespace mit seinem rootful buildah. Für die normalen App-Backups bleibt es beim unprivilegierten Default; ein snac-PVC braucht das nicht. Es ist die Sorte Detail, die man erst im Ernstfall lernt — nämlich dann, wenn man den Restore wirklich einmal braucht.
