Cluster Kubernetes (k8s)¶
Rôle¶
Cluster Kubernetes on-premise de l'infrastructure Indio, destiné à héberger de futures charges de travail conteneurisées. Provisionné avec kubeadm (control-plane HA à 3 nœuds), containerd comme runtime et Calico comme CNI (réseau overlay des pods + network policies). Projet Terraform+Ansible dédié (/root/k8s, repo indio/iac/k8s), volontairement séparé du reste de l'IaC applicative — à la demande explicite du user ("c'est un projet en dehors du projet actuel").
Architecture¶
- 6 nœuds Rocky Linux 9.6, zone service (
seg-service), clones du templatetmpl-rocky96-hardened:
| Rôle | VM | IP | CPU/RAM/Disque |
|---|---|---|---|
| control-plane | INDSERV019 | 10.100.2.149 | 2 vCPU / 4 Go / 40 Go |
| control-plane | INDSERV020 | 10.100.2.150 | 2 vCPU / 4 Go / 40 Go |
| control-plane | INDSERV021 | 10.100.2.151 | 2 vCPU / 4 Go / 40 Go |
| worker | INDSERV022 | 10.100.2.152 | 4 vCPU / 8 Go / 80 Go |
| worker | INDSERV023 | 10.100.2.153 | 4 vCPU / 8 Go / 80 Go |
| worker | INDSERV024 | 10.100.2.154 | 4 vCPU / 8 Go / 80 Go |
(Specs vérifiées directement dans terraform.tfstate le 2026-07-19.)
- Point d'entrée de l'API control-plane : VIP HAProxy
10.100.2.130:6443(servicek8s-api), utilisée commecontrolPlaneEndpointpar kubeadm pour permettre un control-plane HA à 3 nœuds. - Runtime conteneur containerd (pas de Docker). CNI Calico v3.30.7 (déployé via l'opérateur Tigera). Kubernetes v1.33.13.
- Réseau pod
192.168.0.0/16(défaut Calico), réseau service10.96.0.0/12(défaut kubeadm, non surchargé). - Registres d'images miroités via Nexus (un hostname public par port HAProxy dédié, cf. Nexus pour la création des dépôts) :
docker.io→8083,registry.k8s.io→8084,ghcr.io→8085,quay.io→8086. - Manifests Calico (opérateur Tigera + custom resources) servis depuis un dépôt Nexus raw hosted
k8s-manifests, pas depuisraw.githubusercontent.comen direct (host bloqué en sortie depuis le périmètre proxy/FortiGate — voir Procédure manuelle).
Structure du dépôt¶
| Fichier / dossier | Contenu |
|---|---|
main.tf |
Ressource unique vsphere_virtual_machine.k8s, for_each sur var.nodes |
variables.tf |
Défauts (folder, template, gateway, DNS, domaine) + liste nodes (6 objets name/cpu/ram/disk0_gb) |
versions.tf |
Providers hashicorp/vsphere >= 2.6.0, e-breuninger/netbox >= 5.0.0, < 6.0.0 |
ansible/site.yml |
Orchestration des 6 rôles |
ansible/inventory.ini |
Groupes k8s_control_plane/k8s_workers/k8s |
ansible/group_vars/k8s.yml |
Proxy/no_proxy, miroirs registry, versions kube/Calico, subnets |
ansible/roles/common |
Prérequis OS kubeadm |
ansible/roles/containerd |
Runtime + miroirs registry |
ansible/roles/kube-pkgs |
kubelet/kubeadm/kubectl |
ansible/roles/kubeadm-init |
Init du 1er control-plane |
ansible/roles/kubeadm-join |
Join des 5 autres nœuds |
ansible/roles/calico |
CNI |
Topologie¶
flowchart TD
VIP["VIP HAProxy<br/>10.100.2.130:6443<br/>service k8s-api"]
subgraph CP["Control-plane"]
CP1["INDSERV019<br/>10.100.2.149"]
CP2["INDSERV020<br/>10.100.2.150"]
CP3["INDSERV021<br/>10.100.2.151"]
end
subgraph WK["Workers"]
WK1["INDSERV022<br/>10.100.2.152"]
WK2["INDSERV023<br/>10.100.2.153"]
WK3["INDSERV024<br/>10.100.2.154"]
end
VIP --> CP1 & CP2 & CP3
WK1 -.->|kubelet| VIP
WK2 -.->|kubelet| VIP
WK3 -.->|kubelet| VIP
CP1 <-->|etcd raft| CP2
CP2 <-->|etcd raft| CP3
CP1 <-->|etcd raft| CP3
subgraph REG["Nexus — miroirs registry"]
R1["docker-proxy :8083"]
R2["k8s-proxy :8084"]
R3["ghcr-proxy :8085"]
R4["quay-proxy :8086"]
end
CP1 & CP2 & CP3 & WK1 & WK2 & WK3 -->|containerd| REG
Séquence de déploiement Ansible¶
flowchart LR
A["common<br/>swap off, sysctl, SELinux permissive,<br/>firewalld"] --> B["containerd<br/>dépôt Nexus, config.toml,<br/>drop-in no_proxy"]
B --> C["kube-pkgs<br/>kubelet/kubeadm/kubectl 1.33.13,<br/>drop-in no_proxy"]
C --> D["kubeadm-init<br/>1er control-plane<br/>seulement"]
D --> E["kubeadm-join<br/>2 autres CP<br/>+ 3 workers"]
E --> F["calico<br/>1er control-plane :<br/>opérateur Tigera + CR"]
Provisioning Terraform¶
main.tf: ressource uniquevsphere_virtual_machine.k8s,for_eachsurvar.nodes(liste de 6 objetsname/cpu/ram/disk0_gb,coalesce()avec les défauts devariables.tfsi un champ n'est pas renseigné pour un nœud donné).- L'IP de chaque nœud est résolue via
data "netbox_ip_addresses"(providere-breuninger/netbox), une instance par nœud (for_eachsur le même ensemble), filtrée surdns_name = lower("<nom>.infra.indio")— c'est une lecture, pas une réservation : l'IP doit déjà exister côté NetBox avant l'apply (voir Procédure manuelle). - Variables clés (
variables.tf) :vsphere_folderpar défautseg-service,template_name=tmpl-rocky96-hardened,vm_gateway= 10.100.2.190,vm_dns= [10.100.2.241, 10.100.2.242] (FreeIPA),vm_domain= infra.indio. lifecycle.ignore_changessurannotation,clone[0].template_uuid,clone[0].customize,disk[0].io_share_count— évite une recréation si vCenter renvoie ces attributs avec un format légèrement différent après coup.- Sortie
vms: map nom de VM → adresse IP.
Configuration Ansible¶
Playbook ansible/site.yml, inventaire ansible/inventory.ini (groupes k8s_control_plane, k8s_workers, k8s). Six rôles appliqués dans l'ordre (voir diagramme ci-dessus) :
- common — prérequis officiels kubeadm : désactivation du swap (et retrait de l'entrée
fstab), modules noyauoverlay/br_netfilter(chargés + persistés dans/etc/modules-load.d), sysctl (bridge-nf-call-iptables,bridge-nf-call-ip6tables,ip_forward), SELinux en mode permissive (requis par la documentation kubeadm sur RHEL/Rocky), ouverture firewalld : ports communs à tous les nœuds (kubelet 10250, BGP Calico 179, VXLAN Calico 4789/udp, Typha 5473 — les control-plane n'hébergent aucun replica Typha et doivent donc le joindre à travers le réseau), règle riche autorisant le protocole IP-in-IP (protocol value="4", utilisé par Calico pour le trafic inter-nœuds), puis ports spécifiques control-plane (6443, 2379-2380, 10259, 10257) et workers (NodePort 30000-32767). - containerd — dépôt Docker CE via Nexus, installation de
containerd.io, génération de/etc/containerd/config.toml(SystemdCgroup + miroirs registry viahosts.toml), drop-in systemd dédié pour leno_proxy. - kube-pkgs — dépôt Kubernetes via Nexus, installation de
kubelet/kubeadm/kubectlépinglés àkube_version(1.33.13,disable_excludes: indio-kubernetespour contourner l'exclusion de version par défaut du dépôt), drop-in systemdno_proxypour kubelet, activation du service (démarrage réel différé jusqu'àkubeadm init/join). - kubeadm-init — ne s'exécute que sur le premier control-plane (
groups['k8s_control_plane'][0]) : génèrekubeadm-config.yaml, exécutekubeadm init --config=... --upload-certs, configure le kubeconfig pourindio-admetroot, régénère un jeton de join worker (kubeadm token create --print-join-command) et une certificate-key control-plane (kubeadm init phase upload-certs --upload-certs), exposés comme facts partagés (k8s_join_worker_cmd/k8s_join_cp_cmd) pour le rôle suivant. - kubeadm-join — fait rejoindre les 2 autres control-plane et les 3 workers, en excluant le premier control-plane via des conditions
when(comparaison àgroups['k8s_control_plane'][0]) plutôt qu'un patternhostsdynamique ;--node-name {{ inventory_hostname | lower }}(kubeadm exige un nom de nœud en minuscules, contrairement au nom vCenter/NetBox en majuscules). - calico — sur le premier control-plane uniquement : télécharge les manifests (opérateur Tigera + custom resources) depuis Nexus, aligne le
cidrdes custom resources surk8s_pod_subnet, applique l'opérateur puis les CR en--server-side --force-conflicts(avec retry sur l'apply des CR), attend que tous les nœuds soientReady.
Variables (group_vars/k8s.yml) : proxy Squid (10.100.10.4:3128), k8s_no_proxy (CIDR internes cluster + domaines internes exclus du proxy), miroirs registry Nexus, versions kube/Calico, endpoint API, subnets pod/service.
Procédure manuelle¶
Deux étapes réalisées hors Terraform/Ansible, à reproduire pour tout futur rebuild complet :
- Réservation IP NetBox préalable — la data source
netbox_ip_addressesdemain.tféchoue enno resultsi les 6 FQDN (indserv019.infra.indio...indserv024.infra.indio) n'ont pas déjà une IP réservée côté NetBox. Script dédié idempotent :python3 /root/netbox/seed/add_k8s_vms.py(réutilise le patternget_or_create, 2 rôles fonctionnelskubernetes-control-plane/kubernetes-worker) — voir NetBox. Ne jamais rejouerseed/populate.pyen entier pour cet effet. - Upload manuel des manifests Calico vers Nexus —
tigera-operator.yamletcustom-resources.yaml(v3.30.7) ont été récupérés une fois depuis un poste disposant d'un accès Internet direct, puis uploadés tels quels (mêmes octets que l'upstream officiel) dans le dépôt Nexus raw hostedk8s-manifests(chemincalico/v3.30.7/), carraw.githubusercontent.comest injoignable depuis le périmètre proxy Squid/FortiGate. La création du dépôt Nexus lui-même (type raw hosted) suit le même mécanisme que les autres dépôts internes — voir Nexus.
Procédure de déploiement¶
python3 /root/netbox/seed/add_k8s_vms.py # réservation IP préalable (obligatoire)
terraform apply # crée les 6 VM nues
cd ansible && ansible-playbook site.yml # common -> containerd -> kube-pkgs -> kubeadm-init -> kubeadm-join -> calico
Vérification finale intégrée au rôle calico : attente que tous les nœuds soient à l'état Ready (jusqu'à 30 tentatives, 20 s d'intervalle).
Contrôle de santé / Vérification¶
kubectl --kubeconfig ~/.kube/config get nodes -o wide # 6/6 Ready attendu
kubectl get pods -A # 100% Running/Completed attendu
kubectl -n tigera-operator get deployment tigera-operator # Available
Dernier rebuild complet vérifié le 2026-07-15 : 6/6 nœuds Ready, aucun pod résiduel bloqué (ContainerCreating/Pending).
Points d'attention¶
Piège proxy Squid / CIDR internes (rencontré, corrigé)
Le drop-in systemd global (indio-baseline-config) route kubelet et containerd à travers le proxy Squid, dont le no_proxy par défaut ignore les CIDR internes au cluster (ClusterIP 10.96.0.0/12, pods 192.168.0.0/16). Squid refuse alors le CONNECT vers ces IP (403), que les clients CNI/Kubernetes journalisent à tort comme un refus RBAC (Forbidden) — signal trompeur : kubectl auth can-i répond "yes" et un curl manuel en session SSH réussit, car ni l'un ni l'autre n'hérite du proxy systemd. Symptôme distinctif : calico-node (DaemonSet, pas de sandbox CNI à créer pour lui-même) démarre normalement pendant que tout le reste (coredns, calico-apiserver, calico-kube-controllers...) reste bloqué en ContainerCreating — c'est containerd (pas kubelet) qui exécute le binaire CNI en sous-processus, donc c'est son environnement proxy à lui qui compte. Corrigé par des drop-in no_proxy dédiés sur kubelet et containerd séparément.
- Version Calico v3.30.x imposée par la compatibilité avec Kubernetes 1.33 (v3.29.x n'est testé/supporté que jusqu'à la 1.32 selon la documentation Tigera).
- CRD Calico appliquées en
--server-side --force-conflicts: leur taille dépasse la limite de l'annotationkubectl.kubernetes.io/last-applied-configurationutilisée par unapplyclassique ; retry nécessaire car le déploiementtigera-operatorpeut devenirAvailableavant que ses CRD ne soientEstablishedcôté apiserver. kubeadm initpeut timeouter au tout premier essai sur un control-plane à 2 vCPU/4 Go (pull d'images à froid + démarrage simultané etcd/apiserver/scheduler/controller-manager) :/etc/kubernetes/admin.confexiste alors mais le cluster est à moitié initialisé (RBAC bootstrap jamais fait). Le rôle ne détecte l'état "déjà initialisé" que viastat admin.conf, donc un simple re-run ne suffit pas — nécessitekubeadm reset -f+ nettoyage (/etc/cni/net.d,~/.kube) avant de relancer.- Jetons de join régénérés à chaque exécution du playbook (pas de token statique conservé) —
kubeadm-joindoit tourner dans le même run quekubeadm-init(facts partagés en mémoire, pas persistés sur disque). - Manifests Calico mirrorés une seule fois sur Nexus (upload manuel) — toute montée de version Calico future nécessite de répéter cette étape manuelle.
- Registres Docker Nexus (8083-8087) et leur health-check dédié (
/v2/+ 401) sont documentés sur la page Nexus — non dupliqué ici.