Blog & Média | WeScale

Découverte de KubeVirt : Des machines virtuelles sur Kubernetes

Rédigé par Aubin Morand | 10/05/2023

Le développement d’applications conteneurisées est depuis plusieurs années sous la lumière des projecteurs. Kubernetes est aujourd’hui le choix par défaut pour l’orchestration de conteneurs. Aujourd’hui, nous allons nous intéresser à étendre ce modèle, à savoir utiliser des machines virtuelles dans nos clusters Kubernetes.

Présentation

Dans Kubernetes, les applications sont déployées sous la forme de Pods, contenant un ou plusieurs conteneurs. Le Pod est l’unité logique la plus fine qu’est capable de gérer Kubernetes. En tant qu’orchestrateur, Kubernetes gère le cycle de vie des pods de l’application conteneurisée (création, réplication, réponse à la charge, exposition et bien plus encore).

Pour autant, déployer une application sur Kubernetes, quand elle n’a pas été prévue pour être conteneurisée peut être un travail fastidieux, voire impossible selon les cas.

En effet, une application conteneurisée est, par design, stateless et immuable. Elle ne change pas lors de sa reconstruction et n’a qu’une seule tâche, exécuter sa charge, puis s’éteindre.

Les machines virtuelles, elles, se doivent de pouvoir conserver des données qui seront persistées au cours de leur cycle de vie. Elles doivent pouvoir être démarrées et arrêtées un nombre infini de fois sans perturber le déroulement des opérations. Ce modèle est à contre-courant d’un conteneur, alors, par défaut, nos machines virtuelles se retrouvent hébergées dans une infrastructure différente.

C’est ici que KubeVirt intervient.

Cas d’utilisation de KubeVirt

On peut dénombrer plusieurs exemples de cas d’usage dans lesquels une machine virtuelle n’est pas remplaçable par des conteneurs. Principalement lorsque l’application à des dépendances envers des modules kernels qui ne sont pas intégrables dans des conteneurs. On peut également citer l’exemple des progiciels qui ne sont pas conteneurisés et doivent par défaut, s'exécuter dans une machine virtuelle.

L’utilisation de machines virtuelles dans Kubernetes peut également présenter les avantages suivants :

  • On dispose d’un réseau commun avec les conteneurs, les machines virtuelles peuvent communiquer directement avec d’autres conteneurs, ce qui évite la latence de passer au travers d’un load balancer et d’ingress quand la machine virtuelle est hébergée en dehors du cluster Kubernetes.
  • On peut conserver l’investissement dans les méthodes et outils de déploiement, comme ansible par exemple
  • On n’a plus à gérer qu’une seule plateforme qui héberge toutes nos charges applicatives
  • Kubernetes est à l'heure actuelle un des rares composants d’infrastructure commun à l’ensemble des Cloud Providers et peut être installé on premise. On s'assure alors de pouvoir migrer de l’un à l’autre avec un faible coût de transformation. Une migration entre deux Cloud Provider (ou l’utilisation de multi-cloud) s’accompagne dans un contexte conventionnel d’une surcharge de travail lors de la réécriture des fichiers d’IaC qui n’est pas présente quand tout est déployé sur Kubernetes.

Fonctionnement de KubeVirt

KubeVirt étend les fonctionnalités natives de Kubernetes au travers d’un pattern Operator pour exposer des ressources spécifiques dans des Pods.

En pratique, KubeVirt utilise les utilitaires KVM/QEMU du Node et utilise un Pod exécutant un processus libvirt pour gérer la machine virtuelle.

KubeVirt utilise les définitions natives de Kubernetes pour créer ses machines virtuelles:

  • stockage, au travers de PersistentVolumes
  • réseau, au travers des interfaces réseaux fournies par les Container Network Interfaces
  • processeur et mémoire vive au travers des requests/limits

Ces éléments sont déjà présents nativement dans Kubernetes, mais ce dernier ne dispose pas d’une ressource en capacité d’en garantir la cohérence pour les besoins d’une machine virtuelle. On pourrait citer en exemple le fait qu’il doit exister une relation d’exclusivité entre le stockage du système et le compute dans une machine virtuelle. Ainsi, le stockage d’une machine virtuelle ne doit pas être partagé entre deux Pods.

Kubernetes est déjà en capacité de fournir des ressources avec une couche d’abstraction par dessus des Pods au travers de ressources appelées “Workload Controllers”, avec les Deployments, les ReplicaSets, les DaemonSets, les StatefulSets, les Jobs, et plus encore.

De cette manière, KubeVirt fournit son propre Workload Controller au travers du pattern Operator pour proposer une Custom Resource Definition (CRD) de type VirtualMachine.

Ces ressources permettent d’ajouter certaines fonctionnalités que l’on ne retrouve pas nativement avec simplement un Pod. Par exemple, on veut pouvoir éteindre, démarrer et redémarrer une machine virtuelle autant que nécessaire. KubeVirt permet de contrôler davantage l’état du Pod contrôlant la machine virtuelle pour offrir cette capacité d’éteindre et redémarrer au besoin.

Cette approche permet à KubeVirt de pouvoir utiliser l’intégralité des fonctionnalités proposées par l’écosystème kubernetes natif, ainsi que de l’écosystème étendu au travers d’autres opérateurs comme ArgoCD, Cert-Manager, Consul et plus encore.

Tutoriel

Dans le cadre de ce tutoriel, nous allons installer l’opérateur KubeVirt, que nous allons utiliser pour créer une machine virtuelle.

KubeVirt utilisant Cloud-init pour gérer le kickstart de la machine virtuelle, nous allons également créer un script de démarrage pour installer des clefs ssh dans notre machine virtuelle, et créer une ReadinessProbe.

Prérequis

Afin de pouvoir utiliser un peu KubeVirt, nous allons avoir besoin d’un cluster Kubernetes avec les droits cluster-admin, que ce soit un cluster provenant d’un Cloud Provider, d’un MiniKube, d’un openshift ou autre. 

La majeure partie des distributions Kubernetes sont en capacité de faire fonctionner KubeVirt, et même, certaines l’intègrent totalement, comme avec Openshift qui inclut un support étendu dans sa WebUI permettant d’y gérer sa machine virtuelle.

Il va également être nécessaire d’installer des composants depuis internet, donc une connexion est requise. Il est également possible de faire une installation totalement offline, pour ceci, il faudra modifier la cible de l’image dans le manifeste de déploiement.

Déploiement d’une machine virtuelle

Notes 

  • Certaines distributions de cloud providers ne permettent pas de profiter de toutes les fonctionnalités sans un minimum de customisation. Le paramétrage suivant a uniquement vocation à être utilisé pour un poc.
  • Google ne permet pas la communication par websocket entre le client de KubeVirt, virtctl, et l’opérateur KubeVirt. Le déploiement est fonctionnel mais il n’est pas possible d’utiliser le client virtctl pour gérer ni communiquer avec les machines virtuelles.

Mise en place

À partir de là, on utilise un client kubectl connecté à notre cluster kubernetes, on applique les commandes suivantes :

export RELEASE=$(curl https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt)
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/kubevirt-operator.yaml
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/kubevirt-cr.yaml
kubectl -n kubevirt patch kubevirt kubevirt --type=merge --patch '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true}}}}'
kubectl -n kubevirt wait kv kubevirt --for condition=Available --timeout=-1s

Une fois que le client kubectl nous rend la main, nous pouvons alors créer des machines virtuelles… enfin presque.

Installation du client

Afin de pouvoir gérer nos ressources kubevirt, nous allons avoir besoin d’un client, virtctl, permettant de réaliser des opérations particulières sur nos ressources. On peut l’installer depuis la source:

wget -O /usr/local/sbin/virtctl https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/virtctl-${RELEASE}-linux-amd64 && chmod +x /usr/local/sbin/virtctl

Ou alors sous la forme d’un plugin kubectl avec Krew:

kubectl krew install virt

On pourra alors accéder à toutes nos commandes virtctl via “kubectl virt”

Création d’une instance

Nous allons maintenant pouvoir créer une instance de machine virtuelle. Bien entendu, nous allons avoir besoin d’images conteneurisées compatibles avec KubeVirt. On peut retrouver certaines de ces images sur Quay, appelées containerdisks. On peut bien entendu créer ses propres images et les stocker dans ses propres dépôts, mais c’est en dehors du cadre de ce tutoriel.

Les images qui peuvent être utilisées par KubeVirt sont des images cloud-init conteneurisées supportant leur configuration initiale via Cloud-init.

Nous allons avoir besoin d’une clef ssh pour accéder à notre machine virtuelle. Ceci peut être accompli avec une configuration cloud-init, ou, plus flexiblement pour des opérations annexes, un script shell. 

Ici, nous générons une paire de clefs ssh que nous injectons dans un script de démarrage.

ssh-keygen -q -t rsa -N '' -f ./id_rsa <<<y >/dev/null 2>&1
VIRTUAL_MACHINE_SSH_KEY=$(cat ./id_rsa.pub)
NEW_USER="wescale"
cat << EOF > startup-script.sh
#!/bin/bash
export NEW_USER="wescale"
export SSH_PUB_KEY="${VIRTUAL_MACHINE_SSH_KEY}"
sudo adduser -U -m $NEW_USER
sudo mkdir /home/$NEW_USER/.ssh
sudo echo "\$SSH_PUB_KEY" > /home/$NEW_USER/.ssh/authorized_keys
sudo chown -R ${NEW_USER}: /home/$NEW_USER/.ssh
EOF

Afin de pouvoir créer une readiness probe qui nous permettra d’avoir l’état de démarrage de notre machine virtuelle, nous allons créer un serveur http léger pour valider que notre installation s’est bien déroulée à la fin de notre script.

cat << EOF >> startup-script.sh
dnf install -y nmap-ncat
systemd-run --unit=httpserver nc -klp 1500 -e '/usr/bin/echo -e HTTP/1.1 200 OK\\\nContent-Length: 12\\\n\\\nHello World!'
EOF

On peut bien entendu étendre notre script de configuration pour ajouter des droits sudoers et configurer un mot de passe pour notre utilisateur par exemple :

USER_PASSWORD="unmotdepasse"
USER_NAME="wescale"
cat << EOF >> startup-script.sh
echo -e "$USER_PASSWORD\n$USER_PASSWORD" | (passwd --stdin $USER_NAME)
echo "$USER_NAME  ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/username
EOF

Nous pouvons maintenant créer un secret depuis notre script (attention, les objets kubernetes ne peuvent généralement pas excéder 1Mo) :

kubectl create secret generic my-vmi-secret-config 
--from-file=userdata=startup-script.sh

Nous allons maintenant créer le manifeste de notre instance :

cat << EOF > ma-machine-virtuelle.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachineInstance
metadata:
  name: ma-machine-virtuelle
spec:
  terminationGracePeriodSeconds: 0
  domain:
   resources:
     requests:
        memory: 1024M
   devices:
     disks:
      - name: containerdisk
       disk:
         bus: virtio
      - name: cloudinitdisk
       disk:
         bus: virtio
 readinessProbe:
   initialDelaySeconds: 420
   periodSeconds: 20
    timeoutSeconds: 10
   failureThreshold: 10
   successThreshold: 1
   httpGet:
     port: 1500
  volumes:
    - name: containerdisk
     containerDisk:
        image: quay.io/containerdisks/centos-stream:9
    - name: cloudinitdisk
     cloudInitNoCloud:
       secretRef:
          name: my-vmi-secret-config
EOF

Et le déployer dans notre cluster Kubernetes:

kubectl create -f ma-machine-virtuelle.yaml

On va maintenant attendre que la machine virtuelle s’installe et que le script de post-installation s'exécute:

kubectl wait vmis/ma-machine-virtuelle --for=condition=Ready --timeout=10m

Nous pouvons maintenant y accéder en ssh au travers du client virtctl:

virtctl ssh wescale@ma-machine-virtuelle -i /chemin/vers/clef/ssh

Ou, si on souhaite utiliser un client ssh, on peut ajouter ces lignes dans notre configuration ssh:

Host vmi/*
  ProxyCommand virtctl port-forward --stdio=true %h %p
Host vm/*
  ProxyCommand virtctl port-forward --stdio=true %h %p

On pourra alors accéder à notre machine virtuelle en préfixant notre cible par vmi/ ou vm/.

Exemple : ssh vmi/wescale@ma-machine-virtuelle.namespace -i /chemin/vers/clef/ssh

Opérations de maintenance

Une fois la machine virtuelle créée, il est nécessaire de disposer d’outils pour gérer son cycle de vie. Nous allons maintenant nous concentrer sur les opérations de maintenance basique que nous pouvons effectuer sur les machines virtuelles.

Quelques commande utiles

Afin de pouvoir réaliser les opérations standard de maintenance de nos machines virtuelles, nous avons les commandes suivantes :

Pour démarrer la machine virtuelle :

virtctl start my-vm

Pour arrêter la machine virtuelle :

virtctl stop my-vm

On peut mettre la machine virtuelle en pause si besoin :

virtctl pause ma-machine-virtuelle

et sortir de la pause avec

virtctl unpause ma-machine-virtuelle

Enfin, on peut supprimer la machine virtuelle avec kubectl :

kubectl delete vmi ma-machine-virtuelle

Migration de noeud

Il peut arriver qu’il soit nécessaire de changer notre machine virtuelle de machine hôte. Dans un contexte de conteneurs, cette opération se manifeste généralement par la suppression des pods sur un nœud du cluster Kubernetes pour sa recréation sur un autre.

Dans le cas de machines virtuelles, il est préférable d’avoir l’option de migrer de noeud sans pour autant ni supprimer ni éteindre la machine, on appelle cette opération la live migration.

Il existe cependant quelques prérequis:

  • Le stockage de nos machines virtuelles doit être sur des volumes partagés sur les nœuds du cluster Kubernetes. De plus, l’accès du volume doit être en accès ReadWriteMany.
  • Il est nécessaire d’activer une feature de KubeVirt nommé LiveMigration.

Pour activer la feature LiveMigration, il faut appliquer les commandes suivantes:

cat << EOF > enable-feature-gate.yaml
---
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
 name: kubevirt
 namespace: kubevirt
spec:
configuration:
  developerConfiguration: 
    featureGates:
       - LiveMigration
EOF
kubectl apply -f enable-feature-gate.yaml

Nous pouvons maintenant migrer nos machines.

Pour cela, soit on doit créer un objet de type VirtualMachineInstanceMigration :

apiVersion: kubevirt.io/v1
kind: VirtualMachineInstanceMigration
metadata:
 name: migration-job
spec:
 vmiName: ma-machine-virtuelle

Ou, alors depuis virtctl :

virtctl migrate ma-machine-virtuelle

De plus, pour les opérations de maintenance du cluster Kubernetes, nous pouvons ajouter un paramètre dans la spec de notre objet VirtualMachine pour automatiser une migration dans le cas de la maintenance sur le nœud sur lequel la machine virtuelle se trouve.

Pour ceci on peut créer un fichier patch.yaml :

spec:
  evictionStrategy: LiveMigrate

Et l’appliquer avec:

kubectl patch vmi ma-machine-virtuelle --patch-file patch.yaml

Conclusion

KubeVirt est une solution supplémentaire pour augmenter la palette d'outils disponible pour créer nos infrastructures. Les possibilités de KubeVirt vont encore plus loin, et ne cessent de grandir à chaque version. C’est aujourd’hui une possibilité à étudier lors du déploiement de ses applications, qui ne correspondra pas à tous les cas d’usage mais mérite d’être considérée. Et qui sait, peut-être que demain, votre hyperviseur… ce sera KubeVirt.