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!

