KubeVirt: Running VMs on Kubernetes — and Why That Still Matters

Reading Time: 6 minutes

Kubernetes Ecosystem: From User to Contributor, Episode 10 (Series Finale)
← EP09: Karpenter vs Cluster Autoscaler · EP10: KubeVirt · All Kubernetes Ecosystem Episodes →

11 min read


TL;DR

  • KubeVirt runs full virtual machines as first-class Kubernetes objects — a VirtualMachineInstance wraps a real QEMU/KVM VM inside a special pod, scheduled and managed the same way any other workload is
  • The real reason to run KubeVirt isn’t nostalgia for VMs — it’s giving legacy, non-containerizable, or Windows workloads the same control plane, networking, and operational tooling as everything else, instead of maintaining a separate VM infrastructure stack alongside Kubernetes
  • Multus (multiple network interfaces per pod) and CDI (Containerized Data Importer, for getting disk images into the cluster) are the supporting pieces that make VM networking and storage work inside a Kubernetes-native model
  • Live migration — moving a running VM to another node with no downtime — is KubeVirt’s answer to node maintenance, and it’s meaningfully different from how Kubernetes handles pod disruption
  • Nodes running VM workloads need actual hardware virtualization support (KVM) — the same nested-virtualization requirement that constrained Minikube’s VM drivers back in EP02, now showing up at the production node level
  • Contribution opportunity: GPU and device-plugin support for VM workloads specifically lags behind the maturity of the container device-plugin ecosystem — a real, scoped gap

The Big Picture

CONTAINER POD                                VM VIA KUBEVIRT
──────────────                                ───────────────
Pod                                          VirtualMachineInstance
  └── container                                └── virt-launcher Pod
        runs directly on the                        └── QEMU/KVM process
        node's kernel via                                running a REAL VM,
        containerd/runc                                    with its own kernel
                                                             (Linux, Windows,
Scheduling, networking,                                     anything QEMU
resource limits — standard                                  supports)
Kubernetes primitives
                                              Scheduling, networking, resource
                                              limits — SAME standard Kubernetes
                                              primitives, wrapping a VM instead

What is KubeVirt? It’s the project that makes a virtual machine look like just another workload to Kubernetes — the VM runs inside a virt-launcher pod, so everything Kubernetes already does for scheduling, networking policy, and resource accounting applies to the VM too, without Kubernetes itself needing to understand virtualization at all.


The Core Abstraction: VirtualMachine, VirtualMachineInstance, and virt-launcher

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: legacy-app-vm
spec:
  running: true
  template:
    spec:
      domain:
        cpu: {cores: 2}
        memory: {guest: 4Gi}
        devices:
          disks:
          - name: rootdisk
            disk: {bus: virtio}
      volumes:
      - name: rootdisk
        dataVolume: {name: legacy-app-disk}
$ kubectl get vmi
NAME            AGE   PHASE     IP            NODENAME
legacy-app-vm   2m    Running   10.244.1.15   worker-03

$ kubectl get pods
NAME                                READY   STATUS
virt-launcher-legacy-app-vm-x7k2l   2/2     Running
#     ^^^^^^^^^^^^ — this IS the pod hosting the real QEMU process

VirtualMachine is the persistent declaration (should this VM exist, running or stopped); VirtualMachineInstance is the actual running instance; virt-launcher is the pod — an ordinary Kubernetes pod from the scheduler’s point of view — that contains the QEMU process actually executing the VM. This layering mirrors Deployment/ReplicaSet/Pod deliberately, the same pattern CAPI (EP05) uses for Cluster/Machine.


Why Run VMs on Kubernetes At All

The honest case for KubeVirt isn’t “VMs are better than containers” — it’s consolidation. Most real infrastructure estates have some workloads that genuinely can’t be containerized cleanly: legacy applications tightly coupled to a specific OS version, Windows-only software, or workloads with licensing/compliance requirements demanding a VM-level boundary. Without KubeVirt, those workloads need an entirely separate VM infrastructure stack — its own hypervisor management, its own networking, its own monitoring — running alongside Kubernetes rather than on it. KubeVirt collapses that into one control plane, one set of operational tooling, one team’s expertise, for both categories of workload.


Networking and Storage for VMs: Multus, DataVolumes, CDI

# Multus lets a VM's pod attach to more than one network —
# often needed for VMs expecting a dedicated network interface,
# unlike a typical single-network container workload
$ kubectl get network-attachment-definitions

# CDI (Containerized Data Importer) gets an existing VM disk image
# into the cluster as a DataVolume
$ kubectl apply -f - <<EOF
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: legacy-app-disk
spec:
  source:
    http:
      url: "https://images.example.com/legacy-app.qcow2"
  pvc:
    accessModes: ["ReadWriteOnce"]
    resources:
      requests: {storage: 20Gi}
EOF

Regular container images assume a filesystem layer model that doesn’t map cleanly onto “import this existing 20GB qcow2 disk image a legacy app already runs from.” CDI exists specifically to bridge that gap — pulling real VM disk images from HTTP, S3, or a registry into a DataVolume a VirtualMachine can boot from.


Live Migration: KubeVirt’s Answer to Node Maintenance

$ virtctl migrate legacy-app-vm
VM legacy-app-vm was scheduled to migrate

$ kubectl get vmim
NAME                    PHASE       VMI
legacy-app-vm-migrate   Running     legacy-app-vm

# The VM keeps running, on a different node, with no restart —
# genuinely different from how Kubernetes handles pod disruption

A pod being evicted for node maintenance simply gets rescheduled — it restarts fresh somewhere else. A live-migrated VM keeps running throughout, its in-memory state transferred to the new node without a restart, the same live-migration capability traditional hypervisor platforms have offered for years. This is one behavior category where “VM on Kubernetes” is genuinely doing something a plain pod cannot.


⚠ Production Gotchas

Nodes need real KVM support — nested virtualization if the node itself is already a VM (common on cloud instances). This is the same underlying requirement that constrained Minikube’s VM drivers back in EP02, now at the production node level: if your cloud instance type doesn’t expose nested virtualization, KubeVirt workloads simply won’t schedule there.

VM disk I/O performance depends heavily on the underlying storage class and driver (virtio vs alternatives) — don’t assume container-workload storage benchmarks transfer to VM workloads on the same cluster.

Live migration isn’t guaranteed to succeed for every VM under every condition — a VM under heavy memory-write load, or a migration across a congested network path, can fail or take far longer than expected. Test migration behavior under realistic load before depending on it for a real maintenance window.

Windows VMs specifically need the right virtio drivers pre-installed in the image, or networking/storage won’t work post-boot — this is a common first-VM stumbling block distinct from anything Linux-VM users hit.


Quick Reference

kubectl get vm                       # VirtualMachine declarations
kubectl get vmi                      # running VirtualMachineInstances
kubectl get vmim                     # migration status
virtctl start / stop / restart <vm>  # lifecycle control
virtctl console <vm>                 # serial console access
virtctl migrate <vm>                 # trigger a live migration
kubectl get datavolumes              # CDI-imported disk images

Contribution Opportunity: Device-Plugin Parity for VM Workloads

The limitation: Kubernetes’ device-plugin ecosystem for GPUs and other specialized hardware is mature and well-supported for container workloads. Extending that same hardware access cleanly into KubeVirt-managed VMs — GPU passthrough with the same operational ergonomics containers already have — is real, current work with meaningfully more rough edges than the container-side equivalent.

Why it’s hard to fix: Passing a physical device through to a VM (as opposed to a container, which can often share the host’s device more directly) involves IOMMU configuration, PCI passthrough mechanics, and driver considerations inside the guest OS that simply don’t exist for container device access — it’s a genuinely harder problem, not a lagging implementation of an equally-easy one.

What a contribution-shaped fix looks like: KubeVirt’s own documentation and issue tracker identify specific, scoped GPU/device-passthrough gaps — particular hardware combinations without a documented working configuration, or specific device-plugin integrations that work for containers but haven’t been extended to the VM path. Reproducing one of these documented gaps on real hardware you have access to, and contributing either the fix or a verified, tested configuration guide back to the project, is exactly the kind of practitioner-shaped contribution this series has pointed toward in every episode — you need access to the hardware and patience to reproduce, not novel research.


Key Takeaways

  • KubeVirt makes a VM look like an ordinary Kubernetes workload by wrapping it in a virt-launcher pod — the same scheduling, networking, and resource primitives apply to both
  • The real case for running VMs on Kubernetes is infrastructure consolidation — one control plane for both container and VM workloads, not VM enthusiasm for its own sake
  • Multus and CDI solve the networking and storage problems that don’t map cleanly from the container model onto VM disk images and multi-interface networking
  • Live migration is a genuine capability gap between VMs and plain pods — Kubernetes’ own disruption model for pods has no equivalent
  • The clearest current contribution opportunity is closing specific, documented device-passthrough gaps for VM workloads — hardware-access work, not abstract feature design

Series Wrap: From User to Contributor

Ten episodes, ten tools, and one repeated pattern: every project in this series exists because something else fell short — k3s and MicroK8s because upstream Kubernetes was too heavy for the edge, Karpenter because Cluster Autoscaler’s node-group abstraction had a ceiling, Crossplane because Terraform’s model didn’t fit a fully Kubernetes-native platform team. Each one of those gaps was found by someone who was using the previous tool closely enough to feel exactly where it stopped working.

That’s the actual path from user to contributor this series has been pointing at in every Contribution Opportunity section: not a certification, not a novel research breakthrough — close, careful attention to a tool you already run, precise enough to name what’s actually missing and specific enough to describe what fixing it would look like. Every gap named across these ten episodes is real, documented, and waiting for exactly that kind of attention.

Get future series and episodes in your inbox → linuxcent.com/subscribe