Kubevirt - Partie 1: Définition et Réseau

Introduction

J’ai déjà réalisé trois articles sur KubeVirt pour partager ma première expérience autour de ce produit destiné à gérer et exécuter des machines virtuelles sous Kubernetes.

Malheureusement, après plusieurs mois d’utilisation (et de galères), je me suis aperçu que l’architecture et la méthodologie retenues ne sont pas stables.

J’ai rencontré énormément de problèmes, notamment autour de la couche réseau. J’ai donc décidé de réaliser de nouveaux tutoriels, plus complets et associés à des choix de composants plus efficaces.

Lors de ma première installation, mon erreur principale a été de me tourner vers une architecture en developpement, jugée la plus performante, mais pas forcément stable.

Je me suis complexifié la vie, m’obligeant à activer des fonctionnalités au niveau de KubeVirt encore en testing.

KubeVirt étant lui-même toujours en développement, et toujours en phase d’incubation par la Cloud Native Computing Foundation (CNCF). Ce n’était pas la meilleure idée de chercher à le déployer avec des composants non finalisés… surtout pour aller chercher de la soi-disant performance supplémentaire bien inutile dans mon usage (comme dans beaucoup d’autres).

Nouvelle approche

Je vais donc repartir de zéro en exploitant cette fois-ci le bon vieux bridge pour la gestion du réseau, ainsi que la couche de stockage Longhorn pour me mettre à la cible d’un cluster Kubevirt multinode.

Je vais rester pour l’instant sur un seul node physique, mais qui pourra sans difficulté être étendu par la suite.

Plusieurs articles vont illustrer mon installation :

  • 01 - Définition de la cible, déploiement des prérequis et configuration réseau (cet article)
  • 02 - Installation de Kubernetes/KubeVirt et Cilium/Multus
  • 03 - Configuration de Traefik, Gateway API et CertManager
  • 04 - Installation de longhorn
  • 05 - Déploiement d’une VM et outils supplémentaires

Mes précédents articles resteront en ligne au cas où quelqu’un y trouverait un intérêt, notamment pour l’usage de macvtap. Mais ils sont désormais dépréciés au profit des articles nouvellement écrits et mis à jour.

KubeVirt – Rappel (ou pas)

Pour démarrer, je vais reprendre une grande partie de ce que j’ai déjà pu expliquer sur KubeVirt.

KubeVirt est une extension applicative à Kubernetes qui lui permet d’orchestrer l’exécution de machine virtuelle comme il le fait nativement pour des conteneurs.

L’idée est de reprendre exactement la même logique objet de K8S pour étendre son API et lui donner la capacité à déclarer et à gérer des machines virtuelles.

C’est RedHat qui a initié le projet en 2017 avant de le laisser sous la gestion de la CNCF ou celui-ci est toujours en statuts incubating à l’heure de rédaction de cet article.

Statut du projet

Cliquez sur l'image pour l'agrandir.

À noter que RedHat en propose déjà une implémentation commerciale à travers sa plateforme OpenShift.

Au début, KubeVirt est resté relativement confidentiel. Cependant, la perte d’intérêt progressive pour VMware depuis son rachat par Broadcom et l’augmentation sans précédent des coûts de licences associés a remis un coup de projecteur sur les solutions alternatives de virtualisation dont fait partie KubeVirt (comme Proxmox et XCP-NG).

Surtout que derrière, KubeVirt se cache des briques bien connues, à savoir KVM et QEMU. Des solutions éprouvées et implémentées maintenant depuis longtemps, notamment chez les principaux fournisseurs de cloud pour l’exécution de VMs.

Maintenant, contrairement justement aux solutions « clef en main » type ProxMox ou XCP-NG, en dehors de l’offre OpenShift, l’implémentation de KubeVirt reste encore quelque peu hasardeuse dans certains contextes et dans certaines configurations.

Le principal défi, mais aussi l’aspect le plus captivant de KubeVirt, est l’intégration de la logique Kubernetes dans l’univers de la virtualisation. Déployer une VMs en combinant des objets décrits dans des fichiers Yaml n’est pas forcément simple à appréhender.

De plus, lorsqu’on part d’une installation de Kubernetes Vanilla, comme c’est mon cas, il y a pas mal de composants additionnels à déployer et à configurer.

Heureusement, comme ce fut le cas pour K8S, les installations et les déploiements deviennent de plus en plus simples et assistés, en partie grâce à l’aide de l’IA, même si cela peut parfois entraîner des erreurs en raison de la relative jeunesse d’adoption de KubeVirt pour certains usages.

Cible et objectifs du tutorial

Mon expérimentation vise à mettre en place un premier nœud Kubernetes servant à la fois de control plane et de worker, sur lequel je déploierai une VM OPNsense agissante comme un routeur firewall entre deux réseaux de mon LAB.

L’intérêt de cette VM va être de combiner plusieurs besoins qu’on peut être amené à retrouver souvent en virtualisation :

  • Un stockage persistant compatible avec un déplacement de VMs d’un serveur à un autre
  • Plusieurs interfaces réseau virtuelles via la déclaration de switchs virtuels au sein de la plateforme.

À la fin, je devrais avoir une VM capable d’être migrée vers un futur nœud qui serait facile à mettre en place, partant du principe que j’aurai déjà installé tous les prérequis nécessaires pour KubeVirt. Ceci en incluant les bases d’un cluster Kubernetes en mesure d’exécuter des applications conteneurisées et des VMs.

Architecture

Voici le schéma cible décrivant les composants mis en jeu.

Schéma des composants pour KubeVirt

Cliquez sur l'image pour l'agrandir.

Le tableau suivant définit le rôle de chaque brique.

Application Rôle Utilisation
KVM Hyperviseur libre de type I intégré dans le noyau Linux Exécute les VMs
QEMU Solution d'émulation Fournis l’émulation et la gestion des périphériques pour les VMs
libvirt API de gestion des VMs Fournit la couche d'interaction avec les VMs
KubeVirt Controleur K8S et collection d'objets Intègre la gestion des VMs dans Kubernetes et orchestre leur exécution via KVM à travers libvirt
Cilium Connectivité réseau pods/VMs Fournis la connectivité principale aux VMs
Multus Ajoute plusieurs interfaces réseau à un objet K8S Permets d’ajouter des interfaces réseau supplémentaires aux VMs
Linux Bridge Solution réseau L2 intégrée au kernel Linux Attache une interface physique à la VM

Matériel

Le serveur témoin utilisé est une machine physique disposant des caractéristiques suivantes :

  • OS: Rocky Linux 10.1
  • CPU: Ryzen AMD Ryzen 9 7900X (12 cœurs / 24 threads)
  • GPU: NVIDIA 3060 12GO vRAM
  • RAM: 96 Go
  • Stockage: 2 SSD NVME (1To) + 2 SSD SATA (2 To)
  • Network:
    • Une carte intégrée RTL8125 2.5GbE
    • Une carte PCI-E 4 ports Intel I350 Gigabit

J’attire votre attention sur le choix de la carte réseau. Lors de ma première installation, je n’avais à disposition que des chipsets Realtek.

C’est clairement un mauvais choix. Même s’il m’a été possible de modifier les drivers par défaut pour utiliser des versions plus récentes que celles intégrées de base dans l’OS. Realtek n’est clairement pas un bon matériel pour exploiter des fonctionnalités réseau avancées sur un OS Linux.

Privilégiez des contrôleurs Intel, largement mieux supportés.

Configuration réseau

Logique

Dans mon cas, ma carte Realtek 2.5Gb va me servir principalement comme interface d’administration de mon serveur et point d’entrée SSH. C’est aussi elle qui portera l’IP principale de l’API K8S, mais elle ne sera pas utilisée pour les interfaces KubeVirt.

Pour ce besoin, je réserve mes ports sous chipset Intel.

Voici d’ailleurs la logique réseau schématisée.

Schéma détaillé de la cible réseau KubeVirt

Cliquez sur l'image pour l'agrandir.

Les ports enp13s0f0 et enp13s0f3 vont servir d’interface de bridge pour créer des switchs virtuels auxquels connecter mes VMs.

Par défaut, enp13s0f3 ne va desservir qu’un seul réseau, donnant accès à ma DMZ « First », rattachée directement à mon routeur externe.

enp13s0f0 va proposer deux vlans :

  • Le VLAN par défaut rattaché à mon LAN classique
  • Le VLAN 6, rattaché à ma DMZ intermédiaire, que j’ai appelé VLAN WEB, et destiné à accueillir mes serveurs accessibles depuis l’extérieur.

Ma VM OPNsense va exploiter deux interfaces virtuelles associées d’un côté à mon VLAN interne et de l’autre à mon VLAN WEB.

Mise en oeuvre

Passons justement à la configuration des interfaces.

Je ne vais pas détailler la partie concernant l’interface enp8s0 dédié à l’IP par défaut du serveur (192.168.10.160). Celle-ci est configurée à l’installation de l’OS et ne présente pas de particularité.

Tout d’abord, on supprime les configurations par défaut associées à l’interface dédiée à Kubevirt :

sudo nmcli connection delete enp13s0f0
sudo nmcli connection delete enp13s0f3
Suppression des config réseau par défault

Cliquez sur l'image pour l'agrandir.

Choix du composant réseau

Il va maintenant falloir choisir la technologie utilisée pour la création des interfaces virtuelles. Trois choix s’offrent à nous.

Caractéristique Linux Bridge Macvtap SR-IOV
Type Logiciel (Switch L2) Logiciel (Driver optimisé) Matériel (Virtualisation NIC)
Performance Moyenne (Overhead CPU) Très bonne Maximale (Native)
Usage CPU Hôte Élevé (l'hôte route tout) Moyen/Faible Nul (Offload hardware)
Setup Infra Configurer un br0 Rien (juste interface) Drivers + Device Plugin K8s + BIOS
Isolation Faible (l'hôte voit tout) Moyenne Forte (Bypass OS hôte)
Comm. Hôte <-> VM ✅ Fonctionne ❌ Non (sauf astuces) ❌ Non
Live Migration ✅ Facile / Standard ✅ Possible ⚠️ Complexe (nécessite config avancée)
Cas d'usage Dev, General Purpose, Debug Prod généraliste, Bare Metal Telco (5G), Trading HFT, NFV

SR-IOV bien qu’étant la méthode la plus performante, elle implique une complexité de configuration et des prérequis matériels particuliers. Ma carte Intel le supporte, mais je m’expose à des limitations futures sur des déplacements de VMs à chaud.

Auparavant, j’ai utilisé la fonctionnalité macvtap, qui offre une plus grande flexibilité sur le plan matériel, mais j’ai rencontré plusieurs problèmes lors de son intégration avec KubeVirt, notamment en termes de stabilité. Sans doute ai-je mal fait les choses, quoi qu’il en soit si je ne tire pas un trait définitif sur cette manière de faire, je préfère m’orienter vers le mode Linux Bridge.

C’est certes la solution la moins performante et le plus ancienne, mais elle est souple, stable et parfaitement supportée par KubeVirt.

On retrouve d’ailleurs l’usage du mode bridge dans beaucoup de configuration et, hormis des besoins de performance très avancés, l’overhead CPU et la latence engendrée sont le plus souvent parfaitement admissible pour des besoins courants.

Configuration des interfaces

Configurons donc cela en commençant par le réseau LAN par défaut.

sudo nmcli con add type bridge con-name br-lan ifname br-lan ipv4.method disabled ipv6.method disabled bridge.stp no
sudo nmcli con add type ethernet con-name br-lan-slave-1 ifname enp13s0f0 master br-lan
sudo nmcli con up br-lan
Configuration de l'interface LAN

Cliquez sur l'image pour l'agrandir.

L’interface br-lan est maintenant UP.

On poursuite par la configuration du VLAN 6. On commence par créer l’interface du VLAN 6 elle-même :

sudo nmcli con add type vlan con-name vlan-6 dev enp13s0f0 id 6

Puis on construit l’interface bridge qui s’y rattache :

sudo nmcli con add type bridge con-name br-web ifname br-web ipv4.method disabled ipv6.method disabled bridge.stp no
sudo nmcli connection modify vlan-6 master br-web slave-type bridge
sudo nmcli connection modify vlan-6 ipv4.method disabled ipv6.method disabled
sudo nmcli con up br-web
Configuration de l'interface VLAN WEB

Cliquez sur l'image pour l'agrandir.

Voilà maintenant l’interface br-web opérationnelle.

On termine par l’interface DMZ qui reprend les mêmes principes que l’interface par défaut sur le LAN, mais en exploitant comme interface physique source enp13s0f3

sudo nmcli con add type bridge con-name br-dmz ifname br-dmz ipv4.method disabled ipv6.method disabled bridge.stp no
sudo nmcli con add type ethernet con-name br-dmz-slave-1 ifname enp13s0f3 master br-dmz
sudo nmcli con up br-dmz
Configuration de l'interface DMZ

Cliquez sur l'image pour l'agrandir.

L’interface DMZ est up.

Histoire de s’assurer que tout est OK, on refait une commande de montée des interfaces bridge :

sudo nmcli connection up br-lan-slave-1
sudo nmcli connection up br-dmz-slave-1
sudo nmcli connection up vlan-6
Monté de toute les interfaces

Cliquez sur l'image pour l'agrandir.

Ensuite, on s’assure que tout est correctement configuré avec la commande nmcli connection show

Controle des interfaces

Cliquez sur l'image pour l'agrandir.

Voilà une bonne chose de faite.

Prérequis tiers

On peut maintenant poursuivre avec les prérequis tiers nécessaires à notre installation de Kubernetes et de KubeVirt.

Pour cela je vais reprendre mes playbook ansible utilisés pour mon cookbook Kubernetes, légèrement retravaillé.

Je ne vais pas revenir en détail sur tout ce qui est fait dans ces playbooks, mais globalement :

  • On déploie un certain nombre de paquets nécessaires
    • kubeadm
    • wget
    • tar
    • kubectl
    • kubelet
    • cryptsetup
    • crun
    • jq
    • git
    • device-mapper-multipath
  • On désinstalle des paquets inutiles ou pouvant être en conflit avec notre configuration
    • docker
    • containerd
  • On s’assure de la présence de certains modules du kernel :
    • overlay
    • br_netfilter
    • kvm
    • vhost_net
    • dm_crypt
  • On installe et on configure containerd directement à partir des sources afin qu’il puisse être utilisé comme CRI (Container Runtime Interface) pour Kubernetes (sur ce point, il est important de noter qu’avec RockyLinux 10, runc est devenu crun).

Pour rappel, Containerd est le runtime de haut niveau s’exécutant en tant que daemon et en lien avec les agents Kubernetes, les kubelet. Il s’appuie sur un binaire de plus bas niveau, historiquement runc pour piloter les conteneurs.

Runc est un exécutant compatible OCI (Open Container Interface), le standard pour la conteneurisation. Développé en Go, il est remplacé par une version, nommée crun, en C, jugée plus rapide et plus optimisée dans toutes les familles d’OS RedHat à partir de la v10.

Outil Niveau Rôle Langage Performance Usage typique
Containerd Haut Gère les images, API Kubernetes Go N/A (C'est un démon) Standard K8s
Runc Bas Crée le processus Linux Go Standard 99% des clusters K8s
Crun Bas Crée le processus Linux C Haute (+ rapide, - RAM) Edge, Serverless, Fedora/RHEL

Il va etre nécessaire d'adapter la configuration de containerd pourqu'il puisse exploiter crun en place de runc. Vous pouvez trouver une configuration compatible ici.

On ajuste des paramètres du Kernel via l'ajout d'un fichier 99-kubernetes.conf dans /etc/sysctl.d/

  
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net/bridge/bridge-nf-call-arptables = 1
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 8192
    

On prépare les fichiers de configuration propre à Kubernetes, dont le fichier kubeadm.conf qu’on passera à kubeadm pour initier le cluster.

  
---
# PARTIE 1 : Configuration du noeud initial (Master)
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "192.168.10.160"
  bindPort: 6443
nodeRegistration:
  criSocket: "unix:///var/run/containerd/containerd.sock"
  kubeletExtraArgs:
  - name: "node-ip"
    value: "192.168.10.160"

---
# PARTIE 2 : Configuration du Cluster
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: "1.34.2"
clusterName: "rubikub"
controlPlaneEndpoint: "rubikub.coolcorp.priv:6443"
networking:
  dnsDomain: cluster.local
  podSubnet: "10.11.0.0/16"
  serviceSubnet: "10.12.0.0/16"
apiServer:
  certSANs:
  - "192.168.10.160"
  - "rubikub.coolcorp.priv"
  - "127.0.0.1"
  - "localhost"
scheduler: {}
controllerManager: {}

---
# PARTIE 3 : Configuration du Kubelet
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
failSwapOn: true
authentication:
  anonymous:
    enabled: false
    

192.168.10.160 correspond à l'IP de mon serveur et l'IP associé à l'API K8S sur l'interface enp8s0.

Si vous désirez en savoir plus sur l'usage de ce fichier et ses parametres n'hésitez pas à passer par mon article issue de mon cookbook sur Kubernetes.

On traite également le fichier cilium-values.yaml associé au CNI (Container Network Interface) Cilium.

  
kubeProxyReplacement: true
k8sServiceHost: 192.168.10.160
k8sServicePort: 6443
cni:
  # Empêche Cilium de prendre le contrôle total du dossier /etc/cni/net.d
  # Cela permet à Multus de rester le "maître" et d'appeler Cilium comme plugin délégué
  exclusive: false
ipam:
  mode: "cluster-pool"
  operator:
    clusterPoolIPv4PodCIDRList:
      - "10.13.0.0/16"
# Configuration Hubble (Visualisation)
hubble:
  relay:
    enabled: true
  ui:
    enabled: true
    frontend:
      server:
        ipv6:
          enabled: false
  tls:
    auto:
      enabled: true
      method: helm
      certValidityDuration: 1095

cgroup:
  autoMount:
    enabled: false
   

N’hésitez pas également à parcourir mon cookbook Kubernetes pour en apprendre davantage.

Il est important de noter que le fichier de configuration kubeadm a évolué au fil des versions de Kubernetes. La version présentée ici est parfaitement compatible avec K8S 1.34, la version cible de ce tutoriel (à la date de rédaction de l’article, la version 1.35 vient d’être mise à disposition, ce fichier de configuration devrait également être utilisable avec cette version).

Pour ceux qui le souhaite, l'ensemble de mon role playbook ansible en charge de la création des fichiers de configurations et du traitement des prerequis est disponible ici.

On s’arrête la pour cette première partie. La suite est disponible via l'url suivante avec l’initialisation de Kubernetes/KubeVirt et la mise en place de Cilium/Multus.