OpenShift Virtualization: Implementierung und Orientierung

Dies ist für mich eine Leitlinie und Wissenssammlung, welche ich bei der Implementierung von OpenShift Virtualization gesammelt habe. Es ist kein offizielles Dokument von Red Hat, sondern eine persönliche Zusammenfassung meiner Erfahrungen und Erkenntnisse. Alle Angaben ohne Gewähr.

Quellen

Allgemeine Umgebungsanforderungen und Architektur

Hardware

Hier allgemeine Anforderung an die Hardware, ohne die genauen Spezifikationen zu nennen.

CPU

Die Hardware sollte bei allen Severn genau gleich. In OpenShift Virtualization bestimmt das CPU-Modell, welche Funktionen des physischen Host-Prozessors an die virtuelle Maschine (VM) weitergegeben werden. Die Wahl des Modells ist ein Balanceakt zwischen Performance und der Fähigkeit zur Live-Migration. Sollte ein Node A ein CPU-Modell haben, das Funktionen enthält, die in Node B nicht vorhanden sind, könnte dies die Live-Migration von VMs zwischen diesen Nodes verhindern.

Natürlich kann es sein, dass später neue, zusätliche Nodes beschaftt werden, welche ein neueres CPU-Modell haben. Hier muss dann abgeklärt werden, ob die neuen Nodes mit den alten Nodes kompatibel sind oder wie dieses anderweitig mitigiert werden kann. Zum Beispiel durch die Verwendung von CPU-Feature-Gates oder durch die Planung der VMs auf die entsprechenden Nodes.

Unterstütuzung von Hardware-Virtualisierungsfunktionen wie Intel VT-x oder AMD-V ist ein Muss.

Das Hostmodel mit dem kleinsten gemeinsamen Satz an Funktionen sollte gewählt werden, um die beste Kompatibilität und Live-Migration zu gewährleisten.

So kann es eruiert werden (Node Feature Discovery Operator installiert):

oc get nodes -l feature.node.kubernetes.io/cpu-model.vendor_id=Intel --show-labels | tr ',' '\n' | grep host-model-cpu

RAM

Beim RAM sollte nach Möglichkeit auf jedem Node die gleiche Menge an RAM installiert sein. Dies erleichtert die Planung und Verteilung der VMs, da jede Node die gleiche Kapazität hat. Es ermöglicht auch eine bessere Ausnutzung der Ressourcen und reduziert die Komplexität bei der Verwaltung der Nodes.

NIC

Die Anzahl NICs und die “Platzierung” der NICs in den gleichen Slots auf allen Nodes sollte ebenfalls identisch sein. Dies erleichtert eine einheitliche Konfiguration und Verwaltung der Netzwerkverbindungen. Zum Beispiel bei NetworkAttamentDefinitions ist es praktisch, wenn man alle Nodes mit der gleichen Definition konfigurieren kann.

Storage

Unbedingt schauen ob es einen entsprechenden CSI Treiber für den Storage gibt um PVCs dynamisch zu provisionieren. Bei Regional DR Setups ist es wichtig, dass der Storage Hersteller einen CSI Treiber hat, welcher die Replikation der Metadaten der PVC/PVs zwischen den Standorten unterstützt.

Hier eine Liste von Storage Herstellern, welche einen CSI Treiber für OpenShift Virtualization haben und deren Unterstützung für die Replikation der Metadaten der PVC/PVs zwischen den Standorten:

VendorToolMetadata Storage SpeicherortPrimärer Failover Mechanismus
NetAppTrident ProtectExternal S3 Bucket“Restore” job from S3 to K8s API.
DellCSM ReplicationK8s CRDs (Internal)repctl CLI / Controller sync.
PurePortworxDistributed KV StoreCluster-to-cluster sync.
HuaweiCDR Plugin / BCManagerProprietary / BCManager“Recovery Plan” in BCManager.

Support von Snapshot und Cloning des CSI Drivers sind ein muss.

Hier die Liste mit fully supported Storage Provider für OpenShift Virtualization

Network

  • VM und Applikations Traffik getrennt vom restlichen Traffik
  • VLAN Unterstützung für Traffik Isolation
  • Live Migration erfordert RWX
  • Die gängigen Dienste wie DNS, DHCP, NTP, IPAM etc. sind verfügbar

DNS

HostnameTypZielZweck
api.<cluster>.<domain>A/AAAA,APILoad BalancerExterner Zugriff auf die API (Port 6443).
api-int.<cluster>.<domain>A/AAAA,APILoad BalancerInterne Kommunikation (Nodes zu API & Port 22623).
*.apps.<cluster>.<domain>A/AAAA,IngressLoad BalancerWildcard für alle App-Routen (Ports 80/443).
master<0-2>.<cluster>.<domain>A/AAAAIP der Master-NodesPermanent (Control Plane).
worker<N>.<cluster>.<domain>A/AAAAIP der Worker-NodesPermanent (Compute Ebene).

Natürlich für für jeden Eintrag den PTR Record nicht vergessen.

Und schliesslich auch die entsprechenden Einträge für die VMs.

Hochverfügbarkeit

Die Nodes sollten in einem Hochverfügbarkeits-Cluster konfiguriert sein, um sicherzustellen, dass die VMs auch bei einem Node-Ausfall weiterlaufen können, resp. das die restlichen Nodes die Last tragen können.

Identity und Access Integration

RBAC sollte mit dem Firmeneigenen Identity Provider (z.B. LDAP, Active Directory) integriert werden, um eine zentrale Verwaltung der Benutzer und Berechtigungen zu ermöglichen (RBAC).

Hardware Konfiguration

Wir gehen von einer Umgebung mit 500 virtuellen Maschinen aus. Dazu nehmen wir folgendes an:

  • 500 VMs mit durchschnittlich 4 vCPU und 16 GB RAM
  • CPU overcommit: 4:1 - wir nehmen an das die eigentliche VM Auslastung bei 25% liegt, also 1 vCPU pro VM
  • RAM overcommit: 2:1 - wir nehmen an das die eigentliche RAM Auslastung bei 50% liegt, also 8 GB RAM pro VM

Compute Anforderungen

  • Homogene Hardware Konfiguration (gleiche CPU, RAM, NICs) auf allen Nodes.
  • Hyperthreading aktiviert
  • Controle planes sind schedulable
  • Keine dezidierten Infrastructure Nodes.
  • Failover Kapazität: TBC

Cluster und Compute Sizing

Eckdaten:

500 VMs

TypevCPURAM (GB)Anzahl VMsTotal vCPUTotal RAM (GB)
Small28100200800
Medium4162008003200
Large83215012004800
XLarge1664508003200
Total500300012000

Memory und CPU

Um die Memory Anforderungen zu berechnen, müssen wir den Memory Overhead pro VM berücksichtigen.

Hier gehen wir nach der Dokumentation von Red Hat OpenShift Virtualization

Memory overhead pro virtual machine ≈ (0.002 × requested memory) \
              + 218 MiB \
              + 8 MiB × (number of vCPUs) \
              + 16 MiB × (number of graphics devices) \
              + (additional memory overhead)
CountvCPUGiBMiBTotal GiBOverhead per VM MiBTotal Overhead GiB
100288192819200266.38426
200416163843276800298.76858
150832327684915200363.53653
501664655363276800493.07224
Total50012288000161

Um eine ungefähre Ahnung zu haben wie wir die zukünftige Kapazität planen können, berechnen wir die Durchschnittliche Grösse der VMs:

  • Durchschnittliche vCPU pro VM = 3000 / 500 = 6 vCPU
  • Durchschnittliche RAM pro VM = 12000 / 500 = 24 GiB

Overcommitment

Wie bereits erwähnt, planen wir mit einem CPU Overcommitment von 4:1 und einem RAM Overcommitment von 2:1. Das bedeutet, dass wir die Anzahl der vCPUs und die Menge des RAMs, die wir tatsächlich bereitstellen müssen, reduzieren können. Es gilt anzumerken, dass das Bestimmen der Overcommit Werten nicht primär eine technische Entscheidung ist, sondern vom Business gemacht werden muss. Das ganze ist eine Abwägung zwischen Kosten und Performance. Im Fehlerfall kann es zu Performance Einbussen kommen, Host Memory Pressure, Swapping oder sogar OOM Kills (shutdown) kommen.

Wenn wir das darauf anwenden, erhalten wir:

TypeRawOvercommit RatioEffective
CPU3000 vCPU4:1750 Cores
RAM12000 GiB2:16000 GiB

Für die CPU sethzen wir vmiCPUAllocationRatio. Default ist 10:1.

oc edit hyperconverged kubevirt-hyperconverged -n openshift-cnv
spec:
  resourceRequirements:
    vmiCPUAllocationRatio: 4
# ...

Um Memory Overcommitment zu nutzen muss Swap auf jedem System aktiviert sein. Um das zu aktivieren müssen wir das Kubelet mittels einer MachineConfig anpassen. Falls es bereits eine KubeletConfig gibt, muss diese entsprechend angepasst werden.

Seit der Version 4.21+ müssen wir zuerst die MachineConfig von Worker und Master Nodes anpassen, damit die KubeletConfig angepasst werden kann. Swap space is nur auf worker Nodes.

99-worker-swap.yaml

apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
metadata:
  labels:
    machineconfiguration.openshift.io/role: worker
  name: 99-worker-swap
spec:
  config:
    ignition:
      version: 3.5.0
    storage:
      files:
      - contents:
          source: data:text/plain;charset=utf-8;base64,YXBpVmVyc2lvbjoga3ViZWxldC5jb25maWcuazhzLmlvL3YxYmV0YTEKa2luZDogS3ViZWxldENvbmZpZ3VyYXRpb24KZmFpbFN3YXBPbjogZmFsc2UK
        mode: 420
        overwrite: true
        path: /etc/openshift/kubelet.conf.d/90-swap.conf
    systemd:
      units:
        - contents: |
            [Unit]
            Description=Enable swap
            ConditionFirstBoot=no
            ConditionPathExists=/var/tmp/swapfile

            [Service]
            Type=oneshot
            ExecStart=/bin/sh -c "sudo swapon /var/tmp/swapfile"

            [Install]
            RequiredBy=kubelet-dependencies.target
          enabled: true
          name: swap-enable.service
        - contents: |
            [Unit]
            Description=Provision and enable swap
            ConditionFirstBoot=no
            ConditionPathExists=!/var/tmp/swapfile

            [Service]
            Type=oneshot
            Environment=SWAP_SIZE_MB=5000
            ExecStart=/bin/sh -c "sudo fallocate -l ${SWAP_SIZE_MB}M /var/tmp/swapfile && \
            sudo chmod 600 /var/tmp/swapfile && \
            sudo mkswap /var/tmp/swapfile && \
            sudo swapon /var/tmp/swapfile && \
            free -h"

            [Install]
            RequiredBy=kubelet-dependencies.target
          enabled: true
          name: swap-provision.service
        - contents: |
            [Unit]
            Description=Restrict swap for system slice
            ConditionFirstBoot=no

            [Service]
            Type=oneshot
            ExecStart=/bin/sh -c "sudo systemctl set-property --runtime system.slice MemorySwapMax=0 IODeviceLatencyTargetSec=\"/ 50ms\""

            [Install]
            RequiredBy=kubelet-dependencies.target
          enabled: true
          name: cgroup-system-slice-config.service
oc get kubeletconfig 
apiVersion: machineconfiguration.openshift.io/v1
kind: KubeletConfig
metadata:
  name: virt-kubelet-config
spec:
  kubeletConfig:
    kubeReserved:
      memory: 128Mi
    systemReserved:
      cpu: 500m
      memory: 1512Mi
    kubeAPIBurst: 200
    kubeAPIQPS: 100
    maxPods: 500
    nodeStatusMaxImages: -1
    cpuManagerPolicy: static
    topologyManagerPolicy: best-effort
  machineConfigPoolSelector:
    matchLabels:
      custom-kubelet: virt-kubelet-config

Dann entsprechen die Nodes mit dem Label custom-kubelet: virt-kubelet-config die angepasste KubeletConfig verwenden.

oc label mcp worker custom-kubelet=virt-kubelet-config
oc label mcp master custom-kubelet=virt-kubelet-config
oc get nodes -l custom-kubelet=virt-kubelet-config --show-labels

Was die restlichen Parameter wie kubeAPIBurst, kubeAPIQPS, maxPods, nodeStatusMaxImages, cpuManagerPolicy und topologyManagerPolicy und weiteres kann hier nachgelesen werden: OpenShift Virtualization - Tuning & Scaling Guide

Schliesslich die kubeletconfig und die MachineConfig anwenden (erzwingt einen Neustart der Nodes):

oc apply -f kubeletconfig.yaml 
oc apply -f 99-swap-worker.yaml
oc apply -f 99-swap-master.yaml

Nach dem Neustart müssen wir OpenShift Virtualization mitteilen, dass wir Memory Overcommitment nutzen möchten.

oc patch hyperconverged kubevirt-hyperconverged -n openshift-cnv --type=merge -p '{"spec":{"higherWorkloadDensity":{"memoryOvercommitPercentage":150}}}'

Nun können wir das Deployment des wasp-agent prüfen mit:

oc rollout status ds wasp-agent -n openshift-cnv

Networking

Hier eine einfache Übersicht über die Netzwerkarchitektur:

CNV Networking Übersicht

<span style=“font-size: 0.7em;”><strong>Legende:</strong>\

  1. Pods und VMs laufen auf derselben Netzwerkinfrastruktur, sodass containerisierte und virtualisierte Workloads einfach verbunden werden koennen.\
  2. VMs koennen mit dem Standard-Pod-Netzwerk sowie mit beliebig vielen sekundaeren Netzwerken verbunden werden.\
  3. Das Standard-Pod-Netzwerk bietet Konnektivitaet zwischen allen Teilnehmern, Service-Abstraktion, IP-Management, Micro-Segmentation und weitere Funktionen.\
  4. Multus ist ein “Meta”-CNI-Plugin, das Pods oder VMs ueber weitere kompatible CNI-Plugins mit zusaetzlichen Netzwerkschnittstellen verbindet.\
  5. Das Standard-Pod-Netzwerk ist overlay-basiert und wird durch das zugrunde liegende Machine Network getunnelt.\
  6. Das Machine Network kann ueber eine ausgewaehlte Menge an Network Interface Controllern (NICs) definiert werden.\
  7. Sekundaere VM-Netzwerke sind typischerweise direkt auf ein physisches Netzwerk gebrueckt, mit oder ohne VLAN-Kapselung; alternativ sind auch virtuelle Overlay-Netzwerke moeglich. </span>

Standardmäßig sind Pods und VMs nur mit den Pod- und Servicenetzwerken verbunden. Dieses Verhalten ist eine Einschränkung für Anwendungen, die eine Verbindung zu externen Netzwerken herstellen müssen, z. B. einem Unternehmens-Backbone oder einem dedizierten Storage-Netzwerk. Darüber hinaus weisen importierte Legacy-VMs häufig Netzwerkkonfigurationen auf, die mit dem Standard-Kubernetes-Netzwerkmodell nicht kompatibel sind. Diese VMs müssen möglicherweise Zugriff auf externe Netzwerke haben oder mit statischen IP-Adressen konfiguriert werden.

Red Hat OpenShift Virtualization verwendet Custom Resources (CRs) und Container Network Interface (CNI)-Plugins, um erweiterte Netzwerkfunktionen bereitzustellen. OpenShift Virtualization verwendet das Multus-CNI-Plugin, mit dem andere CNI-Plugins aufrufen oder verketten werden können. Der Cluster Network Addons Operator (CNAO) verwaltet Multus. Der HyperConverged-Operator (HCO) deployt den CNAO während der OpenShift Virtualization-Installation.

Mit dem Multus CNI-Plugin können während und nach der OpenShift-Cluster-Installation zusätzliche Netzwerke neben dem Standard-Pod-Netzwerk konfiguriert werden. Das Anschliessen mehrerer Netzwerke an eine VM wird als Multihoming bezeichnet. Diese Funktion ist für verschiedene Szenarien nützlich, darunter auch die folgenden Beispiele:

  • Konfigurieren einer statischen oder dynamischen IP-Adresse auf einer VM
  • Verbinden von Pods und VMs mit Cluster-externen Netzwerken
  • Steigern der Leistung eines netzwerkintensiven Workloads durch Isolierung von Data und Control Planes auf verschiedenen Node-Schnittstellen
  • Steigerung der Sicherheit durch Isolierung von sensiblem Datenverkehr auf einer separaten, gesicherten verwalteten Netzwerkebene oder durch Trennen vertraulicher Daten, die nicht weitergegeben werden dürfen

Wir möchten die VMs mit einem eigenen Netzwerk verbinden (mulithomed), damit wir direkt von aussen auf die VMs zugreifen können, ohne den Umweg über die OpenShift Routen gehen zu müssen.

Wir gehen davon aus, dass wir hierzu ein eigenes physisches Netzwerk haben, welches über die NICs (bond1) der Nodes erreichbar ist.

Ein beispielhafte physische Netzwerktopologie könnte wie folgt aussehen:

Physische Netzwerktopologie (Beispiel)

Machine network         iSCSI network IP
IP address              address                                     +-------------+
    |                       |                                     +-------------+ |
    |                       |                                   +-------------+ | |
.---|-----------------------|--------------------.            +-------------+ | | |
|   |                       V                    |            | VM networks |-+-+-+
|   |                  +---------+               |            +-------------+
|   |                  |10.2.23.x|               |
|   |                  +---------+               |     .----------------------------------------.
|   |                                            |     |                                        |
|   |  +-------------+ +---------+               |     |                                        |
|   |  |br-ex (OVN-K)| |bond0.123|               |     |                                        |
|   V  +-------------+ +---------+               |     |                                        |
| +--------------------------------------------+ |     | +------------------------------------+ |
| |              bond0, 10.2.23.x              | |     | |               bond1                | |
| +--------------------------------------------+ |     | +------------------------------------+ |
|           |                        |           |     |           |                    |       |
|     +----------+             +----------+      |     |     +----------+         +----------+  |
|     |NIC0 port0|             |NIC1 port0|      |     |     |NIC0 port1|         |NIC1 port1|  |
|     +----------+             +----------+      |     |     +----------+         +----------+  |
'-----------|------------------------|-----------'     '-----------|--------------------|-------'
            |                        |                             |                    |
            |                        +-----------------------+     |                    |
            |                                                |     |                    |
            |                +-------------------------------------+                    |
            |                |                               |                          |
           [#]              [#]                             [#]                        [#]
   +--------------------------------+              +--------------------------------------------+
   |      Top-of-rack switch 1      |              |      Top-of-rack switch 2                  |
   +--------------------------------+              +--------------------------------------------+

Die Config für die Bond0.123 und Bond1 für den Installer könnte wie folgt aussehen, für eine agent based Installation:

  apiVersion: v1alpha1
  kind: AgentConfig
  rendezvousIP: rendezvousIP
  hosts:
    - hostname: master0 # für die anderen Master Nodes entsprechend anpassen 
                        # und natürlich auch die Worker Nodes hinzufügen
      role: master
      interfaces:
       - name: enp0s0
         macAddress: 00:21:50:90:c0:10
       - name: enp1s0
         macAddress: 00:21:50:90:c0:20
       - name: enp2s0
         macAddress: 00:21:50:90:c0:30
       - name: enp3s0
         macAddress: 00:21:50:90:c0:40
      networkConfig:
        interfaces:
          - name: bond0.123
            type: vlan 
            state: up
            vlan:
              base-iface: bond0
              id: 300
            ipv4:
              enabled: true
              dhcp: true
              auto-dns: true
              auto-gateway: true
              auto-routes: true
            ipv6:
              enabled: false
          - name: bond0 
            type: bond 
            state: up
            mac-address: 00:21:50:90:c0:10 
            link-aggregation:
              mode: 802.3ad # LACP
              port:
               - enp0s0
               - enp2s0
          - name: bond1 
            type: bond 
            state: up
            mac-address: 00:21:50:90:c0:30 
            ipv4:
              enabled: true
              dhcp: true
              auto-dns: true
              auto-gateway: true
              auto-routes: true
            ipv6:
              enabled: false
            link-aggregation:
              mode: 802.3ad # LACP
              port:
               - enp1s0
               - enp3s0

Nun da wir die Bond0.123 und Bond1 konfiguriert haben, können wir die entsprechenden NetworkAttachementDefinitions erstellen, um die VMs mit diesen Netzwerken zu verbinden.

Um zusätzliche Netzwerkschnittstellen an einen Pod oder an eine VM anzuhängen, muss eine NAD erstellt werden, die die CNI-Plugin-Konfiguration für die Verwendung mit der zusätzlichen Schnittstelle definiert.

OpenShift stellt die folgenden CNI-Plugins für das Multus CNI-Plugin zur Verkettung bereit:

  • bridge
    Konfigurieration eines zusätzlichen bridge-basierten Netzwerks, damit Pods auf demselben Host miteinander und mit dem Host kommunizieren können.
    Eine Linux-Bridge ist erforderlich, um VMs an mehrere Netzwerke anzufügen. Verwenden einer Linux-Bridge, um die Netzwerkschnittstellen der Pods und VMs mit einer physischen Netzwerkschnittstelle auf einem Host oder Node zu verbinden. Pods und VMs in einem Bridge-Netzwerk verwenden dieselbe Broadcast-Domain und dasselbe Subnetz wie die physische Netzwerkschnittstelle des Hosts.
    Wenn der Host Zugriff auf Netzwerke außerhalb des OpenShift-Clusters hat, ermöglichen Bridge-Netzwerke den Pods und VMs im OpenShift-Cluster die Kommunikation mit Ressourcen in diesem externen Netzwerk, z. B. Dateifreigaben oder Datenbanken.
  • host-device
    Konfigurieren eines zusätzlichen host-device-Netzwerks, um Pods und VMs den exklusiven Zugriff auf ein physisches Ethernet-Netzwerkgerät auf dem Host-System zu ermöglichen.
    Der direkte, exklusive Zugriff auf ein physisches Netzwerkgerät ist für jene Anwendungen von Vorteil, die einen hohen Durchsatz und eine geringe Latenz erfordern. Anwendungen, die große Mengen an Datenpaketen verarbeiten, z. B. in Telekommunikationsumgebungen, profitieren ebenfalls von diesem Verbindungstyp.
  • ipvlan
    Konfigurieren eines zusätzlichen IPVLAN-basierten Netzwerks, damit Pods und VMs auf einem Host eindeutige IP-Adressen, aber die gleiche MAC-Adresse wie das physische Netzwerk des Hosts haben können. Der IP-Bereich des physischen Geräts des Hosts wird mit den Pods und VMs gemeinsam verwendet, die mit einem IPVLAN-basierten Netzwerk verbunden sind. Ein IPVLAN-basiertes Netzwerk vereinfacht die Netzwerkadministration, da Sie keine eindeutigen MAC-Adressen erstellen oder verwalten müssen.
  • macvlan
    Konfigurieren eines zusätzlichen MACVLAN-basierten Netzwerks, damit Pods und VMs auf einem Host über eine physische Netzwerkschnittstelle mit anderen Hosts und deren Pods und VMs kommunizieren können. Im Gegensatz zu einem IPVLAN-basierten Netzwerk verfügt jeder Pod und jede VM, der bzw. die an ein zusätzliches MACVLAN-Netzwerk angeschlossen ist, über eine eindeutige MAC-Adresse. Sie können ein MACVLAN-basiertes Netzwerk für Anwendungen nutzen, die eine eindeutige MAC-Adresse für Vorgänge oder die Lizenzierung benötigen. MACVLAN-basierte Netzwerke sorgen auch dafür, dass Ihre Pods und VMs mit Systemen kompatibel sind, die eindeutige MAC-Adressen erwarten, z. B. Netzwerk-Monitoring-Tools und DHCP.
    IPVLAN- und MACVLAN-basierte Netzwerke werden häufig in Container-Umgebungen mit hoher Dichte verwendet, die skalierbare Netzwerke erfordern, z. B. bei Telekommunikations- und IoT-Anwendungen (Internet of Things).
  • SR-IOV
    Konfigurieren eines zusätzlichen SR-IOV-Netzwerks, damit Pods eine Virtual Function-Schnittstelle auf SR-IOV-fähiger Hardware im Hostsystem anschließen können.

Das bridging via Multus ist die gängigste Methode, um VMs mit einem eigenen Netzwerk zu verbinden. Hierbei wird eine Linux-Bridge auf dem Host erstellt, die mit einer physischen Netzwerkschnittstelle verbunden ist. Die VMs werden dann an diese Bridge angeschlossen, wodurch sie Zugriff auf das physische Netzwerk erhalten.

Um eine Linux-Bridge zu erstelle, müssen wir zuerst die eine NodeNetworkConfigurationPolicy erstellen, um die Bridge zu konfigurieren. Anschliessend können wir die entsprechende NetworkAttachementDefinition erstellen, um die VMs mit der Bridge zu verbinden.

Nun müssen wir den NMState Operator installieren, um die Netzwerkinterfaces auf den Nodes zu konfigurieren. Hier die Anleitung dazu.

Dann können wir die NodeNetworkConfigurationPolicy (die NNACP ist Cluster Scoped) erstellen, um die Bridge zu konfigurieren. Hier ein Beispiel für die Konfiguration einer Bridge namens “br0”, die mit der physischen Netzwerkschnittstelle “bond1” verbunden ist:

apiVersion: nmstate.io/v1
kind: NodeNetworkConfigurationPolicy
metadata:
  name: bond1-bridge
spec:
  desiredState:
    interfaces:
    - name: lnbr0
      description: layer2-only linux bridge with an optional port
      type: linux-bridge
      state: up
      ipv4:
        enabled: false
      bridge:
        options:
          stp:
            enabled: true
        port:
        - name: bond1
  nodeSelector: # selektiert die Nodes, auf welchen die Bridge konfiguriert werden soll, resp das label runs_vms: true haben
    runs_vms: true

Nun können wir die NetworkAttachementDefinition erstellen, um die VMs mit der Bridge zu verbinden. Hier ein Beispiel für die Konfiguration einer NAD namens “lnbr0-bridge”, die die Bridge “lnbr0” verwendet:

apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: lnbr0-bridge
  namespace: default # erlaubt die Verwendung der NAD in allen Namespaces
spec:
  config: '{
    "name": "lnbr0-bridge",
    "cniVersion": "0.3.1",
    "type": "bridge",
    "bridge": "lnbr0",
    "ipam": {},
    "macspoofchk": false,
    "disableContainerInterface": false,
    "portIsolation": false
  }'

Storage

Wie weiter oben bereits erwähnt, ist es wichtig, dass der Storage Hersteller einen CSI Treiber für OpenShift Virtualization hat, welcher die Replikation der Metadaten der PVC/PVs zwischen den Standorten unterstützt. Zudem sind Support von Snapshot und Cloning des CSI Drivers ein muss.

Storage Classes

Neben den Standard Storage Classes, das wir vom herkömmlichen OpenShift kennen:

Annotation:
  storageclass.kubernetes.io​/is-default-class=​true

führt Openshift Virtualization eine Default Storage Class für VMs ein:

Annotation:
  storageclass.kubernetes.io​/is-default-class=​true

Diese Annotation gibt an, dass die Storage-Klasse angepasste Einstellungen für Disks virtueller Rechner aufweist. In OpenShift Virtualization hat eine Storage-Klasse mit dieser Annotation Vorrang vor der Standard-Storage-Klasse des Clusters.

In einem normalen OpenShift-Cluster gibt es oft eine generelle, clusterweite Standard-StorageClass. Für “normale” Container (Pods) ist das oft ein File-basiertes Storage wie NFS oder CephFS, weil Container gerne Verzeichnisse teilen.

Für Virtuelle Maschinen ist File-Storage aber oft schlecht. VMs brauchen für gute Performance und Live-Migration meistens Block-Storage (wie Ceph RBD,iSCSI oder Fibre Channel) mit dem Zugriffsmodus ReadWriteMany (RWX) als Block-Device.

Wenn wir die KubeVirt-Annotation setzen, trennen wir diese Welten sauber voneinander

Storage Profiles

Für jede StorageClass, die wir in unserem Cluster haben, generiert der Containerized Data Importer (CDI) vollautomatisch ein passendes StorageProfile (mit exakt demselben Namen).

oc describe storageprofile synology-iscsi-storage
Name:         synology-iscsi-storage
Namespace: #cluster-wide
Labels:       
.
.
.
.

Annotations:  cdi.kubevirt.io/minimumSupportedPvcSize: 1Gi
              storageclass.kubevirt.io/is-default-virt-class: true
API Version:  cdi.kubevirt.io/v1beta1
Kind:         StorageProfile
Metadata:
  Creation Timestamp:  2025-09-15T14:04:59Z
  Generation:          3
  Owner References:
    API Version:           cdi.kubevirt.io/v1beta1
    Block Owner Deletion:  true
    Controller:            true
    Kind:                  CDI
    Name:                  cdi-kubevirt-hyperconverged
    UID:                   c23f67e6-fc16-48e5-ac40-0e763b2d4424
  Resource Version:        385778451
  UID:                     2f62d21c-5d65-4015-9412-1a0f2186ebb2
Spec:
  Claim Property Sets:
    Access Modes:
      ReadWriteMany # Unterstützt RWX Access Mode für Block Storage, auch notwendig für Live Migration
    Volume Mode:  Block # Unterstützt Block Storage
    Access Modes:
      ReadWriteOnce # Unterstützt RWO Access Mode für Block Storage
    Volume Mode:  Block # Unterstützt Block Storage
    Access Modes:
      ReadWriteOnce # Unterstützt RWO Access Mode für Filesystem Storage
    Volume Mode: Filesystem
  Clone Strategy: csi-clone
  Data Import Cron Source Format:  pvc
Status:
  Claim Property Sets:
    Access Modes:
      ReadWriteMany # Unterstützt RWX Access Mode für Block Storage, auch notwendig für Live Migration
    Volume Mode:  Block # Unterstützt Block Storage
    Access Modes:
      ReadWriteOnce # Unterstützt RWO Access Mode für Block Storage
    Volume Mode:  Block # Unterstützt Block Storage
    Access Modes:
      ReadWriteOnce # Unterstützt RWO Access Mode für Filesystem Storage
    Volume Mode: Filesystem # Unterstützt Filesystem Storage
  Clone Strategy: csi-clone
  Data Import Cron Source Format:  pvc
  Provisioner:                     csi.san.synology.com
  Snapshot Class:                  synology-snap-class
  Storage Class:                   synology-iscsi-storage # zugehörige StorageClass
Events:                            <none>

Die Standardeinstellungen für ein Storage-Profil werden durch die Reihenfolge bestimmt, in der der Zugriffsmodus und die Volume-Modi deklariert sind. Das vorherige Storage-Profil deklariert den Zugriffsmodus ReadWriteMany als Standard, wodurch VM-Live-Migrationen ermöglicht werden, und deklariert den Volume-Modus Block als Standard, der eine bessere Leistung für VM-Disks als der Modus Filesystem bietet.

Wenn wir nun eine Disk für via DataVolume erstellen, könnte es wie folgt aussehen:

apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: vm1 
  namespace: my-vm-namespace
  ...output omitted...
spec:
  pvc:
    accessModes:
    - ReadWriteMany 
    resources:
      requests:
        storage: 10Gi 
    storageClassName: synology-iscsi-storage # die StorageClass
    volumeMode: Block 
  source:
    http:
      url: http://images.example.com/openshift4/images/mariadb-server.qcow2

External Storage

Wir haben bis jetzt nur über den internen Storage gesprochen, welcher direkt an die Nodes angeschlossen ist. Es gibt aber auch die Möglichkeit, externen Storage zu verwenden, z.B. über iSCSI oder Fibre Channel. Hierbei wird der Storage über das Netzwerk angebunden und kann von mehreren Nodes gleichzeitig genutzt werden. In diesen Umgebungen verwendet Kubernetes statisches Provisioning. Storage-Administratoren bereiten Volumes im Back-End-Storage vor. Anschließend deklarieren Kubernetes-Administratoren diese Volumes in PV-Ressourcen. Wenn Sie den PVC erstellen, bindet Kubernetes ein verfügbares PV an den Claim.

Hierzu haben wir zwei Möglichkeiten:

  • Den Storage innerhalb der VM verbinden
  • Den Storage über ein Kubernetes-PV verbinden und das Volume dann als VM-Disk anhängen

Das folgende Beispiel zeigt die Konfiguration für ein PV, das die Verbindung mit einem iSCSI-Ziel beschreibt:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: iscsi-pv
spec:
  capacity:
    storage: 50Gi  
  accessModes:
    - ReadWriteOnce  
  volumeMode: Block  
  claimRef:  4
    name: websrv1-staticimgs
    namespace: vm-project
  iscsi:  
     targetPortal: 192.168.51.40:3260
     iqn: iqn.1986-03.com.ibm:2145.disk1
     lun: 0
     initiatorName: iqn.1994-05.com.redhat:openshift-nodes

Snapshots

Mit OpenShift Virtualization können Live-Snapshots von laufenden und angehaltenen VMs erstellt werden. Damit keine inkonsistenten Daten entstehen kommuniziert OpenShift Virtualization mit dem Quick-Emulator, wohl besser bekann als QEMU, um die Dateisysteme “anzuhalten”. Daher ist es wichtig, dass der QEMU Guest Agent in den VMs installiert ist. Zwar können Live-Snapshots auch ohne den QEMU Guest Agent erstellt werden, aber es besteht die Gefahr von inkonsistenten Daten.

Wichtig ist, dass die Snapshot-Funktionalität auf dem Back-End-Storage basieren. Ein VM-Snapshot speichert die VM Konfiguration, wie Anzahl CPUs, RAM, etc. Wenn also die VM Konfiguration geändert wird, dann werden die ursprünglichen Einstellungen bei einem Restore zurückgesetzt.

Einen Snapshot können wir entweder über die OpenShift Web Console oder über die CLI erstellen. Hierzu kreieren eine VirtualMachineSnapshot Ressource:

kind: VirtualMachineSnapshot
metadata:
  name: mariadb-server-20250905
  namespace: my-project
spec:
  source:
    apiGroup: kubevirt.io
    kind: VirtualMachine
    name: mariadb-server

Um einen Restore eines Snapshots zu erstellen, kreieren wir eine VirtualMachineRestore Ressource:

apiVersion: snapshot.kubevirt.io/v1beta1
kind: VirtualMachineRestore
metadata:
  name: restore-db
spec:
  target:
    apiGroup: kubevirt.io
    kind: VirtualMachine
    name: mariadb-server
  virtualMachineSnapshotName: mariadb-server-20250905

Um die verfügbaren Snapshots (im Namespace my-project) aufzulisten, können wir den folgenden Befehl verwenden:

oc get virtualmachinesnapshot -n my-project
NAME                     SOURCEKIND     SOURCENAME     PHASE     READYTOUSE ...
db-before-schema         VirtualMachine mariadb-server Succeeded true       ...
mariadb-server-20250905  VirtualMachine mariadb-server Succeeded true       ...

Cool ist auch die Funktionalität, dass wir einen Snapshot auch als Vorlage für eine neue VM verwenden können.

High Availability

Damit wir im Node Fehlerfall so schnell wie mögich die Workloads müssen wir die Node health Checks und Self Node Remediation oder Fence Agents Remediation einrichten. Somit können wir innert nützlicher Frist einen Node Failure erkennen, Split-Brain Situationen vermeiden und die betroffenen VMs auf einem gesunden Node neu starten.

Hierzu wird vorausgesetzt, dass die Nodes ein BMC Interface haben, wie ILO, iDrac, IPMI etc.. Zudem müssen die BMCs vom OpenShift Cluster aus erreichbar sein, damit die entsprechenden Befehle ausgeführt werden können.

Wir wählen hier die Fence Agent Remediation:

Bei OpenShift Virtualization (KubeVirt) laufen virtuelle Maschinen, die oft auf blockbasierten, geteilten Speicher (z. B. ODF/Ceph, SAN) zugreifen. Wenn ein Node ausfällt, muss der Cluster zu 100 % sicherstellen, dass der fehlerhafte Node wirklich isoliert und abgeschaltet ist (STONITH - Shoot The Other Node In The Head), bevor die VMs auf einem anderen Node neu gestartet werden.

  • FAR kommuniziert auf Hardware-Ebene direkt mit den HP iLO-Schnittstellen (via IPMI oder spezifischem iLO-Agenten wie fence_ilo). Es schaltet den Server hart ab (Power Off/Cycle). Dies ist die deterministischste und sicherste Methode, um sicherzustellen, dass der Node keine IO-Operationen mehr an den Storage sendet.
  • Best Practice: In Baremetal-Umgebungen mit Out-of-Band-Management (OOB) ist hardwarebasiertes Fencing der Standard für HA-Virtualisierung.

Self Node Remediation (SNR)

SNR basiert auf Software-Mechanismen innerhalb des Betriebssystems (/dev/watchdog). Wenn der Node die Verbindung zum Cluster verliert, isoliert er sich selbst (z. B. durch Auslösen eines Kernel-Panics über einen Hardware-Watchdog oder durch das Kappen der Netzwerkverbindungen).

  • Vorteile: Benötigt keine OOB-Management-Schnittstelle (iLO). Leicht einzurichten.
  • Nachteile: Da es auf dem fehlerhaften OS selbst basiert, gibt es keine externe Garantie, dass der Node wirklich tot ist. Wenn das OS komplett einfriert und der Watchdog aus irgendeinem Grund nicht auslöst, hängt der Cluster in einem unbestimmten Zustand.
  • Einsatzgebiet: Hervorragend als Fallback-Methode oder für Edge-Umgebungen (wie Intel NUCs ohne echtes OOB-Management), aber nicht die erste Wahl für Enterprise-Baremetal mit iLO.

Machine Deletion Remediation (MDR)

Diese Methode löst bei Unhealthy Nodes einfach das Löschen des entsprechenden Machine-Objekts in der Machine API von OpenShift aus. Sehr gut geeigent für Cloud Workloads.

  • Vorteile: Sehr nativ für OpenShift.
  • Nachteile: MDR ist primär für Cloud-Umgebungen (AWS, Azure, GCP, VMware) konzipiert, in denen Nodes flüchtig (ephemeral) sind. Wenn dort eine Maschine gelöscht wird, ordnet die Cloud-Infrastruktur sie sofort dem Kümmerer (dem Cloud Provider) zu, schaltet sie ab und provisioniert eine neue. Auf statischem Baremetal müssten Sie hierfür die BareMetal Operator (BMO/Metal3)-Integration massiv ausbauen, was oft komplexer und fehleranfälliger ist als klassisches Fencing.

Fence Agent Remediation Operator

Der Fence Agent Remediation Operator wir verwendet um mit dem BMC zu kommunizieren und die entsprechenden Befehle auszuführen, um einen Node zu isolieren oder neu zu starten.

Hier die Dokumentation zur Installation und Konfiguration des Fence Agent Remediation Operators: Red Hat OpenShift Virtualization - High Availability

apiVersion: fence-agents-remediation.medik8s.io/v1alpha1
kind: FenceAgentsRemediationTemplate
metadata:
  name: hp-ilo-fencing-template
  namespace: openshift-workload-availability
spec:
  template:
    spec:
      agent: fence_ipmilan
      # Globale Parameter, die für alle Nodes gelten
      sharedparameters:
        --lanplus: ""                   # Zwingend erforderlich für IPMI v2.0 (Standard bei HP iLO)
        --action: reboot                # Führt einen Power Cycle durch
        --username: "Administrator"     # Ihr iLO-Benutzer (In Prod: Nutzen Sie stattdessen Secrets!)
        --password: "SuperSecretPassword"
      # Node-spezifische Parameter: Hier wird der Node-Name auf die jeweilige iLO-IP gemappt
      nodeparameters:
        --ip:
          worker-0.example.com: "192.168.100.10"
          worker-1.example.com: "192.168.100.11"
          worker-2.example.com: "192.168.100.12"
          worker-3.example.com: "192.168.100.13"
          worker-4.example.com: "192.168.100.14"
          worker-5.example.com: "192.168.100.15"

Damit im Fehlerfall die Nodes automatisch gefenced werden, müssen wir die entsprechende NodeHealthCheck Ressource erstellen:

apiVersion: remediation.medik8s.io/v1alpha1
kind: NodeHealthCheck
metadata:
  name: vm-worker-health-check
spec:
  # Sicherheitsnetz: Verhindert, dass der ganze Cluster gefenced wird (z.B. bei einem Core-Switch Ausfall)
  minHealthy: 51% 
  
  # Verknüpfung zu unserem Fencing Template von oben
  remediationTemplate:
    apiVersion: fence-agents-remediation.medik8s.io/v1alpha1
    kind: FenceAgentsRemediationTemplate
    name: hp-ilo-fencing-template
    namespace: openshift-workload-availability
    
  # Welche Nodes sollen überwacht werden? 
  selector:
    matchExpressions:
      - key: node-role.kubernetes.io/worker
        operator: Exists
      - key: node-role.kubernetes.io/master
        operator: Exists
        
  # Wann gilt ein Node als tot?
  unhealthyConditions:
    - type: Ready
      status: "False"
      duration: 50s  # Node meldet explizit einen Fehler für mindestens 50 Sekunden
    - type: Ready
      status: "Unknown"
      duration: 50s  # Kubelet erreicht die Control Plane seit 50 Sekunden nicht mehr