Stateful apps need point-in-time copies and a copy off the cluster. The lab uses VolSync with the restic mover: Kubernetes creates a VolumeSnapshot, VolSync runs restic against it, and encrypted data lands in a remote repository. The restic URL and password live in Vault and reach the cluster through External Secrets.
VolSync sits in the middle: you already run the CSI snapshot
controller and a storage class that supports snapshots
(Rook-Ceph block volumes in the lab). VolSync watches a ReplicationSource, triggers on a schedule, and spins
up a short-lived mover job. You get off-site copies without shelling into pods to run restic by hand.
Building blocks
| Software | Role |
|---|---|
| snapshot-controller | CSI snapshot API |
| VolSync | ReplicationSource and movers (restic, rsync, …) |
| restic | Encrypted, deduplicated backup format |
| Rook-Ceph | Block volumes and snapshot class |
Data path
app PVC
-> ReplicationSource (schedule, e.g. @daily)
-> VolumeSnapshot (CSI)
-> restic mover -> remote repository
Retention and prune settings sit on the ReplicationSource. See VolSync
restic usage.
GitOps pattern
- A common Flux app installs the VolSync operator once.
- Each app includes the shared template and sets
dependsOn: volsync. - Flux postBuild sets
APP, capacity, schedule, and storage class — same mechanism as domains in the GitOps overview.
Per-app repository paths append ${APP} at runtime so one leaked credential does not cover every volume.
Opt-in is intentional: not every Deployment needs a PVC backup. Stateless replicas and caches stay out unless you add the template. That keeps mover jobs and repository size predictable.
Example: opt in an app
The bundle in k8s/templates/volsync/ ships a PVC, ExternalSecret, ReplicationSource, and ReplicationDestination.
1. Kustomize — include the template beside the workload:
# app/kustomization.yaml
resources:
- ../../../../../../templates/volsync
- deployment.yaml
2. Flux — depend on the operator and substitute variables:
# ks.yaml (Flux Kustomization)
spec:
dependsOn:
- name: volsync
postBuild:
substitute:
APP: my-app
VOLSYNC_SCHEDULE: "@daily"
VOLSYNC_CAPACITY: 5Gi
VOLSYNC_CACHE_CAPACITY: 5Gi
VOLSYNC_STORAGECLASS: "${BLOCK_STORAGE_CLASS}"
VOLSYNC_SNAPSHOTCLASS: "${BLOCK_STORAGE_CLASS}"
3. Deployment — mount the PVC the template creates (claimName must match APP):
volumes:
- name: data
persistentVolumeClaim:
claimName: my-app
Flux renders ${APP} on the ReplicationSource and sets repository: my-app-volsync-secret (restic settings
from Vault via ExternalSecret):
apiVersion: volsync.backube/v1alpha1
kind: ReplicationSource
metadata:
name: "${APP}"
spec:
sourcePVC: "${APP}"
trigger:
schedule: "${VOLSYNC_SCHEDULE:-@weekly}"
restic:
copyMethod: Snapshot
repository: "${APP}-volsync-secret"
Full field list: ReplicationSource API.
Why snapshots?
A snapshot avoids stopping the pod and works well with Ceph’s CSI driver. The mover reads consistent blocks from the snapshot volume instead of the live mount, which matters when the app keeps writing logs or database pages.
Restore is the mirror path: a ReplicationDestination pulls from restic into a target PVC when you need to
recover or clone. The lab wraps that in task helpers; semantics follow the VolSync
user guide.