Étape 6 : kube-vip

Introduction

La mise en place de HAproxy et Keepalived dans l’article précédent a permis de traiter notre besoin d’exposer des applications exécutées sur le cluster a l’extérieur de ce dernier.

Il reste néanmoins un accès que nous n’avons pas encore traité : l’API de K8S.

Kubernetes est API centrique, toute action de configuration et de déploiement une fois le cluster en place passe par le composant API.

Dans une architecture classique, comme c’est le cas ici, celui-ci est exécuté sur chaque control plane afin d’avoir une résilience associée.

Son accès est effectivement critique, tout comme la base etcd qui est utilisée en backend.

Il faut donc pouvoir associer un mécanisme réseau de HA lié à la connectivité de cette API.

Dans mon précédent cookbook, la logique était la même que pour les applications, à savoir l’usage de HAproxy.

Mais cette fois-ci c’est une autre solution qui va être retenue : kube-vip

Kube-Vip

kube-vip est un projet open source conçu pour fournir une adresse IP virtuelle (VIP) hautement disponible et des fonctionnalités d'équilibrage de charge (Load Balancing) pour les clusters Kubernetes.

Page d'accueil du projet open source kube-vip

Cliquez sur l'image pour l'agrandir.

Il est particulièrement adapté à des infrastructures onprem où l’on ne peut pas se reposer sur des intégrations natives propres aux services managés Kubernetes disponible chez certains opérateurs cloud.

L’avantage d’exploiter kube-VIP au lieu d’un déploiement de HAproxy est qu’il simplifie les dépendances liées à la pile d’outils externes. C’était d’ailleurs l’objectif de son créateur, Dan Finneran, qui, dans le but de rendre plus simple le déploiement de K8s en baremetal, a publié son projet début 2020. Celui-ci a été officiellement accepté par la CNCF (Cloud Native Computing Foundation) en juin 2021.

Il est écrit en go, s’exécute directement à l’intérieur du cluster, sous forme de pod, ce qui rend son déploiement et son monitoring plus simple, puisqu’identique à n’importe quelles autres applications sous k8S.

Fonctionnement

Kube-VIP peut fonctionner en deux modes distincts :

  • Mode ARP (Address Resolution Protocol)(Niveau 2) : C'est le mode le plus simple à déployer sur un réseau local. kube-vip utilise un mécanisme d'élection de leader (leader election). Un seul nœud du cluster est élu et s'attribue l'IP virtuelle. Il émet alors un broadcast Gratuitous ARP pour informer les switchs du réseau que cette IP est désormais associée à son adresse MAC. Si ce nœud tombe, une nouvelle élection a lieu en quelques secondes, et le nouveau leader diffuse un nouvel ARP pour récupérer le flux réseau.
  • Mode BGP (Border Gateway Protocol)(Niveau 3) : Dans ce mode, tous les nœuds équipés de kube-vip annoncent l'IP virtuelle aux routeurs de l’infrastructure via le protocole BGP. Le routeur réseau (et non plus le cluster lui-même) se charge de distribuer le trafic entre les différents nœuds, généralement grâce au routage ECMP (Equal-Cost Multi-Path). Cela permet un véritable équilibrage de charge multi-nœuds et supprime le goulot d'étranglement du leader unique présent dans le mode ARP

Dans le cas de ce cookbook c’est le mode ARP qui est retenu, car plus basique et plus rapide à mettre en œuvre.

Déploiement

Comme pour les articles précédents, on va exploiter Ansible pour générer la configuration nécessaire et la déployer.

Prérequis

Avant toute chose, étant donné qu’on va entamer les étapes liées aux clusters k8S lui-même, il faut s’assurer d’avoir l’enregistrement DNS associé à l’API opérationnel.

Résolution DNS du cluster k8S xkub.coolcorp.priv vers la VIP

Cliquez sur l'image pour l'agrandir.

N’hésitez pas à revoir l’architecture proposée en étape 0 pour plus d’informations, mais nous concernant l’API du cluster « xkub.coolcorp.priv » est lié à l’API 192.168.10.121 qui n’est autre que la VIP qui va être gérée par Kube-vip.

Après avoir établi cette condition initiale, on peut entamer l’examen du playbook Ansible.

Présentation du playbook

Celui-ci se nomme xkub_30_cluster.yml

---

# Ordre strict, chaque play doit terminer sur TOUS ses hôtes avant le suivant :
#   1. kube-vip sur les 3 control planes (AVANT tout kubeadm init/join)
#   2. kubeadm init sur le seul control plane primaire
#   3. kube-vip rebasculé sur admin.conf (fin du bootstrap)
#   4. kubeadm join (CP2/CP3 puis workers) + labels/taints de tous les nœuds
#   5. Cilium (Helm), depuis le control plane primaire

- name: kube-vip — VIP API server en static pod 
  hosts: k8s_controlplane_lan
  become: true
  roles:
    - xkub_kube_vip

- name: kubeadm init — control plane primaire
  hosts: "{{ groups['k8s_controlplane_lan'] | sort | first }}"
  become: true
  roles:
    - xkub_kubeadm_init

- name: kube-vip — retour au kubeconfig admin après le bootstrap
  hosts: "{{ groups['k8s_controlplane_lan'] | sort | first }}"
  become: true
  roles:
    - xkub_kube_vip

- name: kubeadm join — control planes additionnels, workers, labels et taints
  hosts: k8s_controlplane_lan:k8s_workers_lan:k8s_workers_web:gpu
  become: true
  roles:
    - xkub_kubeadm_join

- name: Cilium — CNI avec kubeProxyReplacement (chart Helm depuis le control plane primaire)
  hosts: "{{ groups['k8s_controlplane_lan'] | sort | first }}"
  become: true
  roles:
    - xkub_cilium

playbooks/xkub_30_cluster.yml

Il va appliquer plusieurs rôles qui vont s’enchainer

  • xkub_kube_vip : le rôle propre à kube-vip, a noter que celui-ci n’est rattaché qu’au groupe k8s_controlplane_lan, soit l’ensemble des control plane.
  • Xkub_kubeadm_init : le rôle permettant d’initier la création du cluster. Celui-ci ne sera pas vu ici, mais dans un prochain article.
  • xkub_kube_vip : le role est rappelé une seconde fois par le playbook, vous comprendrez pourquoi à la fin de cet article.
  • xkub_kubeadm_join : le role en charge de joindre les nœuds au cluster.
  • xkub_cilium : le role en charge de déployer Cilium comme CNI (Container Network Interface).

Role xkub_kube_vip

Faisons le focus sur le rôle xkub_kube_vip

Les variables

On commence par le répertoire defaults, avec le classique main.yml

---
# Rôle xkub_kube_vip — VIP de l'API server (kube-apiserver :6443) en static pod,
# mode ARP
# Version épinglée dans inventory/group_vars/clu_k8s_xkub/versions.yml (kube_vip_version).

xkub_kube_vip_image: "ghcr.io/kube-vip/kube-vip:v{{ kube_vip_version }}"

# Interface portant la VIP API. Auto-détectée via les facts Ansible
xkub_kube_vip_interface: "{{ ansible_default_ipv4.interface }}"

# kube-vip ne gère QUE la VIP de l'API server
# LoadBalancer restent hors périmètre — Traefik est exposé par les paires
xkub_kube_vip_enable_services: false

xkub_kube_vip_manifest_path: /etc/kubernetes/manifests/kube-vip.yaml

# Kubeconfig monté dans le static pod ur
# Kubernetes >= 1.29". En régime établi c'est admin.conf ; pendant le tout
# premier `kubeadm init`, admin.conf n'est pas encore utilisable (ses bindings
# RBAC n'existent pas), il faut donc super-admin.conf. Le rôle choisit tout
# seul entre les deux selon l'état réel du nœud (tasks/01_static_pod.yml).
xkub_kube_vip_kubeconfig_bootstrap: /etc/kubernetes/super-admin.conf
xkub_kube_vip_kubeconfig_steady: /etc/kubernetes/admin.conf

roles/xkub_kube_vip/defaults/main.yml

C’est ici que toutes les variables nécessaires vont être paramétrées.

Certaines, comme xkub_kube_vip_image, vont elle-même dépendre de variable de plus haut niveau, notamment kube_vip_version qui se retrouve défini dans le fichier versions.yml associé au groupe clu_k8s_xkub dans le dossier inventory\group_vars\clu_k8s_xkub, soit 1.2.1

N’hésitez pas à revoir l’étape 3 du cookbook qui définit la logique Ansible retenue et le choix du placement des variables.

La variable xkub_kube_vip_interface pointe elle vers une variable système afin de récupérer l’IP du node sur lequel la configuration s’effectue. En effet, nous utiliserons l’adresse du serveur comme interface de base pour kube-vip

xkub_kube_vip_enable_services est à false car kube-vip ne sera utilisé que pour la VIP d’API par pour d’autres besoins, ou c’est Traefik qui s’occupera de ça plus tard.

Xkub_kube_vip_manifest_path est une variable intéressante et importante à comprendre.

Focus sur composant Kubernetes et static pod

Cela va nous permettre de faire un petit écart dans l’article pour expliquer un principe de fonctionnement de Kubernetes.

K8S fonctionne sous forme de composants disposant chacun de rôle prédéfini, comme

  • kube-apiserver (Le serveur d'API). C’est la "porte d'entrée" du cluster. C’est elle qui nous intéresse spécifiquement ici. C'est le seul composant avec lequel vous (via kubectl) et les autres composants communiquez directement. Il valide et configure les données pour les objets API (Pods, Services, etc.).
  • Etcd : C'est la base de données de Kubernetes. Il s'agit d'un magasin clé-valeur cohérent et hautement disponible. Il stocke l'intégralité de l'état du cluster (qui tourne où, quelle configuration est appliquée, etc.).
  • kube-scheduler : C'est le planificateur. Il surveille les nouveaux Pods créés qui n'ont pas encore été assignés à un nœud, et choisit le meilleur nœud pour les exécuter en fonction des ressources demandées, des contraintes matérielles ou des règles d'affinité.
  • kube-controller-manager : C'est le gestionnaire de contrôleurs. Il exécute des boucles de contrôle en arrière-plan qui surveillent l'état partagé du cluster (via l'API server) et tentent de faire correspondre l'état actuel à l'état désiré (par exemple, si un nœud tombe, le Node Controller le détecte et recrée les Pods manquants ailleurs).

Le principe même d’un cluster K8S dit Vanilla comme on le fait ici, est de récupérer ces différents composants pour les exécuter sur des nodes cibles.

Kube-vip va être un composant additionnel, mais qui va s’appuyer pour son démarrage sur le même mécanisme que la plupart de ces composants de bases, soit à travers ce qu’on appelle des statics pod.

La plupart des composants cités, dont kube-VIP, fonctionnent sous forme de conteneur et sont donc exécutés dans un pod. Mais, à ce stade du déploiement, le cluster k8S n’est pas initialisé. Comment soumettre un pod kube-VIP alors que justement, le kube-apiserver qui devrait être sollicité n'est pas encore actif ?

C’est là qu’entrent en jeu nos fameux kubelet déployés dans l’étape 4 de ce cookbook. Disponible sur chaque nœud sous forme de service, il possède la capacité d’exécuter des pods se trouvant dans un répertoire spécifique : les statics pods.

En faite chaque définition de pod visible dans ce dossier par un kubelet est automatiquement exécutée dès lors que les conditions le permettent (runtime de container disponible..)

C’est comme ça que vont pouvoir être initiés les premiers composants du cluster, qui, eux même, pourront par la suite être sollicités pour exécuter les objets Kubernetes souhaités et rentrer dans une logique de déploiement classique.

Kube-vip va hériter de ce mécanisme et c’est pourquoi son manifeste de configuration va être copié dans /etc/kubernetes/manifests/kube-vip.yaml qui n’est autre que la valeur de la variable xkub_kube_vip_manifest_path soit l’emplacement surveillé par les kubelet pour exécuter les statics pod.

En résumé kube-vip porte la VIP de l'API server soit l’ip 192.168.10.121 sur le port 6443 (voir architecture). Il est déployé en static pod, un manifeste posé directement dans /etc/kubernetes/manifests/, que kubelet démarre tout seul, sans passer par l'API server.

Le kubelet d'un control plane lit le manifeste kube-vip.yaml dans /etc/kubernetes/manifests et lance le pod kube-vip, qui monte admin.conf ou super-admin.conf

Cliquez sur l'image pour l'agrandir.

Les deux autres variables xkub_kube_vip_kubeconfig_bootstrap et xkub_kube_vip_kubeconfig_steady sont également intéressantes, mais je ne vais pas rentrer dans le détail maintenant. On va comprendre leur importances juste après dans l’article quand on parlera du manifeste de kube-vip.

Les tasks

On peut enchainer avec le répertoire task.

Comme d’habitude le fichier main.yml ne fait que référencer les autres taches.

---
# Rôle xkub_kube_vip — VIP API server en static pod
# control planes AVANT xkub_kubeadm_init/xkub_kubeadm_join (playbooks/xkub_30_cluster.yml).
- name: Static pod kube-vip (VIP API server)
  ansible.builtin.import_tasks: 01_static_pod.yml
  tags:
    - xkub
    - xkub.kube_vip
    - xkub.kube_vip.static_pod

roles/xkub_kube_vip/tasks/main.yml

Ce qui est relativement simple ici, puisque seule la tache 01_static_pod.yml est présente.

---
- name: Créer le répertoire des manifestes statiques kubelet
  ansible.builtin.file:
    path: /etc/kubernetes/manifests
    state: directory
    owner: root
    group: root
    mode: "0755"

# Le kubeconfig à monter dépend de l'état RÉEL du nœud, pas d'un ordre supposé
# des plays : admin.conf n'existe qu'une fois `kubeadm init` (primaire) ou
# `kubeadm join --control-plane`  passé. Tant qu'il manque sur le
# control plane primaire, on est en phase de bootstrap et il faut monter
# super-admin.conf. Baser la décision sur ce stat rend le rôle
# rejouable : sur un cluster déjà formé il rebascule de lui-même vers
# admin.conf et ne produit plus aucun changement.
- name: Vérifier si ce control plane dispose déjà d'un kubeconfig admin
  ansible.builtin.stat:
    path: "{{ xkub_kube_vip_kubeconfig_steady }}"
  register: xkub_kube_vip_admin_conf

- name: Déterminer le kubeconfig à monter (bootstrap ou régime établi)
  ansible.builtin.set_fact:
    xkub_kube_vip_kubeconfig_path: >-
      {{ xkub_kube_vip_kubeconfig_bootstrap
         if (not xkub_kube_vip_admin_conf.stat.exists
             and inventory_hostname == xkub_primary_control_plane)
         else xkub_kube_vip_kubeconfig_steady }}

# Piège connu  : juste après ce dépôt, et tant
# que `kubeadm init`/`kubeadm join --control-plane` n'a pas écrit le
# kubeconfig, le conteneur kube-vip tourne en CrashLoopBackOff (kubeconfig
# vide). C'est attendu : kubelet le relance automatiquement et le pod devient
- name: Déployer le static pod kube-vip (VIP API {{ k8s_api_vip }}:{{ k8s_api_port }})
  ansible.builtin.template:
    src: kube-vip.yaml.j2
    dest: "{{ xkub_kube_vip_manifest_path }}"
    owner: root
    group: root
    mode: "0644"

roles/xkub_kube_vip/tasks/01_static_pod.yml

Celle-ci a plusieurs objectifs.

Le cluster Kubernetes n’étant pas créé à cette étape, il faut s’assurer que le dossier de lecture des manifests de static pod dont nous avons parlé précédemment soit présent. (/etc/kubernetes/manifests)

Mais ce n’est pas tout et c’est à ce moment qu’on va parler du rôle des variables xkub_kube_vip_kubeconfig_bootstrap et xkub_kube_vip_kubeconfig_steady laissé de côté.

Dès son lancement, kube-VIP doit immédiatement se connecter au serveur API. Pour ce faire, il recherche un fichier kubeconfig, qui est généralement créé lors du déploiement d’un cluster Kubernetes et qui permet une interaction avec ce dernier. C’est un élément qu’on verra plus tard avec l’exécution de la commande kubeadm init qui sera exécutée par le rôle xkub_kubeadm_init.

kube-vip doit également s'authentifier auprès de l'API pour placer le verrou d’élection de leader. En fait il dépose un lease, soit un type d’objet standard K8S (coordination.k8s.io/v1) pour savoir qui est le gestionnaire de l’API parmi tous les control plane à instant t.

Pour réaliser cette action, kube-vip doit disposer de droits importants et s’authentifier auprès de l’API avec un rôle d’administrateur du cluster.

Or, pour l'instant, non seulement l’API Server n’existe pas, mais en plus, lorsque nous passerons à l’initialisation du cluster plus tard, ce processus ne sera pas encore terminé. En effet, le rôle attendu par Kube-VIP n’est pas encore disponible, car il est défini a la fin de la phase de démarrage du cluster, que nous appelons le bootstrap. Bootstrap qui passe par l’accès à l’API….Qui sera dépendante de kube-vip…qui lui-même ne sera pas disponible, car pas en capacité de s’authentifier….On arrive au serpent qui se mord la queue avec le risque que les étapes prochaines où l’on initie le cluster échouent.

Boucle de dépendance entre kube-vip, admin.conf et kubeadm init, contournée en montant super-admin.conf

Cliquez sur l'image pour l'agrandir.

Mais… depuis Kubernetes 1.29, kubeadm init génère deux kubeconfig admin

  • admin.conf : créer à la fin de l’init du cluster avec tous les rôles par défaut disponible.
  • super-admin.conf : fichier dont le superutilisateur est « codé » en dure dans l’API, sans notion de rôle.

super-admin.conf est donc une sorte de fichier de secours, extrêmement critique capable de bypasser tout le mécanisme RBAC (Role Base Access) disponible dans K8S (n’hésitez pas à parcourir mon article sur le sujet).

C’est exactement ce qu’il nous faut pour k8s-vip…mais uniquement lors du boostraping du cluster…par la suite, une fois que tous les nodes seront en place, il n’y aura plus besoin de passer par super-admin.conf et on pourra revenir à admin.conf

C’est tout le principe des actions défini dans 01_static_pod.yml : rendre dynamique le manifeste de k8s-vip en fonction de l’état du cluster. N’oublions pas qu’un playbook Ansible doit pouvoir être rejoué à n’importe quel moment.

Vous comprenez maintenant pourquoi le playbook xkub_30_cluster.yml fait référence 2x au role xkub_kube_vip. Une fois le cluster initié, on relance la configuration de kube-vip pour qu'il bascule sur le fichier admin.conf.

Enfin, retenez bien qu’a ce stade, malgré ce que je viens de décrire, le pod kube-vip sera en échec tant que kubeadm init n’aura pas été lancé, car même super-admin.conf n'existe pas pour l’instant.

Le kubelet re-essaye de toute façon en boucle d’exécuter le manifeste. Dès que les conditions seront réunies, kube-vip sera disponible automatiquement.

Les états possibles au démarrage de kube-vip selon la présence d'un kubeconfig : crash et relance par kubelet, ou annonce de la VIP via l'objet lease

Cliquez sur l'image pour l'agrandir.

Les templates

Ne reste plus qu’a traité du dossier templates dans lequel on va retrouver le fichier kube-vip.yaml.j2

# {{ ansible_managed }}
# Static pod kube-vip — VIP API server {{ k8s_api_vip }}:{{ k8s_api_port }}.
# Déposé AVANT `kubeadm init` / `kubeadm join --control-plane` : kubelet
# démarre ce pod dès qu'il lit son staticPodPath (/etc/kubernetes/manifests),
# indépendamment de l'état du reste du control plane.
apiVersion: v1
kind: Pod
metadata:
  name: kube-vip
  namespace: kube-system
spec:
  containers:
    - name: kube-vip
      image: {{ xkub_kube_vip_image }}
      imagePullPolicy: IfNotPresent
      args:
        - manager
      env:
        - name: vip_arp
          value: "true"
        - name: port
          value: "{{ k8s_api_port }}"
        - name: vip_interface
          value: "{{ xkub_kube_vip_interface }}"
        - name: vip_subnet
          value: "32"
        - name: vip_ddns
          value: "false"
        - name: cp_enable
          value: "true"
        - name: cp_namespace
          value: kube-system
        - name: svc_enable
          value: "{{ xkub_kube_vip_enable_services | bool | lower }}"
        - name: vip_leaderelection
          value: "true"
        - name: vip_leasename
          value: plndr-cp-lock
        - name: vip_leaseduration
          value: "5"
        - name: vip_renewdeadline
          value: "3"
        - name: vip_retryperiod
          value: "1"
        - name: address
          value: "{{ k8s_api_vip }}"
      securityContext:
        capabilities:
          add:
            - NET_ADMIN
            - NET_RAW
            - SYS_TIME
      volumeMounts:
        - mountPath: /etc/kubernetes/admin.conf
          name: kubeconfig
  hostAliases:
    - hostnames:
        - kubernetes
      ip: 127.0.0.1
  hostNetwork: true
  volumes:
    # Le chemin CÔTÉ HÔTE varie selon la phase (bootstrap : super-admin.conf,
    # régime établi : admin.conf), alors que le chemin monté DANS le conteneur
    # reste toujours /etc/kubernetes/admin.conf — c'est celui que kube-vip lit.
   
    # type: FileOrCreate est le second point important : au tout premier
    # démarrage, ce hostPath n'existe pas encore. Sans ce type explicite, le
    # runtime crée un RÉPERTOIRE vide à cet emplacement pour satisfaire le
    # montage, ce qui fait ensuite échouer kubeadm (impossible d'écrire un
    # fichier sur un chemin occupé par un répertoire). FileOrCreate garantit
    # qu'un FICHIER vide est créé, que kubeadm remplace normalement ensuite.
    - hostPath:
        path: {{ xkub_kube_vip_kubeconfig_path }}
        type: FileOrCreate
      name: kubeconfig

roles/xkub_kube_vip/templates/kube-vip.yaml.j2

C’est la définition de notre fameux manifeste pour kube-vip. C’est lui qui une fois placé dans le dossier /etc/Kubernetes/manifests de chaque control plane, va pouvoir être lus par le service kubelet et exécuté en tant que static pod.

L’image du conteneur kube-vip telle que définie dans la variable vu précédemment, va être récupérée puis lancer avec les paramètres qu’on souhaite, dont l’API port du cluster (6443) et définition de notre VIP en 192.168.10.121 dont la résolution que nous avons configurée en début d’article pointe vers xkub.coolcorp.priv.

Nous sommes en mode arp via l’option définie en env vip_arp à true.

On définie également le nom de l’objet lease ainsi que d’autres éléments dont le nom est assez parlant.

Le point le plus marquant du template est l’utilisation de la variable xkub_kube_vip_kubeconfig_path, qui représente la valeur à utiliser pour monter soit admin.conf, soit super-admin.conf dans le contexte du conteneur de kube-VIP pour qu’il puisse savoir comment s’adresser à l’API Server. C’est tout l’objet de ce qu’on a vu précédemment et de la tache 01_static_pod.yml qui fini par copier le template avec les bonnes valeurs vers les serveurs control plane.

Comme évoqué plus haut, ce template sera redeployé à la seconde itération du role xkub_kube_vip qu’une fois le cluster en place, kube-vip puisse être configuré avec l’emplacement définitif du fichier de conf.

Conclusion

Nous ne lancerons pas le plabook maintenant. Avant, il va être nécessaire de parler des autres rôles, notamment xkub_kubeadm_init. Comme vous l’aurez compris, kube-vip et l’initialisation du cluster sont intimement liés et il est plus logique de les traiter l’un à la suite de l’autre dans un playbook unique.

Il reste important de comprendre le fonctionnement de kube-vip et d’avoir retenu les principes fondamentaux qu’on a pu décrire dans cet article au sujet du fonctionnement de Kubernetes.

C’est un parti pris de ma part que de dévoiler progressivement des parties théoriques importantes au fur et à mesure qu’on déroule les fichiers de configuration Ansible pour découvrir quelles actions vont être réalisées et pourquoi.

Si certains points sont encore flous, ils devraient être plus clairs lors des étapes suivantes. On se retrouve donc dans un prochain article pour le bootstrap du cluster.