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
- Red Hat OpenShift Virtualization Documentation
- OpenShift Virtualization Example Architecture: 1000 virtual machines
- Red Hat OpenShift Virtualization Disaster Recovery
- Red Hat OpenShift Virtualization Performance Tuning
- Red Hat OpenShift Virtualization Security Best Practices
- Red Hat OpenShift Cluster Sizing Guide
- Red Hat OpenShift Virtualization Cookbook
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:
| Vendor | Tool | Metadata Storage Speicherort | Primärer Failover Mechanismus |
|---|---|---|---|
| NetApp | Trident Protect | External S3 Bucket | “Restore” job from S3 to K8s API. |
| Dell | CSM Replication | K8s CRDs (Internal) | repctl CLI / Controller sync. |
| Pure | Portworx | Distributed KV Store | Cluster-to-cluster sync. |
| Huawei | CDR Plugin / BCManager | Proprietary / 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
| Hostname | Typ | Ziel | Zweck |
|---|---|---|---|
| api.<cluster>.<domain> | A/AAAA,API | Load Balancer | Externer Zugriff auf die API (Port 6443). |
| api-int.<cluster>.<domain> | A/AAAA,API | Load Balancer | Interne Kommunikation (Nodes zu API & Port 22623). |
| *.apps.<cluster>.<domain> | A/AAAA,Ingress | Load Balancer | Wildcard für alle App-Routen (Ports 80/443). |
| master<0-2>.<cluster>.<domain> | A/AAAA | IP der Master-Nodes | Permanent (Control Plane). |
| worker<N>.<cluster>.<domain> | A/AAAA | IP der Worker-Nodes | Permanent (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
| Type | vCPU | RAM (GB) | Anzahl VMs | Total vCPU | Total RAM (GB) |
|---|---|---|---|---|---|
| Small | 2 | 8 | 100 | 200 | 800 |
| Medium | 4 | 16 | 200 | 800 | 3200 |
| Large | 8 | 32 | 150 | 1200 | 4800 |
| XLarge | 16 | 64 | 50 | 800 | 3200 |
| Total | 500 | 3000 | 12000 |
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)
| Count | vCPU | GiB | MiB | Total GiB | Overhead per VM MiB | Total Overhead GiB |
|---|---|---|---|---|---|---|
| 100 | 2 | 8 | 8192 | 819200 | 266.384 | 26 |
| 200 | 4 | 16 | 16384 | 3276800 | 298.768 | 58 |
| 150 | 8 | 32 | 32768 | 4915200 | 363.536 | 53 |
| 50 | 16 | 64 | 65536 | 3276800 | 493.072 | 24 |
| Total | 500 | 12288000 | 161 |
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:
| Type | Raw | Overcommit Ratio | Effective |
|---|---|---|---|
| CPU | 3000 vCPU | 4:1 | 750 Cores |
| RAM | 12000 GiB | 2:1 | 6000 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>\
- Pods und VMs laufen auf derselben Netzwerkinfrastruktur, sodass containerisierte und virtualisierte Workloads einfach verbunden werden koennen.\
- VMs koennen mit dem Standard-Pod-Netzwerk sowie mit beliebig vielen sekundaeren Netzwerken verbunden werden.\
- Das Standard-Pod-Netzwerk bietet Konnektivitaet zwischen allen Teilnehmern, Service-Abstraktion, IP-Management, Micro-Segmentation und weitere Funktionen.\
- Multus ist ein “Meta”-CNI-Plugin, das Pods oder VMs ueber weitere kompatible CNI-Plugins mit zusaetzlichen Netzwerkschnittstellen verbindet.\
- Das Standard-Pod-Netzwerk ist overlay-basiert und wird durch das zugrunde liegende Machine Network getunnelt.\
- Das Machine Network kann ueber eine ausgewaehlte Menge an Network Interface Controllern (NICs) definiert werden.\
- 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