The moment VolumeSnapshots actually mattered to me was during a botched database migration where a bad script wiped rows it shouldn’t have touched. Restoring from a raw cloud provider snapshot taken outside Kubernetes meant digging through the console, matching volume IDs by hand, and hoping I’d grabbed the right one. Once I switched to Kubernetes-native VolumeSnapshots, that entire process became a kubectl apply away — versioned, labeled, and tied directly to the PVC it came from.
What Are VolumeSnapshots?
VolumeSnapshots are a Kubernetes API resource (part of the snapshot.storage.k8s.io API group) that let you capture the state of a PersistentVolume at a point in time, using your storage backend’s native snapshot capability (EBS snapshots, GCE PD snapshots, Ceph RBD snapshots, etc.) — all exposed through a consistent Kubernetes-native interface via the CSI (Container Storage Interface).
This matters because before the Snapshot API existed, snapshotting was entirely provider-specific and lived outside Kubernetes entirely.
Architecture: How Snapshotting Works
Three CRDs make up the snapshot API:
- VolumeSnapshotClass — cluster-scoped, defines which CSI driver handles snapshots and any driver-specific parameters (analogous to StorageClass for provisioning).
- VolumeSnapshot — namespaced, a user-facing request for a snapshot of a specific PVC.
- VolumeSnapshotContent — cluster-scoped, the actual bound snapshot resource, usually created automatically by the CSI driver (dynamic provisioning) but can also be pre-provisioned.
The flow: you create a VolumeSnapshot referencing a PVC → the CSI external-snapshotter sidecar sees it → it calls the CSI driver’s CreateSnapshot gRPC method → the driver talks to the storage backend → a VolumeSnapshotContent is created and bound back to your VolumeSnapshot.
Prerequisites
- A CSI driver that supports the snapshot capability (most major ones do:
ebs.csi.aws.com,pd.csi.storage.gke.io,disk.csi.azure.com, Ceph-CSI, Longhorn, etc.) - The snapshot CRDs and controller installed in the cluster
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
Verify:
kubectl get pods -n kube-system | grep snapshot-controller
Step 1: Create a VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-ebs-snapclass
driver: ebs.csi.aws.com
deletionPolicy: Delete
deletionPolicy: Delete means deleting the VolumeSnapshot object also deletes the underlying storage snapshot. Use Retain for anything you need to survive accidental kubectl delete commands — recommended for compliance/backup snapshots.
kubectl apply -f volumesnapshotclass.yaml
Step 2: Take a Snapshot of an Existing PVC
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-data-snapshot
namespace: production
spec:
volumeSnapshotClassName: csi-ebs-snapclass
source:
persistentVolumeClaimName: postgres-data
kubectl apply -f postgres-snapshot.yaml
kubectl get volumesnapshot -n production
NAME READYTOUSE SOURCEPVC RESTORESIZE SNAPSHOTCLASS
postgres-data-snapshot true postgres-data 20Gi csi-ebs-snapclass
READYTOUSE: true confirms the storage backend finished creating the snapshot and it can now be used for restores.
Step 3: Restore a PVC from a Snapshot
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data-restored
namespace: production
spec:
storageClassName: gp3
dataSource:
name: postgres-data-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
kubectl apply -f postgres-restore-pvc.yaml
kubectl get pvc postgres-data-restored -n production
Point a new Pod (or a recovery StatefulSet) at this restored PVC to inspect or promote the recovered data:
apiVersion: v1
kind: Pod
metadata:
name: postgres-recovery-check
namespace: production
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data-restored
Scheduled Snapshots with CronJob
There’s no built-in “scheduled VolumeSnapshot” resource, so you script it with a CronJob and RBAC-scoped ServiceAccount:
apiVersion: v1
kind: ServiceAccount
metadata:
name: snapshot-creator
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: snapshot-creator-role
namespace: production
rules:
- apiGroups: ["snapshot.storage.k8s.io"]
resources: ["volumesnapshots"]
verbs: ["create", "get", "list", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: snapshot-creator-binding
namespace: production
subjects:
- kind: ServiceAccount
name: snapshot-creator
namespace: production
roleRef:
kind: Role
name: snapshot-creator-role
apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-postgres-snapshot
namespace: production
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: snapshot-creator
containers:
- name: snapshotter
image: bitnami/kubectl:1.30
command:
- /bin/sh
- -c
- |
kubectl apply -f - <<EOF
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-data-$(date +%Y%m%d-%H%M%S)
namespace: production
spec:
volumeSnapshotClassName: csi-ebs-snapclass
source:
persistentVolumeClaimName: postgres-data
EOF
restartPolicy: OnFailure
Cleaning Up Old Snapshots
Pair scheduled creation with a retention CronJob so snapshots don’t accumulate indefinitely and inflate storage costs:
kubectl get volumesnapshot -n production --sort-by=.metadata.creationTimestamp
apiVersion: batch/v1
kind: CronJob
metadata:
name: snapshot-cleanup
namespace: production
spec:
schedule: "0 4 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: snapshot-creator
containers:
- name: cleanup
image: bitnami/kubectl:1.30
command:
- /bin/sh
- -c
- |
kubectl get volumesnapshot -n production -o json | \
jq -r '.items[] | select(.metadata.creationTimestamp < (now - 604800 | todate)) | .metadata.name' | \
xargs -r -n1 kubectl delete volumesnapshot -n production
restartPolicy: OnFailure
This deletes snapshots older than 7 days (604800 seconds).
Cross-Namespace and Cross-Cluster Considerations
Snapshots are namespace-scoped and generally tied to the storage backend region/account of the source PVC. Moving a snapshot to a different cluster or region typically requires backend-specific tooling (e.g., copying an EBS snapshot to another region via AWS APIs) — the Kubernetes VolumeSnapshot API itself doesn’t handle cross-region replication.
Integration with Velero for Full Backup/DR
For complete disaster recovery — not just data volumes but entire namespace state (Deployments, Services, ConfigMaps, etc.) — combine VolumeSnapshots with Velero:
velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws:v1.10.0 \
--bucket my-velero-backups \
--backup-location-config region=us-east-1 \
--snapshot-location-config region=us-east-1 \
--use-node-agent
velero backup create production-backup --include-namespaces production
velero restore create --from-backup production-backup
Velero uses the CSI VolumeSnapshot API under the hood when configured with the CSI plugin, giving you application-consistent, cluster-object-aware backups rather than just raw volume data.
Pre-Provisioned Snapshots
Sometimes a snapshot already exists on the storage backend outside of Kubernetes — created manually, by a legacy script, or by a different tool entirely. You can bind it into the cluster as a VolumeSnapshotContent and reference it from a VolumeSnapshot, rather than only supporting dynamic provisioning:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotContent
metadata:
name: preexisting-snapshot-content
spec:
deletionPolicy: Retain
driver: ebs.csi.aws.com
source:
snapshotHandle: snap-0123456789abcdef0
volumeSnapshotRef:
name: preexisting-snapshot
namespace: production
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: preexisting-snapshot
namespace: production
spec:
source:
volumeSnapshotContentName: preexisting-snapshot-content
This is the mechanism you’d reach for during a migration — importing snapshots taken before your cluster adopted the CSI Snapshot API, or reusing a snapshot a cloud provider console created directly.
Snapshot Storage Costs and Lifecycle Management
Incremental snapshots (which is how most cloud block storage snapshotting works, including EBS) only store the delta from the previous snapshot, but costs still accumulate as your retention window grows and your data changes. It’s worth pairing scheduled snapshots with clear retention tiers rather than a single flat policy — for example, keeping hourly snapshots for a day, daily snapshots for a month, and weekly snapshots for a year, deleting everything in between. This is typically implemented as a slightly more sophisticated version of the cleanup CronJob shown earlier, filtering by both age and a label indicating snapshot tier:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-weekly-20260802
namespace: production
labels:
tier: weekly
spec:
volumeSnapshotClassName: csi-ebs-snapclass
source:
persistentVolumeClaimName: postgres-data
kubectl get volumesnapshot -n production -l tier=weekly --sort-by=.metadata.creationTimestamp
Filtering cleanup logic by the tier label lets you apply different retention windows to different snapshot cadences without maintaining separate CronJobs for every combination.
Common Mistakes
- Not setting
deletionPolicy: Retainon snapshot classes used for compliance/DR — an accidentalkubectl delete namespacecan cascade-delete your only backups. - Assuming snapshots are application-consistent by default. A raw volume snapshot of a running database can capture data mid-write. For true consistency, quiesce the application (e.g.,
pg_start_backup) or use a CSI driver/plugin that supports pre/post snapshot hooks. - No retention policy, leading to runaway storage costs from accumulated snapshots.
- Confusing VolumeSnapshot with VolumeSnapshotContent — you almost always work with VolumeSnapshot; VolumeSnapshotContent is typically managed automatically.
- Restoring into a different-sized PVC incorrectly — the restore size must be greater than or equal to the snapshot’s
restoreSize.
Application-Consistent Snapshots with Pre/Post Hooks
For databases specifically, some CSI drivers and orchestration tools support hooks that run before and after the snapshot to ensure consistency — flushing writes, briefly pausing transactions, or calling a database-specific backup API. Velero’s CSI plugin, for instance, supports pod exec hooks alongside VolumeSnapshot creation:
apiVersion: v1
kind: Pod
metadata:
name: postgres
annotations:
pre.hook.backup.velero.io/container: postgres
pre.hook.backup.velero.io/command: '["/bin/bash", "-c", "psql -c \"SELECT pg_start_backup(''velero-backup'')\""]'
post.hook.backup.velero.io/container: postgres
post.hook.backup.velero.io/command: '["/bin/bash", "-c", "psql -c \"SELECT pg_stop_backup()\""]'
spec:
containers:
- name: postgres
image: postgres:16
This ensures the snapshot captures a genuinely consistent point in the database’s write-ahead log rather than an arbitrary mid-transaction state — the difference between a backup that restores cleanly and one that requires manual recovery steps afterward.
Troubleshooting
# Check snapshot status
kubectl describe volumesnapshot <name> -n <namespace>
# Check the underlying content object
kubectl get volumesnapshotcontent
# Check the CSI snapshotter sidecar logs
kubectl logs -n kube-system -l app=csi-snapshotter -c csi-snapshotter
Common issue: READYTOUSE: false stuck indefinitely usually means the CSI driver lacks snapshot support enabled, or IAM/permissions on the storage backend are missing.
Summary
Kubernetes VolumeSnapshots bring point-in-time, storage-backend-native snapshotting into the standard Kubernetes API, letting you version, restore, and automate data protection with the same kubectl workflow you already use for everything else. Combine scheduled snapshot creation, retention cleanup, and — for full disaster recovery — Velero, to build a backup strategy that survives more than just accidental rm commands.