How to Capture Database Dumps on Staging PVCs
Some applications need a point-in-time dump instead of a live volume
snapshot. This pattern creates a staging PersistentVolumeClaim (PVC) in
backupPrehook and writes the dump to it. It then declares the PVC for
late capture, so Kasten captures it in the
restore point.
This procedure uses a PostgreSQL StatefulSet as a concrete example. The dump and replay commands are specific to PostgreSQL. Adapt them to your application's tools.
Prerequisites
- A blueprint bound to the workload through an annotation or a BlueprintBinding.
-
A CSI storage class that supports volume snapshots and the
Filesystemvolume mode. The cluster default applies unless you set one in step 1. - A backup policy whose resource filters exclude the application's own data volume. The dump captures that data, so a snapshot of the volume is redundant.
- Database credentials the blueprint can read. This example reads a Secret named after the StatefulSet.
- A location profile for Kanister actions on the policy. This blueprint passes artifacts between actions, which requires one.
Procedure
The following steps build one blueprint. For the assembled result, see Complete Blueprint.
-
In
backupPrehook, declare alateCaptureoutput artifact and create the staging PVC with a deterministic name.actions:backupPrehook:kind: StatefulSetoutputArtifacts:lateCapture:keyValue:apiVersion: "{{ .Phases.createStagingPVC.Output.apiVersion }}"group: "{{ .Phases.createStagingPVC.Output.group }}"resource: "{{ .Phases.createStagingPVC.Output.resource }}"name: "{{ .Phases.createStagingPVC.Output.name }}"namespace: "{{ .Phases.createStagingPVC.Output.namespace }}"phases:- func: KubeOpsname: createStagingPVCargs:operation: createnamespace: "{{ .StatefulSet.Namespace }}"spec: |-apiVersion: v1kind: PersistentVolumeClaimmetadata:name: "{{ .StatefulSet.Name }}-dump"namespace: "{{ .StatefulSet.Namespace }}"spec:accessModes: ["ReadWriteOnce"]resources:requests:storage: 10GiThis PersistentVolumeClaim omits
storageClassName, so it uses the cluster default. SetstorageClassNamewhen you need a specific class. Size the storage request to hold the uncompressed dump, which is roughly the logical size of the database.Prefer a deterministic name, such as
{{ .StatefulSet.Name }}-dump.backupPosthookcan then delete the PVC by name. If a crashed backup leaves the PVC behind, Kasten fails the next backup, so the leftover cannot go unnoticed. If your use case requires a generated name, see Dynamic PVC Names. -
In the same action, use
PrepareDatato write the dump to the staging PVC and flush it to disk.- func: PrepareDataname: dumpDatabaseobjects:pgSecret:kind: Secretname: "{{ .StatefulSet.Name }}"namespace: "{{ .StatefulSet.Namespace }}"args:namespace: "{{ .StatefulSet.Namespace }}"image: postgres:16volumes:"{{ .Phases.createStagingPVC.Output.name }}": /backupcommand:- bash- -c- |export PGPASSWORD='{{ index .Phases.dumpDatabase.Secrets.pgSecret.Data "postgres-password" | toString }}'pg_dumpall --clean --if-exists -h "{{ .StatefulSet.Name }}" -U postgres > /backup/full.sqlsyncfailOnError: trueThe
synccall flushes the page cache before Kasten snapshots the PVC. Without it, the snapshot can capture a 0-byte file.The
--clean --if-existsflags drop and recreate each object on replay. A restore then replaces the database contents and does not add to them. The result is the same whether you restore to the original namespace or a new one. -
In
backupPosthook, delete the staging PVC by name. Kasten already snapshotted it after step 2, so the data is safe.backupPosthook:kind: StatefulSetphases:- func: KubeOpsname: deleteStagingPVCargs:operation: deleteobjectReference:apiVersion: v1resource: persistentvolumeclaimsname: "{{ .StatefulSet.Name }}-dump"namespace: "{{ .StatefulSet.Namespace }}"The
backupPosthookmust delete the staging PVC. A surviving PVC fails the backup. See Staging PVC Requirements. -
In
restorePosthook, declare the artifact as an input, mount the restored staging PVC and replay the dump.restorePosthook:kind: StatefulSetinputArtifactNames:- lateCapturephases:- func: PrepareDataname: replayDumpobjects:pgSecret:kind: Secretname: "{{ .StatefulSet.Name }}"namespace: "{{ .StatefulSet.Namespace }}"args:namespace: "{{ .StatefulSet.Namespace }}"image: postgres:16volumes:"{{ .ArtifactsIn.lateCapture.KeyValue.name }}": /restorecommand:- bash- -c- |export PGPASSWORD='{{ index .Phases.replayDump.Secrets.pgSecret.Data "postgres-password" | toString }}'psql -h "{{ .StatefulSet.Name }}" -U postgres -f /restore/full.sqlfailOnError: true- func: KubeOpsname: removeRestoredPVCargs:operation: deleteobjectReference:apiVersion: v1resource: persistentvolumeclaimsname: "{{ .ArtifactsIn.lateCapture.KeyValue.name }}"namespace: "{{ .StatefulSet.Namespace }}"
Verification
- Run a backup and open the restore point details. Confirm that the staging PVC appears with a volume snapshot, like any other captured PVC.
-
Run
kubectl get pvc -n <namespace>. Confirm the staging PVC is gone from the namespace. - Run a restore. Confirm the database contains the dumped data. Confirm the restored staging PVC is gone from the namespace.
Dynamic PVC Names
When your use case requires it, Kasten supports a generated name, such as one with a timestamp. Kasten fails the next backup only when a leftover PVC has the same name. A leftover PVC with a different name can go undetected. Kasten discovers and snapshots it like any other PVC until you delete it, which consumes storage unnecessarily.
Because the hook cannot reconstruct a generated name, add a label to the PVC
so that backupPosthook can find it. Add a workload-scoping label as well
when several workloads in the namespace each create one. Delete by the labels
you set:
command:
- bash
- -c
- |
kubectl delete pvc -n "{{ .StatefulSet.Namespace }}" \
-l example.com/staging-pvc=true,example.com/workload={{ .StatefulSet.Name }} \
--ignore-not-found
Complete Blueprint
The following blueprint combines every step in this procedure.
apiVersion: cr.kanister.io/v1alpha1
kind: Blueprint
metadata:
name: postgres-staging-pvc
namespace: kasten-io
actions:
backupPrehook:
kind: StatefulSet
outputArtifacts:
lateCapture:
keyValue:
apiVersion: "{{ .Phases.createStagingPVC.Output.apiVersion }}"
group: "{{ .Phases.createStagingPVC.Output.group }}"
resource: "{{ .Phases.createStagingPVC.Output.resource }}"
name: "{{ .Phases.createStagingPVC.Output.name }}"
namespace: "{{ .Phases.createStagingPVC.Output.namespace }}"
phases:
- func: KubeOps
name: createStagingPVC
args:
operation: create
namespace: "{{ .StatefulSet.Namespace }}"
spec: |-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "{{ .StatefulSet.Name }}-dump"
namespace: "{{ .StatefulSet.Namespace }}"
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
- func: PrepareData
name: dumpDatabase
objects:
pgSecret:
kind: Secret
name: "{{ .StatefulSet.Name }}"
namespace: "{{ .StatefulSet.Namespace }}"
args:
namespace: "{{ .StatefulSet.Namespace }}"
image: postgres:16
volumes:
"{{ .Phases.createStagingPVC.Output.name }}": /backup
command:
- bash
- -c
- |
export PGPASSWORD='{{ index .Phases.dumpDatabase.Secrets.pgSecret.Data "postgres-password" | toString }}'
pg_dumpall --clean --if-exists -h "{{ .StatefulSet.Name }}" -U postgres > /backup/full.sql
sync
failOnError: true
backupPosthook:
kind: StatefulSet
phases:
- func: KubeOps
name: deleteStagingPVC
args:
operation: delete
objectReference:
apiVersion: v1
resource: persistentvolumeclaims
name: "{{ .StatefulSet.Name }}-dump"
namespace: "{{ .StatefulSet.Namespace }}"
restorePosthook:
kind: StatefulSet
inputArtifactNames:
- lateCapture
phases:
- func: PrepareData
name: replayDump
objects:
pgSecret:
kind: Secret
name: "{{ .StatefulSet.Name }}"
namespace: "{{ .StatefulSet.Namespace }}"
args:
namespace: "{{ .StatefulSet.Namespace }}"
image: postgres:16
volumes:
"{{ .ArtifactsIn.lateCapture.KeyValue.name }}": /restore
command:
- bash
- -c
- |
export PGPASSWORD='{{ index .Phases.replayDump.Secrets.pgSecret.Data "postgres-password" | toString }}'
psql -h "{{ .StatefulSet.Name }}" -U postgres -f /restore/full.sql
failOnError: true
- func: KubeOps
name: removeRestoredPVC
args:
operation: delete
objectReference:
apiVersion: v1
resource: persistentvolumeclaims
name: "{{ .ArtifactsIn.lateCapture.KeyValue.name }}"
namespace: "{{ .StatefulSet.Namespace }}"
See Late Capture for the full declaration contract. See Phase Lifecycle Reference for where late capture runs in the backup sequence.