Backing up and Restoring Kubevirt VMs with Velero¶
Since OpenShift Virtualization (Kubevirt) runs VMs inside a container, these can be backed up using Velero just like any other containerized workload.
Pre-checks¶
Use snapshottable storage
You can skip this section if your VMs are already backed by snapshotable storage like Synology, Ceph/ODF, etc.
The assumption is that your disks are iSCSI backed by a snapshot capable CSI driver. This sections talks about converting QCOW images to raw iscsi disks.
-
Create a peristent volume claim large enough to hold the non-sparse disk contents:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: radius02-iscsi namespace: virtual-machines spec: accessModes: - ReadWriteOnce volumeMode: Block resources: requests: storage: 25Gi storageClassName: synology-iscsi -
Create a small utility pod that you can mount both filesystems in order to transfer the data:
apiVersion: v1 kind: Pod metadata: name: disk-migrator namespace: virtual-machines spec: restartPolicy: Never containers: - name: mover image: registry.access.redhat.com/ubi9/ubi command: ["/bin/bash", "-c", "sleep infinity"] securityContext: privileged: true volumeDevices: - name: target devicePath: /dev/target volumeMounts: - name: source mountPath: /source volumes: - name: source persistentVolumeClaim: claimName: radius02 # NFS PVC - name: target persistentVolumeClaim: claimName: radius02-iscsi # iSCSI PVC -
Connect to the pod and copy the data:
oc rsh disk-migrator dd if=/source/disk.img of=/dev/target bs=4M status=progress conv=fsync -
Edit the VM and replace the dataVolume with a persistentVolumeClaim:
spec: template: spec: volumes: - name: rootdisk persistentVolumeClaim: claimName: radius02-iscsi
Backup¶
There are two methods to backup Kubevirt VMs. Either backup the whole namespace (lazy man's way), or label each resource for a particular VM and backup using labelSelectors. The second gives the most flexability so we'll cover it here.
Create the following configmap since you don't want your VM starting as soon as they are restored:
apiVersion: v1
kind: ConfigMap
metadata:
name: velero-restore-modifiers-kubevirt
namespace: openshift-adp
data:
resource-modifiers.yaml: |
version: v1
resourceModifierRules:
- conditions:
groupResource: virtualmachines.kubevirt.io
patches:
- operation: remove
path: "/spec/running"
- operation: add
path: "/spec/runStrategy"
value: "Halted"
The following script will identify resources that require labels for each VM:
#!/usr/bin/env bash
set -euo pipefail
NS="${1:?namespace required}"
VM="${2:?vm name required}"
echo "VM:"
oc -n "$NS" get vm "$VM" -o name
echo
echo "DataVolumes:"
DVS=$(oc -n "$NS" get vm "$VM" -o json | jq -r '
.spec.template.spec.volumes[]
| select(.dataVolume != null)
| .dataVolume.name
' | sort -u)
if [ -n "${DVS:-}" ]; then
while read -r dv; do
[ -z "$dv" ] && continue
oc -n "$NS" get dv "$dv" -o name
done <<< "$DVS"
fi
echo
echo "PVCs:"
PVCS=$(
{
oc -n "$NS" get vm "$VM" -o json | jq -r '
.spec.template.spec.volumes[]
| select(.persistentVolumeClaim != null)
| .persistentVolumeClaim.claimName
'
if [ -n "${DVS:-}" ]; then
while read -r dv; do
[ -z "$dv" ] && continue
oc -n "$NS" get dv "$dv" -o json | jq -r '.status.claimName // empty'
done <<< "$DVS"
fi
} | sort -u
)
if [ -n "${PVCS:-}" ]; then
while read -r pvc; do
[ -z "$pvc" ] && continue
oc -n "$NS" get pvc "$pvc" -o name
done <<< "$PVCS"
fi
echo
echo "PVs:"
if [ -n "${PVCS:-}" ]; then
while read -r pvc; do
[ -z "$pvc" ] && continue
pv=$(oc -n "$NS" get pvc "$pvc" -o jsonpath='{.spec.volumeName}')
[ -n "$pv" ] && echo "persistentvolume/$pv"
done <<< "$PVCS" | sort -u
fi
echo
echo "Cloud-init / secret refs:"
oc -n "$NS" get vm "$VM" -o json | jq -r '
.spec.template.spec.volumes[] |
if .cloudInitNoCloud?.secretRef then
"secret/" + .cloudInitNoCloud.secretRef
elif .cloudInitConfigDrive?.secretRef then
"secret/" + .cloudInitConfigDrive.secretRef
else
empty
end
' | sort -u
The output will look something like:
[randal@ws02 velero]$ ./vm-resource-list.sh virtual-machines radius02
VM:
virtualmachine.kubevirt.io/radius02
DataVolumes:
datavolume.cdi.kubevirt.io/radius02
PVCs:
persistentvolumeclaim/radius02
PVs:
persistentvolume/pvc-6f3bf32d-e18c-4e6e-bd86-54d7695f465b
Cloud-init / secret refs:
[randal@ws02 velero]$
Label the VM itself:
oc -n virtual-machines label vm radius02 backup.vm/name=radius02 --overwrite
Label the storage:
oc -n virtual-machines label dv radius02 backup.vm/name=radius02 --overwrite
oc -n virtual-machines label pvc radius02 backup.vm/name=radius02 --overwrite
oc label pv pvc-6f3bf32d-e18c-4e6e-bd86-54d7695f465b backup.vm/name=radius02 --overwrite
Label any cloud-init / secret refs identified in a similar fashion.
Create the backup CR using a labelSelector to target the VMs resources:
apiVersion: velero.io/v1
kind: Backup
metadata:
name: radius02-adhoc-20260420-0905
namespace: openshift-adp
spec:
hooks: {}
includedNamespaces:
- virtual-machines
labelSelector:
matchLabels:
backup.vm/name: radius02
includedResources: []
excludedResources: []
storageLocation: synologyonprem
ttl: 720h00m00s
snapshotVolumes: false
defaultVolumesToFsBackup: true
Restore¶
Restoring a VM into a running state will cause issues. The following ConfigMap on the restore target will mutate the restore and prevent the restored VM from automatically starting:
apiVersion: v1
kind: ConfigMap
metadata:
name: velero-restore-modifiers-kubevirt
namespace: openshift-adp
data:
resource-modifiers.yaml: |
version: v1
resourceModifierRules:
- conditions:
groupResource: virtualmachines.kubevirt.io
patches:
- operation: remove
path: "/spec/running"
- operation: add
path: "/spec/runStrategy"
value: "Halted"
Then you can restore using:
velero restore create radius02-restore-test \
--from-backup <backup-name> \
--exclude-resources datavolumes.cdi.kubevirt.io \
--resource-modifier-configmap velero-restore-modifiers-kubevirt