Étape 4 : Prérequis système et réseau pour Kubernetes

Introduction

Lors du dernier article, nous avons introduit Ansible et présenté la logique retenue pour déployer les différentes configurations sur l’ensemble des nodes.

Nous avons également démarré l’analyse du premier playbook « xkub_10_base.yml » en rentrant dans le détail du premier rôle auquel il est associé : xkub_common

Ce dernier est relativement basique et centré sur les bases minimales autour de l’OS.

Il est temps maintenant d’enchainer sur le second rôle de ce playbook : xkub_k8s_prereqs

Chaque analyse de ce rôle va nous permettre d’illustrer les prérequis autour de Kubernetes.

Que vous utilisiez Ansible ou un paramétrage manuel, l’important va être de bien comprendre les configurations à mettre en place et quels effets elles auront sur K8S.

Role xkub_k8s_prereqs

Voici donc la présentation de l’arborescence du rôle xkub_k8s_prereqs. Celui-ci ne va s’appliquer qu’aux machines appartenant aux groupes k8s_controlplane_lan, k8s_workers_lan et k8s_workers_web, qui font partie de l’inventaire Ansible (voir l’article précédent).

Détail du rôle Ansible xkub_k8s_prereqs : defaults, handlers, tasks et templates

Cliquez sur l'image pour l'agrandir.

C’est donc tous les futurs node Kubernetes qui vont être impactés, les serveurs avec HAproxy n’étant pas concerné par ces prérequis.

defaults\main.yml

On commence par le fichier main.yml, situé dans le dossier defaults du rôle.

Voici son contenu

---
# Rôle xkub_k8s_prereqs — valeurs par défaut.
# Les versions (k8s_version, containerd_*) viennent de group_vars/clu_k8s_xkub/versions.yml,
# les ports de group_vars/clu_k8s_xkub/main.yml.

# Paquets sur TOUS les nœuds K8s (control planes, workers, GPU).
# iproute-tc : évite le warning kubelet "tc not found".
# crun + container-selinux : runtime OCI et politique SELinux (containerd est
# livré en binaire officiel, sans runtime OCI). RHEL/Rocky Linux 10 a RETIRÉ
# runc : crun (Podman 5, cgroups v2) est désormais le seul runtime OCI
# disponible — voir tasks/03_containerd.yml pour le pointage explicite du
# binaire dans /etc/containerd/config.toml (le générateur par défaut de
# containerd ne connaît que le nom "runc").
# conntrack-tools, ethtool, ebtables, socat : binaires exigés par les preflight
# checks de kubeadm (kubeadm init/join échoue si l'un d'eux est absent).
# cri-tools (crictl) n'est PAS ici : installé depuis le binaire officiel GitHub
# (voir tasks/03_containerd.yml), indisponible par défaut sur Rocky Linux 10.
xkub_k8s_prereqs_packages:
  - "kubeadm-{{ k8s_version }}"
  - "kubelet-{{ k8s_version }}"
  - nfs-utils
  - iproute-tc
  - crun
  - container-selinux
  - conntrack-tools
  - ethtool
  - ebtables
  - socat

# Paquets réservés aux control planes
xkub_k8s_prereqs_controlplane_packages:
  - "kubectl-{{ k8s_version }}"

# Modules noyau requis (containerd + CNI)
xkub_k8s_prereqs_kernel_modules:
  - overlay
  - br_netfilter

# Ports firewalld — décision D4 : firewalld RESTE ACTIF, ouvertures explicites.
# Communs à tous les nœuds K8s :
xkub_k8s_prereqs_fw_ports_common:
  - "{{ k8s_port_kubelet }}/tcp"
  - "{{ k8s_port_kube_proxy_health }}/tcp"
  - "{{ cilium_port_vxlan_udp }}/udp"
  - "{{ cilium_port_health }}/tcp"
  - "{{ cilium_port_hubble_peer }}/tcp"
  - "{{ cilium_port_hubble_relay }}/tcp"

# Control planes uniquement — API server. Il doit rester joignable largement
# depuis le LAN (kubelets des workers, kube-vip, poste d'administration) : on
# ne le restreint donc pas par source ici.
xkub_k8s_prereqs_fw_ports_controlplane:
  - "{{ k8s_api_port }}/tcp"

# etcd (2379 client, 2380 peer) est traité à part, en rich_rule restreinte aux
# seuls control planes (audit sécurité phase 3, constat 4). etcd contient TOUS
# les objets du cluster, Secrets compris : il ne doit jamais être exposé au
# reste du LAN (workers, HAProxy, nœud GPU). Le trafic légitime est uniquement
# control plane ↔ control plane, plus l'API server local via 127.0.0.1.
xkub_k8s_prereqs_fw_etcd_source_group: k8s_controlplane_lan

# Workers uniquement (NodePorts, dont ceux de Traefik) :
xkub_k8s_prereqs_fw_ports_worker:
  - "{{ k8s_port_nodeport_range }}/tcp"

# Groupes d'inventaire considérés comme workers pour le firewall
xkub_k8s_prereqs_worker_groups:
  - k8s_workers_lan
  - k8s_workers_web
  - gpu

roles/xkub_k8s_prereqs/defaults/main.yml

Les variables du roles

C’est ici qu’on va retrouver les différentes variables propres au rôle

Les paquets

À commencer par la liste des paquets à déployer « xkub_k8s_prereqs_packages ».

Deux variables sont elles-mêmes dérivées des variables de plus haut niveau, concernant la version de Kubernetes à déployer.

  • "kubeadm-{{ k8s_version }}"
  • "kubelet-{{ k8s_version }}"

En l’occurrence, au moment de cet article, la version ciblée de K8S est 1.36.2, elle est figée dans le fichier inventory\group_vars\clu_k8s_xkub\versions.yml.


---
# =============================================================================
# TOUTES les versions épinglées du volet Ansible .
# Une seule source : ne jamais mettre une version en dur dans un rôle.
# =============================================================================

# Kubernetes (dépôt pkgs.k8s.io — le dépôt mineur v1.36 est dérivé de cette valeur)
k8s_version: "1.36.2"
...
...

extrait de inventory\group_vars\clu_k8s_xkub\versions.yml

Kubeadm n’est autre que l’assistant d’installation du cluster Kubernetes. C’est lui qui va nous permettre d’initialiser le cluster, puis d’en assurer la maintenance, notamment sur les opérations d’upgrade.

Il va se charger de récupérer chaque image des différents composants de Kubernetes, puis de les paramétrer en fonction du fichier de configuration que nous lui passerons en arguments.

Kubeadm doit être disponible sur chaque node.

Kubelet joue le rôle d’agent au sein d’un cluster Kubernetes. Il doit être présent et exécuté en permanence sur chaque serveurr qui compose le cluster.

Il fait partie des rares composants K8S à ne pas être un conteneur, mais en service présent au niveau de l’OS.

Parmi ses taches, kubelet est en charge :

  • De s’assurer du démarrage des pods non crée par Kubernetes, soit les static pods. Il s’agit le plus souvent des composants Kubernetes eux-mêmes.
  • D’interagir avec le Container Runtime (CRI) (voir premier article). Il discute avec ce denier pour démarrer/arrêter ou inspecter les conteneurs.
  • De communiquer avec les composants type CNI(Container Network Interface)/CSI (Container Storage Interface) pour la gestion du stockage et du réseau (voir premier article)
  • D’ exécuter les règles de supervision configurée sur les pods (on verra ça plus tard)
  • De remonter périodiquement le statut du node sur lequel il est déployé.

D’autres paquets sont nécessaires sur l’ensemble des nœuds, comme nfs-utils pour la gestion des partages nfs et surtout crun.

Crun n’est autre que runc réécrit en C et en charge du runtime de plus bas niveau pour l’exécution des conteneurs (voir l’article d’introduction de ce cookbook)

Je ne détaillerais pas forcément les autres paquets, mais sachez qu’ils doivent être installés en amont du déploiement de K8S et concerne les OS type RedHat, comme Rocky Linux 10 qu’on utilise ici. L’usage d’autres familles Linux peut impacter cette liste.

La variable « xkub_k8s_prereqs_controlplane_packages » contient elle les paquets qui n’ont besoin que d’ être présent sur un contrôle plane. En l’occurrence, kubectl, la CLI d’usage de Kubernetes.

Après le déploiement de K8S, on utilise de base kubectl pour communiquer et agir avec le cluster.

Pour ce faire, l’outil kubectl se réfère à un fichier de configuration généré lors de la création du cluster. Ce fichier renferme les informations d’identification nécessaires pour accéder à l’API du cluster. N’hésitez pas à parcourir mon article spécifique au sujet au besoin.

Les modules

En dehors de paquets applicatifs spécifiques, il faut aussi s’assurer de la présence de certains modules dans le kernel Linux.

C’est le rôle de la variable xkub_k8s_prereqs_kernel_modules.

Elle permet de s’assurer de la présence du module overlay, soit le pilote système de fichier OverlayFS. C’est ce driver qui permet d’exploiter le système de couches propre aux conteneurs, c'est à dire l’empilement des strates en lecture seul utilisées par l’image du conteneur (Read Only Layer) avec l’ajout d’une couche en écriture (Read-Write Layer) .

Le second module est « br_netfilter ». Il permet au sous-système de filtrage réseau du noyau (Netfilter / iptables) de voir et d'inspecter le trafic qui passe à travers les ponts réseau virtuels (Linux Bridges).

Sans ce composant, les plugins CNI (Container Network Interface) ne peuvent fonctionner et donc on ne pourrait avoir de réseau entre les composants Kubernetes.

Notons qu’il est possible que certains paquets ou modules soient déjà présents par défaut dans l’installation de l’OS. Néanmoins les lister pour un déploiement via Ansible permet de s’assurer qu’ils doivent bien être présents et actifs.

Les ports et les flux réseau

On poursuit avec la variable xkub_k8s_prereqs_fw_ports_common.

Il s’agit de traiter ici les ports à ouvrir sur tous les serveurs Kubernetes.

La plupart sont véritablement décrits dans ce fichier inventory/group_vars/clu_k8s_xkub/main.yml.


---
# =============================================================================
# Variables du cluster xkub.coolcorp.priv — portée : groupe clu_k8s_xkub
# (tous les nœuds K8s + paires HAProxy). 
# =============================================================================

# 

k8s_api_port: 6443

# --- NodePorts Traefik -------------------------------------------------------
# SOURCE DE VÉRITÉ UNIQUE partagée entre les rôles xkub_haproxy_keepalived
# (backends) et xkub_traefik_instance (service NodePort). 
traefik_nodeport_lan_http: 30080
traefik_nodeport_lan_https: 30443
traefik_nodeport_dmz_http: 31080
traefik_nodeport_dmz_https: 31443

# --- Ports système Kubernetes------
k8s_port_kubelet: 10250
k8s_port_kube_proxy_health: 10256
k8s_port_etcd_range: 2379-2380
k8s_port_nodeport_range: 30000-32767
cilium_port_vxlan_udp: 8472
cilium_port_health: 4240
cilium_port_hubble_peer: 4244
cilium_port_hubble_relay: 4245

extrait de inventory/group_vars/clu_k8s_xkub/main.yml

Les ports sont soit liés à Kubernetes lui-même, soit au CNI Cilium. C’est là que le choix du CNI connait son premier impact. Les flux à ouvrir entre les nodes peuvent varier d’un CNI à un autre.

Pour Kubernetes on va trouver les ports propres au kubelet dont on a parlé précédemment, ainsi qu’au kube-proxy, un autre composant lié au réseau (mais dont le rôle va être repris par Cilium).

Le reste des ports concerne Cilium.

Une autre variable xkub_k8s_prereqs_fw_ports_controlplane touché quant à elle uniquement les ports propres au contrôle plane, soit le port par défaut 6443 pour l’API (composant qui tourne exclusivement sur les control plane)

(ici aussi le port est figé dans \inventory\group_vars\clu_k8s_xkub\main.yml)

La variable xkub_k8s_prereqs_fw_etcd_source_group est un peu particulière. Elle touche au port propre à la base, etcd.

Pour rappel, Kubernetes exploite une base clef/valeur, soit etcd par défaut, pour stocker les configurations des différents objets K8S.

Cette base est primordiale, puisque, sans elle, le cluster n’a pas de configuration et il ne peut pas fonctionner. C’est pourquoi son contenu est répliqué sur chaque contrôle plane. À l’instant T, un seul control plane est désigné comme maître, et c’est le contenu de sa base etcd qui est consulté par l’API, alors que les control plane répliquent le contenu de cette base sur leur propre stockage.

Donc, les ports ouverts pour les échanges entre instances, etcd ne sont à réaliser qu’entre les control plane. C’est donc une bonne pratique de sécurité de les filtrer avec les IPs des serveurs associés.

Pour cela, sous Rocky Linux, on va passer par des règles firewalld dites « rich_rule ».

Cet élément de sécurisation a été poussé par mon subagent claude « k8s-security-auditor.md » dédié à la cybersécurité. (voir l’étape 0 du cookbook). Un des intérêts d’exploiter l’IA ici et de combiner les subagents entre eux: celui dédié à Ansible et celui dédié à la sécurité.

En ce qui concerne les ports, nous arrivons à la variable « xkub_k8s_prereqs_fw_ports_worker », qui est spécifique à l’ouverture des ports sur les workers.

Moins de ports à ce niveau, on va se concentrer sur les ports traefik (puisque c’est certains workers qui vont porter le composant : voir l’architecture dans le premier article), ainsi que les nodesports, soit la manière de faire par défaut pour exposer un pods. Je ne rentre pas dans le détail à ce niveau, on en reparlera plus tard.

En résumé voici la matrice des flux et des ouvertures des ports.

Port / Proto Service Source du flux Destination Périmètre (nœuds & variable) Justification
10250/tcp kubelet API Control planes (kube-apiserver) Tous les nœuds K8s (LAN + DMZ + GPU) Tous les nœuds
xkub_k8s_prereqs_fw_ports_common
Permet à l'API server de piloter le kubelet du nœud : kubectl logs / exec / port-forward, remontée des métriques. Sans ce port, le nœud rejoint le cluster mais devient inexploitable à distance. Flux LAN → DMZ à autoriser aussi sur les firewall externes (OPNsense me concernant)
10256/tcp kube-proxy healthz HAProxy LAN et DMZ (sondes), nœuds du cluster Tous les nœuds K8s Tous les nœuds
xkub_k8s_prereqs_fw_ports_common
Point de santé de kube-proxy. C'est ce que sondent les HAProxy pour ne router le trafic NodePort que vers des nœuds réellement capables de le traiter.
8472/udp VXLAN (overlay Cilium) Tous les autres nœuds K8s Tous les nœuds K8s Tous les nœuds
xkub_k8s_prereqs_fw_ports_common
Tunnel de l'overlay Cilium : tout le trafic pod-à-pod inter-nœuds y transite. Doit impérativement être autorisé sur les firewall entre LAN et DMZ, faute de quoi les pods des workers DMZ sont coupés du reste du cluster.
4240/tcp Cilium health Tous les autres nœuds K8s Tous les nœuds K8s Tous les nœuds
xkub_k8s_prereqs_fw_ports_common
Sondes cilium-health de nœud à nœud. Alimente cilium status et cilium connectivity test : sans ce port, le CNI fonctionne mais se déclare dégradé.
4244/tcp Hubble peer hubble-relay (pods du cluster) Tous les nœuds K8s Tous les nœuds
xkub_k8s_prereqs_fw_ports_common
Service de découverte des pairs Hubble : hubble-relay s'y connecte pour s'abonner aux flux observés par chaque agent Cilium.
4245/tcp Hubble relay Clients Hubble (CLI, UI) et pods du cluster Tous les nœuds K8s Tous les nœuds
xkub_k8s_prereqs_fw_ports_common
Point d'accès agrégé aux flux réseau du cluster (observabilité). Ouvert par anticipation de l'activation de Hubble.
6443/tcp kube-apiserver kubelets de tous les workers (LAN, DMZ, GPU), kube-vip, poste d'administration Control planes .161 / .162 / .163 et VIP API 192.168.10.121 Control planes uniquement
Condition : 'k8s_controlplane_lan' in group_names
Point d'entrée unique de l'API Kubernetes. Volontairement NON restreint par source : il doit rester joignable largement depuis le LAN (kubelets des deux zones, VIP kube-vip, administration). Le contrôle d'accès se fait ici par l'authentification et le RBAC de l'API, pas par le firewall.
2379-2380/tcp etcd (2379 client, 2380 peer) Les AUTRES control planes uniquement — adresses /32 (rich_rule firewalld) Control planes .161 / .162 / .163 Control planes uniquement
Boucle sur le groupe k8s_controlplane_lan
Seule ouverture restreinte par source du rôle. etcd contient l'intégralité des objets du cluster, Secrets compris (en clair tant que le chiffrement at-rest n'est pas activé). Une ouverture large donnerait à tout worker ou nœud compromis un accès direct à l'état complet du cluster. Le trafic légitime est uniquement control plane ↔ control plane, l'API server local passant par 127.0.0.1.
30000-32767/tcp Plage NodePort (dont Traefik) HAProxy LAN .185 / .186 → workers LAN
HAProxy DMZ .185 / .186 → workers DMZ
Workers LAN, workers DMZ, nœud GPU Workers uniquement
k8s_workers_lan, k8s_workers_web, gpu
Plage où sont publiés les Services de type NodePort, dont les deux instances Traefik : 30080/30443 côté LAN, 31080/31443 côté DMZ. C'est le chaînon entre les VIP HAProxy et l'ingress du cluster.

Les handlers

On enchaine avec le fichier \handlers\main.yml

---
- name: Reload sysctl
  ansible.builtin.command: sysctl --system
  changed_when: true

- name: Restart containerd
  ansible.builtin.systemd_service:
    name: containerd
    state: restarted
    daemon_reload: true

roles/xkub_k8s_prereqs/handlers/main.yml

Pour rappel, ce fichier contient les instructions à déclencher suite à un changement d’état provoqué par une tache Ansible.

On rechargera, par exemple, les paramètres du kernel via la commande sysctl --system et on réalisera un redémarrage de containerd au besoin.

Les taches

On entre dans le dur avec l’analyse des différentes taches présentes dans le dossier taks

Comme d’habitude le fichier main.yml ne fait que lister l’ordre des taches

---
# Rôle xkub_k8s_prereqs — prépare un nœud Rocky Linux 10 pour kubeadm.
# À l'issue de ce rôle, le nœud est "prêt pour kubeadm" 
- name: Dépôt Kubernetes (pkgs.k8s.io)
  ansible.builtin.import_tasks: 01_repos.yml
  tags:
    - xkub
    - xkub.k8s_prereqs
    - xkub.k8s_prereqs.repos

- name: Noyau (modules, sysctl) et swap
  ansible.builtin.import_tasks: 02_kernel.yml
  tags:
    - xkub
    - xkub.k8s_prereqs
    - xkub.k8s_prereqs.kernel

- name: Runtime containerd
  ansible.builtin.import_tasks: 03_containerd.yml
  tags:
    - xkub
    - xkub.k8s_prereqs
    - xkub.k8s_prereqs.containerd

- name: Paquets kubeadm/kubelet/kubectl épinglés
  ansible.builtin.import_tasks: 04_packages.yml
  tags:
    - xkub
    - xkub.k8s_prereqs
    - xkub.k8s_prereqs.packages

- name: Politique firewalld (décision D4)
  ansible.builtin.import_tasks: 05_firewalld.yml
  tags:
    - xkub
    - xkub.k8s_prereqs
    - xkub.k8s_prereqs.firewalld

roles/xkub_k8s_prereqs/tasks/main.yml

Ces dernières sont décrites dans des fichiers spécifiques

01_repos.yml

On commence par 01_repos.yml

---
# Dépôt officiel pkgs.k8s.io. La version mineure (v1.36) est dérivée de version.yml dans group_vars/clu_k8s_xkub.
# k8s_version directement dans le template — pas de set_fact intermédiaire.
- name: Déployer le dépôt Kubernetes
  ansible.builtin.template:
    src: kubernetes.repo.j2
    dest: /etc/yum.repos.d/kubernetes.repo
    owner: root
    group: root
    mode: "0644"

roles/xkub_k8s_prereqs/tasks/01_repos.yml

Assez facile à comprendre, cette tache a pour mission d’ajouter un repo supplémentaire à l’OS, propre à la récupération des composants Kubernetes, à savoir kubeam, kubelet et kubectl.

Chaque release majeur de K8S dispose de son repo, l’on verra par la suite qu’on s’appuie sur un template qui prend la variable de la version de Kubernetes qu’on souhaite déployer pour créer le fichier a copier sur les cibles, actions que réalise cette task.

02_kernel.yml

On continue sur 02_kernel.yml

---
# --- Swap : kubelet est configuré avec failSwapOn=true --------------------
- name: Désactiver la swap active
  ansible.builtin.command: swapoff -a
  when: ansible_swaptotal_mb | int > 0
  changed_when: true

- name: Commenter les entrées swap du fstab
  ansible.builtin.replace:
    path: /etc/fstab
    regexp: '^([^#\n]*\sswap\s.*)$'
    replace: '# \1'

# --- Modules noyau ---------------------------------------------------------
- name: Charger les modules noyau au démarrage
  ansible.builtin.template:
    src: kubernetes-modules.conf.j2
    dest: /etc/modules-load.d/kubernetes.conf
    owner: root
    group: root
    mode: "0644"

- name: Charger les modules noyau immédiatement
  community.general.modprobe:
    name: "{{ item }}"
    state: present
  loop: "{{ xkub_k8s_prereqs_kernel_modules }}"

# --- Paramètres sysctl -----------------------------------------------------
- name: Déployer les paramètres sysctl Kubernetes
  ansible.builtin.template:
    src: 99-kubernetes.conf.j2
    dest: /etc/sysctl.d/99-kubernetes.conf
    owner: root
    group: root
    mode: "0644"
  notify: Reload sysctl

roles/xkub_k8s_prereqs/tasks/02_kernel.yml

C’est à ce niveau qu’on va gérer toute la configuration du kernel et des composants système de l’OS.

On s’assure que la SWAP est désactivée, c’est déjà normalement le cas, puisque notre Golden Image n’en possède pas, mais c’est mieux d’en faire le contrôle.

En effet, même si, depuis la version 1.22 de Kubernetes il est possible de conserver la swap, son usage impose des conditions et des restrictions spécifiques.

Ça reste plus simple de la laisser inactive.

Et pour information, si la swap est ainsi considéré c’est parce que Kubernetes repose sur le scheduling garanti des ressources. Si un pod nécessite 2G de ram, le scheduler de Kubernetes s’attend à ce que cette quantité soit réservée en RAM. L’usage de la SWAP ne garantit plus cela et l’aspect prédictif de Kubernetes s’en trouve perturbé.

On continue avec tout ce qui touche aux modules du noyau dont on a parlé précédemment. Il s’agit ici de juste s’assurer qu’il soit chargé au démarrage.

Enfin, et en lien avec notre handler sur la commande systcl, la dernière instruction de 02_kernel.yml concerne l’usage d’un fichier 99-kubernetes.conf qui va contenir des paramètres spécifiques au kernel Linux en lien avec k8s (d’où le besoin de recharger ces derniers une fois le fichier en place).

On détaillera ce point un peu plus loin quand on évoquera les templates.

03_containerd.yml

Vient le tour de 03_containerd.yml.

Comme son nom l’indique, il concerne l’installation et la configuration de containerd.

---
# containerd est installé depuis le binaire officiel GitHub (version épinglée
# dans versions.yml) : le paquet containerd.io des dépôts Docker ne couvre pas
# encore EL10 de façon certaine. crun et container-selinux viennent de dnf
# (installés dans 04_packages.yml via xkub_k8s_prereqs_packages).
- name: Télécharger et extraire containerd dans /usr/local
  ansible.builtin.unarchive:
    src: "{{ containerd_download_url }}"
    dest: /usr/local
    remote_src: true
    creates: /usr/local/bin/containerd

- name: Déployer l'unité systemd containerd
  ansible.builtin.template:
    src: containerd.service.j2
    dest: /etc/systemd/system/containerd.service
    owner: root
    group: root
    mode: "0644"
  notify: Restart containerd

- name: Créer /etc/containerd
  ansible.builtin.file:
    path: /etc/containerd
    state: directory
    owner: root
    group: root
    mode: "0755"

# Idempotence : la config par défaut n'est générée qu'à la création du fichier.
- name: Générer la configuration containerd par défaut
  ansible.builtin.shell:
    cmd: /usr/local/bin/containerd config default > /etc/containerd/config.toml
    creates: /etc/containerd/config.toml

- name: Activer le cgroup driver systemd
  ansible.builtin.replace:
    path: /etc/containerd/config.toml
    regexp: 'SystemdCgroup\s*=\s*false'
    replace: "SystemdCgroup = true"
  notify: Restart containerd

# La config générée par `containerd config default` ne connaît que le nom
# "runc" pour son handler de runtime OCI, et cherche ce binaire dans le PATH
# si BinaryName n'est pas renseigné. RHEL/Rocky Linux 10 a retiré runc au
# profit de crun : on pointe donc explicitement le binaire crun installé par dnf.
- name: Pointer le runtime OCI du handler "runc" sur le binaire crun
  ansible.builtin.lineinfile:
    path: /etc/containerd/config.toml
    regexp: '^\s*BinaryName\s*='
    insertafter: 'SystemdCgroup = true'
    line: '            BinaryName = "/usr/bin/crun"'
  notify: Restart containerd

- name: Activer et démarrer containerd
  ansible.builtin.systemd_service:
    name: containerd
    state: started
    enabled: true
    daemon_reload: true

- name: Télécharger et extraire crictl dans /usr/local/bin
  ansible.builtin.unarchive:
    src: "{{ cri_tools_download_url }}"
    dest: /usr/local/bin
    remote_src: true
    creates: /usr/local/bin/crictl

- name: Configurer crictl
  ansible.builtin.template:
    src: crictl.yaml.j2
    dest: /etc/crictl.yaml
    owner: root
    group: root
    mode: "0644"

roles/xkub_k8s_prereqs/tasks/03_containerd.yml

Je vous encourage à revoir l’article d’introduction de ce guide pour mieux comprendre la place de containerd dans la pile K8S.

Dans mon choix d’installation, j’ai décidé de partir des sources explicites de containerd sans me baser sur la version potentiellement disponible dans les repo par défaut de l’OS.

La dépendance à containerd est très forte dans une installation Vanilla de Kubernetes. Il est très important de suivre la matrice (même si, ici, je me suis permis une part d’anticipation de la version).

Récupérer les sources me permet d’avoir un contrôle précis de containerd.

La tâche les télécharge via la variable containerd_download_url, qui n’a pas été mentionnée, car elle se trouve dans les variables group de l’inventaire group_vars\clu_k8s_xkub\versions.yml (n’hésitez pas à consulter l’article précédent pour bien comprendre les liens entre les variables pour Ansible).

Une fois récupérées, les sources sont déployées sur l’OS dans /usr/local/bin/containerd

Puis Ansible va créer une unit systemd pour gérer le démarrage de containerd.

Vient ensuite le tour de la création du fichier de conf dans /etc/containerd

Celui-ci est d’abord généré avec ces valeurs par défaut ( /usr/local/bin/containerd config default > /etc/containerd/config.toml )

Puis personnalisé pour correspondre au besoin du cluster :

  • Activation des cgroup sur systemd : les cgroups (Control Groups) sont une fonctionnalité du noyau Linux qui sert de compteur et de limiteur de ressources pour les processus. C’est la base du mécanisme de conteneurisation sous Linux. Par défaut, si le système d'initialisation principal est systemd, deux gestionnaires de cgroups risquent d'exister en parallèle si la configuration n'est pas explicite : Il est fortement recommandé (et c'est devenu la norme depuis l'adoption généralisée de cgroup v2) d'aligner kubelet, containerd et le système hôte sur le même cgroup driver soit systemd.
  • Faire pointer containerd vers crun : on rappel que runc est devenu crun sous Rocky Linux 10. Containerd devant exploiter ce composant, il faut s’assurer de pointer vers le bon binaire.

Ansible enchaine avec l’activation et le démarrage de containerd (d’où sa référence dans le dossier handler).

Enfin, on en arrive à la récupération de crictl, la CLI de base pour communiquer avec Containerd. Cela permettra d’interagir avec ce dernier sur chaque node, un peu comme on le ferait avec une installation classique de Docker, sauf qu’ici c’est crictl qui sera appelé.

04_packages.yml

On passe à 04_packages.yml

---
# Le dépôt kubernetes.repo porte un `exclude` de protection contre les mises à
# jour involontaires : on le lève explicitement à l'installation (versions
# épinglées kubeadm-X.Y.Z).
- name: Installer les paquets Kubernetes (tous les nœuds)
  ansible.builtin.dnf:
    name: "{{ xkub_k8s_prereqs_packages }}"
    state: present
    update_cache: true
    disable_excludes: kubernetes

- name: Installer kubectl (control planes uniquement)
  ansible.builtin.dnf:
    name: "{{ xkub_k8s_prereqs_controlplane_packages }}"
    state: present
    disable_excludes: kubernetes
  when: "'k8s_controlplane_lan' in group_names"

# kubelet est seulement ACTIVÉ : il boucle volontairement tant que
# `kubeadm init/join` (phase 4) n'a pas généré sa configuration.
- name: Activer kubelet (sans le démarrer)
  ansible.builtin.systemd_service:
    name: kubelet
    enabled: true

roles/xkub_k8s_prereqs/tasks/04_packages.yml

Je ne vais pas rentrer dans le détail, il s’agit juste d’installer les paquets qu’on a présentés lors de l’analyse des variables.

Seule subtilité : si les kubelet sont déployés, ils ne sont pas lancés, ceci devant se faire plus tard lorsqu’on initialisera le cluster.

05_firewalld.yml

Enfin c’est au tour du dernier fichier de taches 05_firewalld.yml

---
- name: S'assurer que firewalld est actif
  ansible.builtin.systemd_service:
    name: firewalld
    state: started
    enabled: true

- name: Ouvrir les ports communs à tous les nœuds K8s
  ansible.posix.firewalld:
    port: "{{ item }}"
    permanent: true
    immediate: true
    state: enabled
  loop: "{{ xkub_k8s_prereqs_fw_ports_common }}"

- name: Ouvrir les ports control plane (API server)
  ansible.posix.firewalld:
    port: "{{ item }}"
    permanent: true
    immediate: true
    state: enabled
  loop: "{{ xkub_k8s_prereqs_fw_ports_controlplane }}"
  when: "'k8s_controlplane_lan' in group_names"

# etcd n'est ouvert qu'entre control planes : c'est la donnée la plus sensible
# du cluster (Secrets inclus, en clair dans etcd sauf chiffrement at-rest).
# Une ouverture large de 2379-2380 sur le LAN donnerait à tout worker ou nœud
# compromis un accès direct à l'intégralité de l'état du cluster.
- name: Ouvrir etcd (2379-2380) aux seuls autres control planes
  ansible.posix.firewalld:
    rich_rule: >-
      rule family="ipv4"
      source address="{{ hostvars[item].ansible_host }}/32"
      port port="{{ k8s_port_etcd_range }}" protocol="tcp" accept
    permanent: true
    immediate: true
    state: enabled
  loop: "{{ groups[xkub_k8s_prereqs_fw_etcd_source_group] | difference([inventory_hostname]) | sort }}"
  when: "'k8s_controlplane_lan' in group_names"

# Nettoyage de l'ouverture large posée par les runs antérieurs au durcissement.
- name: Retirer l'ancienne ouverture etcd non restreinte
  ansible.posix.firewalld:
    port: "{{ k8s_port_etcd_range }}/tcp"
    permanent: true
    immediate: true
    state: disabled
  when: "'k8s_controlplane_lan' in group_names"

- name: Ouvrir la plage NodePorts (workers)
  ansible.posix.firewalld:
    port: "{{ item }}"
    permanent: true
    immediate: true
    state: enabled
  loop: "{{ xkub_k8s_prereqs_fw_ports_worker }}"
  when: xkub_k8s_prereqs_worker_groups | intersect(group_names) | length > 0

roles/xkub_k8s_prereqs/tasks/05_firewalld.yml

Pareil, je ne vais pas entrer dans le détail. Il va s’agir d’utiliser le module firewalld d’Ansible pour paramétrer le firewall de chaque OS en fonction de la matrice des flux qui vous a été présenté.

A noter que comme pour SElinux, certains préconisent de désactiver firewalld sur des environnements Kubernetes. Personnelement, et meme si cela induit des taches supplémentaires, je préfère conserver firewalld actif (comme SELinux) et configurer les ouvertures de manière explicite.

Les templates

On va maintenant parler du dossier template.

99-kubernetes.conf.j2

On y trouve le fichier 99-kubernetes.conf.j2

# {{ ansible_managed }}
# Paramètres noyau requis par Kubernetes (kubelet + CNI)
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-arptables = 1
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 8192

roles/xkub_k8s_prereqs/templates/99-kubernetes.conf.j2

Il est très important, car il va influencer le kernel Linux et modifier ses paramètres de bases.

Paramètre sysctl Ce que fait le noyau Linux Conséquence pour Kubernetes
# {{ ansible_managed }} (Commentaire) En-tête injecté par Ansible. Indique aux administrateurs que ce fichier est généré automatiquement et ne doit pas être modifié à la main.
net.ipv4.ip_forward = 1 Active le routage des paquets IPv4 d'une interface réseau à une autre au niveau de l'hôte. Vital. Permet au nœud de transférer le trafic entre les Pods (réseau virtuel) et les cartes réseau physiques de la machine hôte.
net.bridge.bridge-nf-call-iptables = 1 Force le trafic IPv4 traversant un pont virtuel (Linux Bridge) à être filtré par iptables. Permet aux plugins CNI (Calico, Flannel, etc.) et à kube-proxy d'appliquer la sécurité (NetworkPolicies) et la redirection de trafic des Services.
net.bridge.bridge-nf-call-ip6tables = 1 Identique au précédent, mais dédié au trafic IPv6. Garantit que le trafic IPv6 traversant les ponts virtuels passe aussi par les règles de filtrage ip6tables (nécessaire si le cluster utilise l'IPv6 ou le Dual-Stack).
net.bridge.bridge-nf-call-arptables = 1 Force le trafic ARP traversant un pont virtuel à être filtré par arptables. Permet d'inspecter ou de filtrer la résolution d'adresses MAC au niveau couche 2, utile pour certains plugins réseau CNI spécifiques ou pour prévenir le MAC/ARP spoofing.
fs.inotify.max_user_watches = 1048576 Augmente le nombre maximal de fichiers ou dossiers qu'un utilisateur peut surveiller simultanément en mémoire. Performance & Stabilité. Évite l'erreur System limit for number of file watchers reached
fs.inotify.max_user_instances = 8192 Augmente le nombre maximal d'instances d'écoute inotify qu'un seul utilisateur peut créer. Permet à l'hôte d'accueillir un grand nombre de conteneurs en parallèle, chacun ouvrant ses propres instances de surveillance de fichiers.

Il faut bien comprendre qu’un node Kubernetes n’est pas un serveur Linux classique. Étant donné qu’il est destiné à faire tourner un grand nombre de conteneurs, il faut paramétrer le kernel Linux de manière à ce que ce fonctionnement puisse se faire sans incident.

containerd.service.j2

containerd.service.j2 est un template pour définir la unit systemd de containerd. Rien de spécifique à ce niveau, juste quelques paramétrages conseillés associés au service pour garantir un bon fonctionnement de containerd.

# {{ ansible_managed }}
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target

[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd
Type=notify
Restart=always
RestartSec=5
KillMode=process
Delegate=yes
OOMScoreAdjust=-999
LimitNOFILE=1048576
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity

[Install]
WantedBy=multi-user.target

roles/xkub_k8s_prereqs/templates/containerd.service.j2

crictl.yaml.j2

C’est le fichier de configuration pour la cli crictl afin qu’elle puisse interroger containerd.

# {{ ansible_managed }}
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 2
debug: false
pull-image-on-create: false
disable-pull-on-run: false

roles/xkub_k8s_prereqs/templates/crictl.yaml.j2

kubernetes.repo.j2

Comme discuté précédemment, c’est le fichier spécifique au repo Kubernetes associé à la version déployée qui sera envoyé sur chaque nœud pour l’installation des paquets kubeadm, kubectl et kubelet.

# {{ ansible_managed }}
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v{{ k8s_version.split('.')[0:2] | join('.') }}/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v{{ k8s_version.split('.')[0:2] | join('.') }}/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni

roles/xkub_k8s_prereqs/templates/kubernetes.repo.j2

Voici ce que ça donne à la cible.


[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.36/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni

/etc/yum.repos.d/kubernetes.repo généré sur chaque nœud

kubernetes-modules.conf.j2

Et le dernier template pour s’assurer du chargement des modules dont nous avons parlé au démarrage du serveur.

# {{ ansible_managed }}
{% for module in xkub_k8s_prereqs_kernel_modules %}
{{ module }}
{% endfor %}

roles/xkub_k8s_prereqs/templates/kubernetes-modules.conf.j2

Voici ce que ça donne à la cible.


overlay
br_netfilter

/etc/modules-load.d/kubernetes.conf généré sur chaque nœud

Lancement du playbook

C’est terminé du rôle xkub_k8s_prereqs.

En lien avec la présentation du rôle xkub_common vu dans l’article précédent, on a traité de l’intégralité du playbook xkub_10_base.yml

C’est donc le moment de l’exécuter.

Modules ansible supplémentaires

Juste avant de l’appliquer, on va simplement passer une première instruction Ansible pour s’assurer d’avoir tous les modules nécessaires à l’exécution des playbooks.

Pour rappel, j’utilise Ansible installé sur mon poste W11 via WSL.

Par défaut, Ansible dispose de modules intégrés, mais, pour ce cookbook, il va être nécessaire d’intégrer d’autres modules issus de ansible galaxy, soit le référentiel de modules tiers pour Ansible.

Ces modules additionnels se retrouvent listés dans le fichier requirements.yml, dont voici le contenu

---
# Collections requises
# (kubernetes.core n'est utilisé qu'à partir des phases Kubernetes).
collections:
  - name: ansible.posix
    version: 1.5.4
  - name: community.general
    version: 9.5.0
  - name: kubernetes.core
    version: 3.2.0

requirements.yml

On s’assure juste d’avoir les modules de base (ansible.posix, community.general) et surtout le module Kubernetes (kubernetes.core). Ce dernier ne va pas nous être utile tout de suite, mais servira plus tard quand on initialisera le cluster Kubernetes.

La commande pour prendre en compte le fichier et installer les dépendances est ansible-galaxy collection install -r requirements.yml.

Installation des collections Ansible Galaxy avec ansible-galaxy collection install

Cliquez sur l'image pour l'agrandir.

Check avant lancement

Avant d’opérer, il est important de ne pas oublier les entrées DNS si celles-ci n’ont pas encor été réalisées.

Pour ma part, j’ai créé les enregistrements au niveau de mon DNS interne opéré par mon serveur Windows.

Création des enregistrements DNS des nœuds du cluster sur le DNS interne Windows

Cliquez sur l'image pour l'agrandir.

C’est un élément qu’on pourrait automatiser. Une fois les résolutions en place, pour s’assurer que tous les serveurs sont prêts, on peut commencer par vérifier si Ansible y a bien accès.

ansible all -m ping

Résultat de la commande ansible all -m ping sur les nœuds du cluster

Cliquez sur l'image pour l'agrandir.

Exécution du playbook

Si le retour est positif, alors on peut appeler à l’exécution de notre playbook.

ansible-playbook playbooks/xkub_10_base.yml

Lancement du playbook Ansible xkub_10_base.yml

Cliquez sur l'image pour l'agrandir.

C’est là que la magie opère, puisqu’on va pouvoir suivre le déroulé des actions.

Ansible va se connecter à chaque cible décrite dans l’inventaire et en lien avec les rôles à appliquer.

Il va réaliser les instructions demandées dans les taches et s’assurer que les modifications ont bien été réalisées.

Une fois terminée, on a un rapport du nombre de modifications apporté.

Sortie PLAY RECAP du premier lancement du playbook xkub_10_base.yml

Cliquez sur l'image pour l'agrandir.

Relance du playbook

Il est crucial de relancer le playbook une seconde fois, comme décrit dans l’article précédent. L'indempotence des playbooks Ansible doit être assurée.

Normalement, au niveau de la sortie « PLAY RECAP » tout devrait être en vert sans aucun change.

Comme vous le voyez ici, ce n’est pas le cas, mais j’ai néanmoins très peu de modifications.

Sortie PLAY RECAP du second lancement du playbook : une seule modification par nœud

Cliquez sur l'image pour l'agrandir.

Seule une instruction a été rejouée et il s’agit de la récréation des fichiers hosts. En effet la méthode utilisée fait que le fichier est regénéré quoi qu’il arrive.

Tâche xkub_common de déploiement de /etc/hosts rejouée à chaque exécution

Cliquez sur l'image pour l'agrandir.

Ce n’est pas très grâve en soi, mais si on voulait être méticuleux, il faudrait revoir cette partie.

Contrôles sur les nodes

Si maintenant on se connecte à l’un des serveurs du futur cluster, on peut vérifier plusieurs choses

Le fichier host justement

cat /etc/hosts

Contenu du fichier /etc/hosts généré par Ansible sur un nœud du cluster

Cliquez sur l'image pour l'agrandir.

le status de containerd

systemctl status containerd

Statut du service containerd sur un nœud Rocky Linux 10

Cliquez sur l'image pour l'agrandir.

On peut vérifier la version de containerd (containerd --version) et de crictl (crictl --version)

Versions de containerd et de crictl installées sur un nœud du cluster

Cliquez sur l'image pour l'agrandir.

Enfin, on peut contrôler les ouvertures firewall (sudo firewall-cmd --list-all), à raison:

  • D’un test sur un futur control plane
Sortie firewall-cmd --list-all sur un futur control plane, avec les rich rules etcd

Cliquez sur l'image pour l'agrandir.

  • D’un test sur un futur worker
Sortie firewall-cmd --list-all sur un futur worker, avec la plage NodePort

Cliquez sur l'image pour l'agrandir.

On observe bien la différence d’ouverture de flux entre les deux nodes.

Conclusion

Nos serveurs disposent à présent de tous les prérequis nécessaires à leur intégration dans un cluster Kubernetes, en adéquation avec l’architecture attendue et présenté en début de cookbook.

Le parcours des configurations via une logique Ansible nous a permis de traiter point par point ce qui est nécessaire au bon fonctionnement de K8S pour la suite.

  • Installation des paquets de bases incluant crun (repo OS) et kubeadm, kubelet, kubectl depuis le repo officiel K8S
  • Activation des modules du kernel Linux overlay et br_netfilter
  • Installation de containerd et crictl (depuis les sources en lignes)
  • Configuration du kernel Linux via sysctl, notamment pour le routage interne et la levée des limitations autour du nombre de connexions et de fichiers ouverts
  • Ouverture des ports nécessaires au fonctionnement du cluster et de ses services(cilium, traefik...) en tenant compte des futurs rôles des nodes (control plane, worker)

L’IA a accéléré l’écriture des playbooks, mais là encore, on a étudié chacun des fichiers produits pour s’assurer de la cohérence de l’ensemble. À noter que l’IA est intervenu sur instruction de ma part et qu’une partie de ce qui a été écrit est dépendante de la logique que j’ai souhaité mettre en œuvre.

Avant de pouvoir procéder à l’initialisation du cluster en lui-même, il va nous falloir traiter du cas des serveurs HAproxy. Nous verrons cela dans le prochain article.