KubeVirt 1.0 in der Praxis: VMs und Container vereint
Von der Evaluation zur Produktion
Im Sommer 2022 haben wir in unserem KubeVirt-Einführungsartikel beschrieben, wie KubeVirt virtuelle Maschinen als native Kubernetes-Objekte behandelt. Damals war das Projekt im Pre-1.0-Stadium, und wir schlossen mit der Frage, ob die Einschränkungen für produktive Szenarien tragbar seien. Anderthalb Jahre später können wir diese Frage beantworten: Ja -- mit Einschränkungen, die man kennen sollte.
Im Juli 2023 hat KubeVirt die Version 1.0 erreicht. Das war mehr als eine Versionsnummer: Es signalisierte API-Stabilität, definierte Deprecation-Zyklen und gab Teams wie uns die Sicherheit, dass sich bestehende VM-Definitionen nicht bei jedem Minor-Upgrade ändern. Mittlerweile sind wir bei KubeVirt 1.2, das Projekt ist weiterhin CNCF Incubating, und die Community wächst stetig. Zeit für einen ehrlichen Praxisbericht.
Unsere Use Cases
Wir betreiben KubeVirt nicht aus technischer Neugier, sondern weil es konkrete Probleme löst. Drei Szenarien haben sich bei uns etabliert.
Legacy-Anwendungen. In Kundenprojekten treffen wir regelmäßig auf Software, die sich nicht containerisieren lässt -- proprietäre Dienste mit spezifischen Kernel-Anforderungen, Anwendungen mit fest verdrahteten Dateipfaden oder schlicht Software, deren Hersteller ausschließlich VM-Images liefert. Statt dafür eine separate Virtualisierungsplattform zu betreiben, laufen diese Workloads jetzt als VirtualMachine-Objekte auf demselben Cluster wie unsere Container.
Datenbanken mit Performance-Anforderungen. Bestimmte Datenbank-Workloads brauchen vorhersagbare I/O-Latenz und direkten Zugriff auf dedizierte Ressourcen. In einer VM mit zugewiesenen CPU-Cores und Hugepages können wir das besser garantieren als in einem Container, der sich Ressourcen mit anderen Pods teilt.
Windows-Workloads. Ja, es gibt sie noch -- Windows-Dienste, für die es keine Linux-Alternative gibt. Mit KubeVirt können wir Windows-VMs auf unseren Linux-Nodes betreiben, inklusive VNC-Zugriff über virtctl und automatisierter Provisionierung über cloud-init-kompatible Windows-Images.
Infrastruktur: K3s auf NixOS
Wie in unseren Artikeln zu K3s im Produktiveinsatz und NixOS als Server-Betriebssystem beschrieben, betreiben wir unsere Cluster auf K3s-Nodes mit NixOS als Host-OS. Für KubeVirt mussten wir die NixOS-Konfiguration der Nodes erweitern -- insbesondere die KVM-Unterstützung und die Device-Permissions:
# KubeVirt-relevante NixOS-Konfiguration
virtualisation.libvirtd.enable = true;
boot.kernelModules = [ "kvm-intel" "vhost_net" ];
boot.kernel.sysctl = {
"net.bridge.bridge-nf-call-iptables" = 1;
"net.ipv4.ip_forward" = 1;
};
Entscheidend ist, dass /dev/kvm auf allen Nodes verfügbar und für die Container-Runtime zugänglich ist. Auf NixOS ist das über die deklarative Konfiguration sauber lösbar. Die Reproduzierbarkeit von NixOS spielt hier ihre Stärke aus: Wenn wir einen neuen Node zum Cluster hinzufügen, ist die KubeVirt-Fähigkeit automatisch dabei.
Die Installation von KubeVirt selbst hat sich seit der 1.0 vereinfacht. Wir nutzen den Operator in der aktuellen Version:
# KubeVirt 1.2 Operator installieren
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/v1.2.0/kubevirt-operator.yaml
# KubeVirt CR deployen
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/v1.2.0/kubevirt-cr.yaml
# CDI fuer Image-Import installieren
kubectl apply -f https://github.com/cdi-project/containerized-data-importer/releases/download/v1.58.0/cdi-operator.yaml
kubectl apply -f https://github.com/cdi-project/containerized-data-importer/releases/download/v1.58.0/cdi-cr.yaml
VMs mit Cloud-Init und persistentem Storage
Im Einführungsartikel haben wir eine einfache VM mit containerDisk gezeigt. In der Produktion arbeiten wir mit persistenten Volumes und umfangreicher Cloud-Init-Konfiguration. Hier ein Beispiel, das unserem typischen Setup entspricht:
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: legacy-app-01
namespace: vms
labels:
app: legacy-app
env: production
spec:
running: true
template:
metadata:
labels:
kubevirt.io/vm: legacy-app-01
app: legacy-app
spec:
domain:
cpu:
cores: 2
model: host-passthrough
memory:
guest: "4Gi"
hugepages:
pageSize: "2Mi"
resources:
requests:
memory: "4Gi"
limits:
memory: "4Gi"
devices:
disks:
- name: rootdisk
disk:
bus: virtio
bootOrder: 1
- name: datadisk
disk:
bus: virtio
- name: cloudinitdisk
disk:
bus: virtio
interfaces:
- name: default
masquerade: {}
networks:
- name: default
pod: {}
volumes:
- name: rootdisk
dataVolume:
name: legacy-app-01-root
- name: datadisk
persistentVolumeClaim:
claimName: legacy-app-01-data
- name: cloudinitdisk
cloudInitNoCloud:
userData: |
#cloud-config
hostname: legacy-app-01
manage_etc_hosts: true
users:
- name: ops
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 AAAA...
packages:
- qemu-guest-agent
- prometheus-node-exporter
runcmd:
- systemctl enable --now qemu-guest-agent
- systemctl enable --now prometheus-node-exporter
dataVolumeTemplates:
- metadata:
name: legacy-app-01-root
spec:
source:
http:
url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
pvc:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: longhorn
Einige Details sind hier erwähnenswert. Die dataVolumeTemplates-Sektion nutzt CDI, um das Ubuntu-Cloud-Image automatisch herunterzuladen und in ein PVC zu schreiben -- beim ersten Erstellen der VM. Bei nachfolgenden Starts wird das bestehende Volume wiederverwendet. Das host-passthrough CPU-Modell gibt der VM Zugriff auf die tatsächlichen CPU-Features des Hosts, was für Performance kritisch ist. Und der qemu-guest-agent in der Cloud-Init-Konfiguration ermöglicht es KubeVirt, den Zustand der VM genauer zu überwachen.
virtctl im Alltag
virtctl ist unser tägliches Werkzeug für die VM-Verwaltung. Die wichtigsten Befehle, die wir regelmäßig nutzen:
# VM-Lebenszyklus
virtctl start legacy-app-01 -n vms
virtctl stop legacy-app-01 -n vms
virtctl restart legacy-app-01 -n vms
# SSH-Zugriff ueber den Kubernetes-API-Server (kein NodePort noetig)
virtctl ssh ops@legacy-app-01 -n vms
# Konsole fuer Debugging ohne Netzwerk
virtctl console legacy-app-01 -n vms
# Live Migration manuell ausloesen
virtctl migrate legacy-app-01 -n vms
# VM-Snapshot erstellen (seit KubeVirt 1.0 stabil)
virtctl snapshot create legacy-app-01 --name=pre-upgrade-snapshot -n vms
# Port-Forwarding fuer lokalen Zugriff
virtctl port-forward legacy-app-01 8080:80 -n vms
Besonders virtctl ssh hat unseren Workflow verändert. Statt SSH über NodePorts oder LoadBalancer zu exponieren, tunnelt virtctl die Verbindung über die Kubernetes-API. Das vereinfacht die Netzwerkkonfiguration erheblich und reduziert die Angriffsfläche. Die Snapshot-Funktionalität, die mit 1.0 stabil geworden ist, nutzen wir konsequent vor VM-Upgrades -- ein Sicherheitsnetz, das bei klassischen Containern so nicht existiert.
Live Migration: Ernüchterung und Erfolg
Live Migration war eines der Features, das uns im Einführungsartikel am meisten beeindruckt hat. In der Praxis haben wir sowohl Erfolge als auch Grenzen erlebt.
Was zuverlässig funktioniert: VMs mit moderatem Speicherverbrauch (bis ca. 8 GB) migrieren in wenigen Sekunden zwischen Nodes. Das ist entscheidend für Node-Wartung -- ein kubectl drain auf einem Node löst automatisch die Migration der VMs aus, und der Downtime-Fenster bleibt im Millisekundenbereich.
Wo es komplizierter wird: VMs mit großen Speicher-Footprints und hoher Schreibrate. Eine Datenbank-VM mit 16 GB RAM und aktiven Schreibvorgängen braucht deutlich länger, weil KubeVirt den Speicher inkrementell kopiert, während die VM weiterschreibt. In einem Fall mussten wir die VM kurz pausieren, um die Migration abzuschließen. Für solche Workloads planen wir Wartungsfenster bewusst ein.
Voraussetzung für Live Migration bleibt geteilter Storage. Auf unseren Nodes nutzen wir Longhorn, was mit ReadWriteMany-Volumes die notwendige Flexibilität bietet.
Networking: Multus für sekundäre Netzwerke
Das Standard-Networking über Pod-Netzwerke und masquerade-Interfaces reicht für viele Szenarien. Aber in Kundenprojekten brauchen VMs häufig Zugriff auf VLANs oder dedizierte Netzwerksegmente -- etwa für Legacy-Anwendungen, die per IP-Adresse aus einem bestimmten Subnetz erreichbar sein müssen.
Dafür setzen wir Multus CNI ein, das mehrere Netzwerk-Interfaces pro Pod (und damit pro VM) ermöglicht. Eine VM kann so gleichzeitig am Pod-Netzwerk hängen und ein zweites Interface in ein VLAN erhalten:
spec:
template:
spec:
domain:
devices:
interfaces:
- name: default
masquerade: {}
- name: vlan100
bridge: {}
networks:
- name: default
pod: {}
- name: vlan100
multus:
networkName: vlan100-net-attach
Die zugehörige NetworkAttachmentDefinition konfiguriert das VLAN-Interface auf den Nodes. Das erfordert Vorarbeit auf der NixOS-Seite -- die physischen Interfaces müssen für Multus verfügbar sein --, aber einmal eingerichtet funktioniert es zuverlässig.
Monitoring mit Prometheus
KubeVirt exponiert umfangreiche Metriken über einen Prometheus-kompatiblen Endpoint. Damit integriert sich das VM-Monitoring nahtlos in unseren bestehenden Prometheus-Stack. Die wichtigsten Metriken, die wir überwachen:
kubevirt_vmi_memory_resident_bytes-- tatsächlicher Speicherverbrauch der VMkubevirt_vmi_vcpu_seconds_total-- CPU-Nutzung pro vCPUkubevirt_vmi_storage_read_traffic_bytes_total-- Disk-I/Okubevirt_vmi_network_receive_bytes_total-- Netzwerk-Traffickubevirt_vmi_migration_data_processed_bytes-- Fortschritt bei Live Migrations
Zusätzlich installieren wir per Cloud-Init den prometheus-node-exporter in jede VM. Damit erhalten wir dieselben Host-Level-Metriken, die wir von unseren physischen Nodes kennen -- Dateisystem-Auslastung, Systemd-Service-Status, detaillierte Netzwerkstatistiken. Die Kombination aus KubeVirt-Metriken (Hypervisor-Ebene) und Node-Exporter-Metriken (Guest-Ebene) gibt uns ein vollständiges Bild.
Unsere Grafana-Dashboards zeigen VMs und Container nebeneinander. Ein Operator sieht auf einen Blick, ob eine VM lebt, wie ihre Ressourcen ausgelastet sind und ob eine Migration läuft. Das war eines unserer Ziele: eine einheitliche Betriebssicht auf beide Welten.
Was sich seit 1.0 verbessert hat
Die Bedenken, die wir im Einführungsartikel geäußert haben, können wir größtenteils entkräften. Die Dokumentation hat sich massiv verbessert -- das KubeVirt-User-Guide deckt mittlerweile auch komplexere Szenarien ab. Die API-Stabilität seit 1.0 gibt uns Planungssicherheit für Upgrades. Und die Snapshot- und Restore-Funktionalität, die damals noch experimentell war, nutzen wir jetzt regelmäßig in der Produktion.
Was weiterhin fehlt, ist ein ausgereiftes GUI. Es gibt Ansätze -- das KubeVirt-Plugin für die Rancher-UI und die OpenShift-Konsole --, aber ein eigenständiger, leichtgewichtiger VM-Manager vergleichbar mit Proxmox oder vCenter existiert nicht. Für uns als CLI-affines Team ist das kein Problem. Für Teams, die eine grafische Verwaltung erwarten, bleibt das eine Hürde.
Fazit
KubeVirt hat den Sprung von der vielversprechenden Technologie zur produktionstauglichen Lösung geschafft. Die 1.0 war der Wendepunkt -- nicht weil sich die Technik fundamental geändert hat, sondern weil sie das Signal für API-Stabilität und langfristige Unterstützung war. Auf unseren K3s-Clustern mit NixOS betreiben wir mittlerweile ein gutes Dutzend VMs neben unseren Container-Workloads, und die beiden Welten fühlen sich nicht mehr wie getrennte Systeme an.
Der größte Gewinn ist operativ: ein Team, eine Plattform, ein Monitoring-Stack, ein Deployment-Workflow. Wer heute Kubernetes betreibt und VM-Workloads hat -- sei es Legacy-Software, Windows-Dienste oder Performance-sensitive Datenbanken --, sollte KubeVirt ernsthaft evaluieren. Die Zeit des "interessant, aber noch nicht reif" ist vorbei.
Geschrieben von
Patrick HütterGründer & Software-Architekt
Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.
Das könnte dich auch interessieren
CI-Runner in der microVM: Docker-Builds mit Kata Containers auf Kubernetes
31.08.2026 · 10 Min. Lesezeit
GitOps mit Helmfile und Kyverno: Unser Deployment-Workflow
10.03.2025 · 7 Min. Lesezeit
K3s und KubeVirt: Konvergente Infrastruktur auf Bare Metal
20.01.2025 · 6 Min. Lesezeit