How to Use VolumeSnapshots in Kubernetes

How to Use VolumeSnapshots in Kubernetes

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: Retain on snapshot classes used for compliance/DR — an accidental kubectl delete namespace can 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.

References

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Kubernetes Monitoring with Alertmanager

How to Set Up Kubernetes Monitoring with Alertmanager

Next Post
How to Set Up a Kubernetes Development Environment with Kind

How to Set Up a Kubernetes Development Environment with Kind

Related Posts