KubeVirt 1.0 in der Praxis: VMs und Container vereint
Zurück zum Blog

KubeVirt 1.0 in der Praxis: VMs und Container vereint

6 Min. Lesezeit
Lesen auf English

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 VM
  • kubevirt_vmi_vcpu_seconds_total -- CPU-Nutzung pro vCPU
  • kubevirt_vmi_storage_read_traffic_bytes_total -- Disk-I/O
  • kubevirt_vmi_network_receive_bytes_total -- Netzwerk-Traffic
  • kubevirt_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.

Teilen

Patrick Hütter

Geschrieben von

Patrick Hütter

Grü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.