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).
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 :
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.
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.

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.
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 :
À 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.
Voici le schéma cible décrivant les composants mis en jeu.

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 |
Le serveur témoin utilisé est une machine physique disposant des caractéristiques suivantes :
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.
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.

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 :
Ma VM OPNsense va exploiter deux interfaces virtuelles associées d’un côté à mon VLAN interne et de l’autre à mon VLAN WEB.
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

Cliquez sur l'image pour l'agrandir.
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.
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

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

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

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

Cliquez sur l'image pour l'agrandir.
Ensuite, on s’assure que tout est correctement configuré avec la commande
nmcli connection show

Cliquez sur l'image pour l'agrandir.
Voilà une bonne chose de faite.
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 :
kubeadmwgettarkubectlkubeletcryptsetupcrunjqgitdevice-mapper-multipathdockercontainerdoverlaybr_netfilterkvmvhost_netdm_cryptPour 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.