Skip to main content

Kubernetes Backup and Restore with Velero

Velero is an open-source tool that backs up and restores Kubernetes cluster resources and persistent volumes. It runs a server inside the cluster and a CLI on your workstation, and stores backups in S3-compatible object storage.

In this guide we install Velero on a Vietnix Cloud Kubernetes cluster, store backups in Vietnix Cloud S3, then back up and restore a sample application — including its persistent volumes.

How it works

  • The Velero server runs as a Deployment in the velero namespace.
  • Each backup (resource metadata plus volume data) is stored in an S3 bucket.
  • Persistent volumes are captured with CSI snapshots, and with the built-in data mover the snapshot data is moved to the bucket.
  • Velero can also perform file system backups (Kopia) for volumes that do not support snapshots.

Prerequisites

  • A Kubernetes cluster in Vietnix Cloud — Create Cluster and Connect Cluster.
  • An S3 bucket and an access keyManage Bucket and Manage Access Key.
  • kubectl and Helm installed on your workstation (see Connect Cluster).
  • The Velero CLI installed on your workstation. See the Velero basic install guide.
  • A StorageClass backed by a CSI driver that supports snapshots. On Vietnix Cloud use cinder.csi.openstack.org and set an explicit volume type (see Step 1).
  • Enough block storage (Cinder) quota in your project to create the persistent volumes you want to back up, since CSI snapshots are taken from Cinder volumes.

Step 1: Create a default StorageClass

If your cluster does not already have a default StorageClass, create one that uses the Vietnix Cloud CSI provisioner:

storage-class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: default
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: cinder.csi.openstack.org
parameters:
type: nvmer3
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
kubectl apply -f storage-class.yaml
kubectl get storageclass
Note

parameters.type selects the Cinder volume type. On Vietnix Cloud this is nvmer3. If your project uses a different volume type, change it accordingly — otherwise volume creation may fail with gigabytes_default ... quota is 0G.

Step 2: Check the CSI snapshot support

Velero uses the Kubernetes CSI snapshot APIs, so the VolumeSnapshot CRDs and the snapshot controller must be present. Check them:

kubectl api-resources | grep volumesnapshot
volumesnapshotclasses     vsclass,vsclasses     snapshot.storage.k8s.io/v1   false   VolumeSnapshotClass
volumesnapshotcontents vsc,vscs snapshot.storage.k8s.io/v1 false VolumeSnapshotContent
volumesnapshots vs snapshot.storage.k8s.io/v1 true VolumeSnapshot

If the output lists VolumeSnapshot, VolumeSnapshotClass, and VolumeSnapshotContent, you can skip the rest of this step. Otherwise, install the external-snapshotter:

git clone https://github.com/kubernetes-csi/external-snapshotter/
cd external-snapshotter

# Check out the release compatible with your Kubernetes version
git checkout v8.6.0

kubectl apply -f client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
kubectl apply -f deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
kubectl apply -f deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
Warning

Choose the external-snapshotter release that matches your Kubernetes version — see the compatibility matrix in the external-snapshotter repository. The example above (v8.6.0) targets recent Kubernetes versions.

Step 3: Create a VolumeSnapshotClass

Create a VolumeSnapshotClass for the Cinder CSI driver, with the label Velero uses to select it:

snapshotclass.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: cinder-snapclass
labels:
velero.io/csi-volumesnapshot-class: "true"
driver: cinder.csi.openstack.org
deletionPolicy: Delete
parameters:
force-create: "true"
kubectl apply -f snapshotclass.yaml
kubectl get volumesnapshotclass
NAME               DRIVER                     DELETIONPOLICY   AGE
cinder-snapclass cinder.csi.openstack.org Delete 1s

Step 4: Prepare the S3 credentials

Create a credentials file with your Vietnix Cloud S3 access key and secret key (see S3 Storage overview for the endpoint):

credentials-velero
[default]
aws_access_key_id=<YOUR_ACCESS_KEY>
aws_secret_access_key=<YOUR_SECRET_KEY>

Step 5: Install Velero with Helm

Create a values file that points Velero at your S3 bucket. Replace the bucket name with the bucket you created for backups:

velero-values.yaml
deployNodeAgent: true
snapshotsEnabled: false

initContainers:
- name: velero-plugin-for-aws
image: velero/velero-plugin-for-aws:v1.14.2
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /target
name: plugins

configuration:
features: EnableCSI
backupStorageLocation:
- name: default
provider: aws
bucket: <YOUR_BUCKET_NAME>
prefix: velero
default: true
config:
region: vn-hcm-1
s3Url: https://s3.vn-hcm-1.vietnix.cloud
s3ForcePathStyle: "true"
signatureVersion: "v4"

Install the chart:

helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts
helm repo update

helm install velero vmware-tanzu/velero \
--namespace velero \
--create-namespace \
--set-file credentials.secretContents.cloud=credentials-velero \
-f velero-values.yaml

Where:

  • deployNodeAgent: true deploys the node agent that is required for the data mover and file system backups.
  • snapshotsEnabled: false stops the chart from creating a default VolumeSnapshotLocation, which is not used for CSI snapshots.
  • features: EnableCSI enables CSI snapshot support on the Velero server. Without it, Velero falls back to the object-storage plugin's native snapshotter and cannot move snapshot data.
  • provider: aws is used because Velero talks to S3-compatible storage through the AWS plugin.
  • s3Url and region are the Vietnix Cloud S3 endpoint and region.
  • s3ForcePathStyle: "true" is required for S3-compatible endpoints.
Info

CSI snapshot support is built into Velero (since v1.14), but you must enable the EnableCSI feature flag on the server — this is why configuration.features: EnableCSI is set in the values file. Without it, Velero uses the object-storage plugin's native snapshotters and refuses to move snapshot data.

Step 6: Verify the installation

kubectl get pods -n velero
kubectl get backupstoragelocations -n velero
velero version

The Velero server, the node agent, and the default backup storage location should all be ready:

NAME                       READY   STATUS    RESTARTS   AGE
node-agent-xxxxx 1/1 Running 0 1m
node-agent-yyyyy 1/1 Running 0 1m
node-agent-zzzzz 1/1 Running 0 1m
velero-xxxxxxxxxx-aaaaa 1/1 Running 0 1m
NAME      PHASE       LAST VALIDATED   AGE   DEFAULT
default Available 57s 4m true
Client:
Version: v1.18.2
Server:
Version: v1.18.1

Enable the CSI feature on the Velero client so that velero backup describe shows CSI snapshot details:

velero client config set features=EnableCSI

Step 7: Deploy a sample application

To demonstrate a backup, deploy a small application that stores its data on a persistent volume:

demo-app.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
labels:
app: demo-app
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
labels:
app: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: web
image: nginx:stable
ports:
- containerPort: 80
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
command: ["/bin/sh", "-c"]
args:
- echo "Hello from the Velero demo" > /usr/share/nginx/html/index.html && exec nginx -g 'daemon off;'
volumes:
- name: data
persistentVolumeClaim:
claimName: demo-pvc
---
apiVersion: v1
kind: Service
metadata:
name: demo-app
labels:
app: demo-app
spec:
selector:
app: demo-app
ports:
- port: 80
kubectl apply -f demo-app.yaml
kubectl get deployments,pods,pvc

Wait until the pod is running and the volume is bound:

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/demo-app 1/1 1 1 30s

NAME READY STATUS RESTARTS AGE
pod/demo-app-647dc64f7-ztwfq 1/1 Running 0 30s

NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/demo-pvc Bound pvc-72252ff1-9785-4345-a760-a59da46897ba 1Gi RWO default 30s

Step 8: Create a backup

Back up every resource that carries the app=demo-app label:

velero backup create demo-b1 \
--selector app=demo-app \
--snapshot-volumes --snapshot-move-data

Where:

  • --selector app=demo-app selects the sample application resources.
  • --snapshot-volumes captures persistent volumes with CSI snapshots.
  • --snapshot-move-data moves the snapshot data to the S3 bucket.

Check the backup and the data movement:

kubectl get backups -n velero
kubectl get datauploads -n velero
velero backup describe demo-b1 --details
NAME      STATUS      ERRORS   WARNINGS   CREATED                         EXPIRES   STORAGE LOCATION   SELECTOR
demo-b1 Completed 0 0 2026-09-15 11:42:06 +0700 +07 29d default app=demo-app
NAME             STATUS      STARTED   BYTES DONE   TOTAL BYTES   STORAGE LOCATION   AGE   NODE
demo-b1-xxxxx Completed 68s 27 27 default 77s node-2

The backup Phase should be Completed, and the DataUpload objects should be Completed. In velero backup describe, the CSI snapshot is shown under Backup Item Operations and Backup Volumes:

Phase:  Completed

Backup Item Operations:
Operation for persistentvolumeclaims default/demo-pvc:
Backup Item Action Plugin: velero.io/csi-pvc-backupper
Phase: Completed
Progress description: Completed

Backup Volumes:
CSI Snapshots:
default/demo-pvc:
Data Movement:
Data Mover: velero
Uploader Type: kopia
Moved data Size (bytes): 27
Result: succeeded

Step 9: Restore the application

Simulate an accidental deletion:

kubectl delete -f demo-app.yaml

Restore from the backup:

velero create restore demo-restore1 --from-backup demo-b1
kubectl get restores -n velero
kubectl -n velero get datadownloads
NAME            AGE
demo-restore1 2m

NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE
demo-restore1-x5k2p Completed 112s 27 27 default 2m node-2

After a few minutes the application and its persistent data are restored:

kubectl get deployments,pods,pvc
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/demo-app 1/1 1 1 2m

NAME READY STATUS RESTARTS AGE
pod/demo-app-647dc64f7-ztwfq 1/1 Running 0 2m

NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/demo-pvc Bound pvc-96e97482-3e47-4d35-ab93-9ecb15c3a00d 1Gi RWO default 2m

Step 10: Schedule backups

Velero uses cron expressions to schedule backups. To run a backup every hour:

velero schedule create demo-hourly \
--schedule="0 * * * *" \
--selector app=demo-app \
--snapshot-volumes --snapshot-move-data

velero schedule get
NAME          STATUS    CREATED                         SCHEDULE    BACKUP TTL   LAST BACKUP   SELECTOR       PAUSED
demo-hourly Enabled 2026-09-15 11:51:34 +0700 +07 0 * * * * 0s n/a app=demo-app false

Step 11 (Optional): File system backup

If a volume cannot be captured with a CSI snapshot, Velero can copy its data from the file system instead:

velero backup create demo-b2 \
--selector app=demo-app \
--default-volumes-to-fs-backup

kubectl -n velero get podvolumebackups
velero backup describe demo-b2 --details
NAME             STATUS      STARTED   BYTES DONE   TOTAL BYTES   STORAGE LOCATION   AGE   NODE   UPLOADER
demo-b2-cj5j4 Completed 3s 27 27 default 4s kopia
Backup Volumes:
Pod Volume Backups - kopia:
Completed:
default/demo-app-7c9f6d8b5-x2lmn: data (size: 27, incremental size: 27)

Cleanup

Delete the schedule and backups (optional):

velero schedule delete demo-hourly --confirm
velero backup delete demo-b1 --confirm

Remove the sample application and Velero:

kubectl delete -f demo-app.yaml
helm uninstall velero --namespace velero
Warning

Uninstalling Velero does not delete the backups stored in your S3 bucket. Delete the velero/ prefix in the bucket manually if you no longer need them.