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.
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).
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.
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
C’est ici qu’on va retrouver les différentes variables propres au rôle
À 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.
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 :
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.
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.
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. |
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.
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
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.
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.
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 :
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é.
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.
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.
On va maintenant parler du dossier template.
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 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
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
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
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.
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.
Cliquez sur l'image pour l'agrandir.
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.
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
Cliquez sur l'image pour l'agrandir.
Si le retour est positif, alors on peut appeler à l’exécution de notre playbook.
ansible-playbook playbooks/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é.
Cliquez sur l'image pour l'agrandir.
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.
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.
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.
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
Cliquez sur l'image pour l'agrandir.
le status de containerd
systemctl status containerd
Cliquez sur l'image pour l'agrandir.
On peut vérifier la version de containerd (containerd --version) et de crictl (crictl --version)
Cliquez sur l'image pour l'agrandir.
Enfin, on peut contrôler les ouvertures firewall (sudo firewall-cmd --list-all), à raison:
Cliquez sur l'image pour l'agrandir.
Cliquez sur l'image pour l'agrandir.
On observe bien la différence d’ouverture de flux entre les deux nodes.
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.
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.