In a traditional virtualization world, provisioning a virtual machine often meant downloading a 15GB .iso file, mounting it through an administrative console, clicking through an OS installer wizard, and running post-install scripts.
In a cloud-native platform like OpenShift Virtualization, that manual approach is unacceptable.
We need VMs to spin up in seconds—just like containers—using pre-baked, customized Golden Images (Boot Sources) that update automatically on a schedule.
In this third part of our series, we will walk through the end-to-end production workflow of building custom golden OS images, pushing them to the OpenShift Internal Registry, and automating recurring imports with CDI (Containerized Data Importer) and DataImportCron.
🏗️ Architecture: The Automated Boot Source Pipeline
The automated pipeline consists of three core stages:
- Packaging: Wrapping a raw/qcow2 virtual disk into an ultra-lightweight containerDisk.
- Distribution: Storing the image inside the secured OpenShift Internal Registry.
- Import & Consumption: CDI continuously pulls updates, populates a DataVolume, and registers a ready-to-use DataSource for instant VM cloning.
🛠️ Stage 1: Build the containerDisk from Scratch
A containerDisk is simply an OCI container image containing a disk image located at /disk/. To keep it minimal, secure, and disconnected-friendly, we build it directly FROM scratch without pulling heavy base images like UBI.
1. Prepare File Permissions & Containerfile
Before building, ensure the host qcow2 file has read-only permissions (0440), and assign ownership to UID 107:107 (the internal QEMU user inside KubeVirt pods):
chmod 0440 custom-disk.qcow2
cat << 'EOF' > Containerfile
FROM scratch
ADD --chown=107:107 custom-disk.qcow2 /disk/
EOF
🚀 Stage 2: Push to the OpenShift Internal Registry
To push images without relying on external registries, we leverage OpenShift's built-in registry.
1. Expose the Internal Registry Route
oc patch configs.imageregistry.operator.openshift.io/cluster \
--type=merge -p '{"spec":{"defaultRoute":true}}'
export REGISTRY_ROUTE=$(oc get route default-route -n openshift-image-registry --template='{{ .spec.host }}')
echo "Internal Registry: $REGISTRY_ROUTE"
2. Login and Push
podman login --tls-verify=false -u kubeadmin -p $(oc whoami -t) $REGISTRY_ROUTE
podman build -t $REGISTRY_ROUTE/openshift-virtualization-os-images/custom-os-disk:latest .
podman push --tls-verify=false $REGISTRY_ROUTE/openshift-virtualization-os-images/custom-os-disk:latest
🔐 Stage 3: Configure Authentication & TLS Certificates
When the CDI importer pod pulls the image from the internal service address (image-registry.openshift-image-registry.svc:5000), it requires credentials and cluster trust:
1. Create the Pull Secret
oc create secret generic kubeadmin-reg-secret \
--from-literal=accessKeyId=kubeadmin \
--from-literal=secretKey=$(oc whoami -t) \
-n openshift-virtualization-os-images
2. Extract Cluster CA for TLS Verification
To prevent x509 certificate errors:
oc extract configmap/openshift-service-ca.crt \
-n openshift-virtualization-os-images --to=- > service-ca.crt
oc create configmap internal-registry-ca \
--from-file=ca.crt=service-ca.crt \
-n openshift-virtualization-os-images
⏰ Stage 4: Deploy the Automated DataImportCron
Now we wire CDI to monitor the registry and automatically update a DataSource.
Notice the crucial annotation cdi.kubevirt.io/storage.bind.immediate.requested: "true", which forces storage allocation immediately rather than getting stuck in WaitForFirstConsumer:
cat << 'EOF' | oc apply -f -
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataImportCron
metadata:
name: custom-os-image-cron
namespace: openshift-virtualization-os-images
annotations:
cdi.kubevirt.io/storage.bind.immediate.requested: "true"
spec:
garbageCollect: Outdated
importsToKeep: 2
managedDataSource: custom-os-datasource
retentionPolicy: None
schedule: "*/15 * * * *"
template:
spec:
source:
registry:
pullMethod: pod
url: "docker://image-registry.openshift-image-registry.svc:5000/openshift-virtualization-os-images/custom-os-disk:latest"
secretRef: kubeadmin-reg-secret
certConfigMap: internal-registry-ca
storage:
resources:
requests:
storage: 15Gi
EOF
Verify Importer Status
oc get dataimportcron,dv,pvc,datasource -n openshift-virtualization-os-images
Wait until the DataSource reports READY: True.
🚀 Stage 5: Spin Up a VM in 5 Seconds!
With our managed DataSource active, developers can spin up a production VM with a single CLI command using fast CSI cloning:
virtctl create vm --name production-rhel-vm \
--volume-datasource src:openshift-virtualization-os-images/custom-os-datasource \
| oc apply -f -
⚠️ Battle-Tested Gotchas & Troubleshooting Guide
Here are the top 5 errors engineers face when working with custom boot sources and CDI:
| Common Error | Root Cause | Proven Fix |
|---|---|---|
| error: token is not available | Relying on stale KUBECONFIG without active login. | Always run oc login -u kubeadmin before generating tokens. |
| Permission denied on VM boot | Disk file inside container not owned by user 107. | Set chmod 0440 and ADD --chown=107:107 in Containerfile. |
| x509: certificate signed by unknown authority | Importer pod does not trust internal service CA. | Extract openshift-service-ca.crt and bind via certConfigMap. |
| pullMethod: node ignores secretRef | Node pull method uses kubelet service accounts, not user secrets. | Use pullMethod: pod when pulling with custom secrets. |
| Stuck in WaitForFirstConsumer | StorageClass waits for VM scheduling before binding PVC. | Add annotation cdi.kubevirt.io/storage.bind.immediate.requested: "true". |
🎯 Key Takeaways
- Use FROM scratch and assign ownership to
107:107for lightweight, securecontainerDiskimages. - Automate lifecycle updates with DataImportCron so developers always provision from the latest patched OS baseline.
- Don't forget immediate binding annotations to eliminate PVC provisioning delays.
⏭️ Next Up: Advanced VM Networking & KubeMacPool
Now that our VMs can boot in seconds, how do we connect them to enterprise networks, VLANs, and external subnets?
In Part 4, we dive deep into:
- Pod Networking (Masquerade) vs. Bridged Secondary Networks with Multus CNI.
- Managing MAC address allocation and avoiding duplicate MAC clashes with KubeMacPool.
💬 How do you currently build and distribute OS golden images in your infrastructure? Share your workflow in the comments below!
Top comments (0)