Skip to content

Version du cluster : pourquoi ce n'est pas une montée de version

Issue #186. Le cluster est passé de 1.32 à 1.35 en changeant une valeur par défaut. Cette page explique pourquoi c'est suffisant ici, ce que ça ne serait pas ailleurs, et ce qui reste à faire pour aller jusqu'à 1.36.


Le constat

Mail AWS du 2026-08-27 : le cluster est en 1.32 et doit monter de version.

En interrogeant l'API plutôt que le mail, le tableau est plus précis :

aws eks describe-cluster-versions \
  --query 'clusterVersions[].[clusterVersion,clusterVersionStatus,endOfStandardSupportDate]' \
  --output table
Version Fin du support standard Situation au 2026-08-30
1.32 2026-03-23 support étendu depuis 5 mois
1.33 2026-07-29 support étendu depuis 1 mois
1.34 2026-12-02 standard, 3 mois restants
1.35 2027-03-27 standard, 7 mois restants
1.36 2027-08-02 standard, 11 mois restants

La nuance compte : le surcoût du support étendu ne va pas arriver, il court déjà dès qu'un cluster 1.32 est monté. Le seul frein aujourd'hui est que l'infrastructure est détruite, donc rien n'est facturé.

1.33 et 1.34 sont éliminées d'office : la première est déjà en support étendu, la seconde y bascule dans trois mois.


Ce qui rend ce cluster différent : il est recréé, pas mis à jour

L'issue #186 était rédigée pour une montée de version classique : « une version mineure à la fois », « quatre montées successives », « le plan de contrôle puis les nœuds ». Tout cela est vrai pour un cluster qu'on fait vivre.

Ici, le cluster est éphémère. Il est détruit en fin de session et recréé au montage suivant. Or à la création, on choisit directement la version.

graph TD
    subgraph "Cluster que l'on fait vivre"
        A["1.32"] --> B["1.33"] --> C["1.34"] --> D["1.35"]
        A -.->|"interdit"| D
    end
    subgraph "Cluster ephemere (ce projet)"
        E["terraform destroy"] --> F["eks_cluster_version = 1.35"]
        F --> G["terraform apply<br/>cluster neuf en 1.35"]
    end
    style D fill:#27ae60,color:#fff
    style G fill:#27ae60,color:#fff

Il n'y a donc ni palier, ni séquence, ni drain, ni nœuds à faire suivre. La contrainte « une version mineure à la fois » s'applique à un UpdateClusterVersion sur un cluster existant, pas à un CreateCluster.

C'est un effet de bord bénéfique d'une contrainte de coût : le cluster est détruit chaque soir pour ne pas payer, et cette destruction transforme une migration en choix de paramètre.

Ce qui reste vrai, et qu'il faut quand même faire

Tâche Pourquoi
Vérifier les API dépréciées les manifests du dépôt doivent être valides dans la version cible
Vérifier la matrice de compatibilité des composants Cilium, cert-manager, ArgoCD, ESO, ExternalDNS ont chacun la leur
Aligner le client kubectl la politique d'écart de version ne tolère qu'une version mineure
Prouver le montage tant qu'un cluster n'est pas monté dans la nouvelle version, rien n'est prouvé

Les API dépréciées : l'absence se prouve

Entre deux versions mineures, Kubernetes retire des API. Un manifeste qui utilise une API retirée est rejeté par l'API server, et le déploiement échoue.

Inventaire des apiVersion du dépôt :

grep -rh '^apiVersion:' k8s/ | sort | uniq -c | sort -rn
Famille Verdict
v1, apps/v1, batch/v1, policy/v1, autoscaling/v2, networking.k8s.io/v1 API natives stables, aucune retirée
gateway.networking.k8s.io/v1 Gateway API, version stable
argoproj.io/v1alpha1, external-secrets.io/v1, cert-manager.io/v1, monitoring.coreos.com/v1, gateway.envoyproxy.io/v1alpha1 CRD tierces

Le point à comprendre : une CRD (Custom Resource Definition, une ressource ajoutée au cluster par un opérateur) ne suit pas le cycle de dépréciation de Kubernetes. Son apiVersion dépend de la version de l'opérateur qui l'installe, pas de celle du cluster. Un v1alpha1 d'Envoy Gateway ne devient pas invalide parce que Kubernetes passe de 1.32 à 1.35.

Aucune migration de manifeste n'était donc nécessaire.


Pourquoi 1.35 et pas 1.36 : le CNI est le facteur limitant

Le CNI (Container Network Interface, le composant qui donne une adresse IP aux pods et fait circuler leurs paquets) est ici Cilium, en version 1.19.5 (ansible/bootstrap.yml).

Cilium Kubernetes testés de bout en bout
1.19 1.32, 1.33, 1.34, 1.35
1.20 1.33, 1.34, 1.35, 1.36

Cilium 1.19 ne couvre pas 1.36. Viser 1.36 imposerait donc deux changements simultanés : la version du cluster et celle du CNI.

Et ce n'est pas un changement anodin sur ce cluster. Cilium y tourne en kubeProxyReplacement : le bootstrap Ansible supprime aws-node et kube-proxy au cutover (ADR 019). Si Cilium ne démarre pas, le résultat n'est pas un cluster dégradé, c'est un cluster sans réseau du tout.

Un seul changement à la fois. Passer de 1.32 à 1.35, c'est déjà passer de « quatre versions de retard, en support étendu depuis cinq mois » à « une version de retard, en support standard ». Le bump vers Cilium 1.20 et Kubernetes 1.36 aura sa propre issue et sa propre validation en direct.


Et si on mettait vraiment le CNI à jour : qu'est-ce qui coupe ?

La réponse est contre-intuitive, et elle vaut d'être connue.

Cilium programme le réseau avec des programmes eBPF : du code chargé dans le noyau Linux, qui traite les paquets sans repasser par un processus en espace utilisateur. Ces programmes restent chargés dans le noyau même quand le pod cilium-agent disparaît.

graph TD
    A["cilium-agent redemarre"] --> B["programmes eBPF<br/>toujours chargees dans le noyau"]
    B --> C["le trafic existant<br/>continue de passer"]
    A --> D["plus personne ne programme<br/>de NOUVELLES regles"]
    D --> E["un pod qui demarre<br/>attend son endpoint reseau"]
    D --> F["une NetworkPolicy modifiee<br/>n'est pas appliquee"]
    style C fill:#27ae60,color:#fff
    style E fill:#e67e22,color:#fff
    style F fill:#e67e22,color:#fff

Ce qui gèle pendant la fenêtre n'est donc pas le trafic, c'est le changement.

Cela vaut pour un redémarrage. Une montée de version mineure engage davantage : Cilium fournit un cilium-pre-flight-check qui pré-télécharge l'image et valide la configuration sur chaque nœud avant la bascule, précisément parce que le risque n'est pas nul.


Trois stratégies de montée de version

Stratégie Comment Quand
Rolling in-place mise à jour des composants sur le cluster vivant le standard, la plupart des équipes
Blue/green de node group nouveaux nœuds à la nouvelle version, migration des pods, retrait des anciens changement de nœuds risqué
Blue/green de cluster cluster neuf, bascule du trafic (DNS ou load balancer), destruction de l'ancien changement majeur, aucune tolérance au risque

Ce projet fait déjà du blue/green de cluster, sans l'avoir décidé : il détruit et recrée au lieu de mettre à jour. Ce qui manque pour que ce soit un blue/green complet, c'est le recouvrement — les deux clusters vivants en même temps et une bascule DNS progressive, au lieu d'un trou entre la destruction et le montage.

Ce qu'un rolling in-place ferait ici, et pourquoi l'application ne tomberait pas

Même en montée in-place, l'application resterait servie, par construction :

Garde-fou Effet
HPA minReplicas: 2 il y a toujours au moins deux pods
PDB maxUnavailable: 1 on peut toujours évincer un pod, il en reste un
unhealthyPodEvictionPolicy: AlwaysAllow un pod cassé ne bloque pas le drain
anti-affinité preferred et non required les deux pods peuvent cohabiter sur un nœud
node group core en max_size = 2 AWS crée le nouveau nœud avant de drainer l'ancien

C'est le résultat de #83 / INC-054 : un minAvailable statique bloquait les drains, il a été remplacé par maxUnavailable, et le HPA est passé à minReplicas: 2.

Le node group observability porte le taint workload=observability:NoSchedule et ne peut donc pas accueillir les pods applicatifs. Ce n'est pas un piège : le node group core monte à deux nœuds pendant le remplacement, la place existe.


Ne pas confondre haute disponibilité et stratégie de montée de version

Ce sont deux sujets distincts, et les mélanger est une erreur fréquente.

  • La haute disponibilité d'un cluster se joue à l'intérieur : plusieurs nœuds, répartis sur plusieurs zones de disponibilité, avec PDB et anti-affinité.
  • Un second cluster répond à « ma région AWS tombe » ou « je veux isoler des charges ». Ce n'est pas la réponse à « je mets à jour mon CNI ».

Sur ce projet, le node group core est à desired_size = 1. Il n'y a donc pas de haute disponibilité aujourd'hui, par choix de coût assumé. La configuration la supporte (max_size = 2, deux sous-réseaux privés, PDB et anti-affinité en place), seul le nombre de nœuds désirés est à changer.


Le client doit suivre : le skew de kubectl

Le contexte, avant le détail

Le skew est l'écart de version toléré entre deux composants qui se parlent. Kubernetes ne garantit qu'une seule version mineure d'écart entre le client kubectl et l'API server.

Au-delà, la panne n'est pas franche. Le client peut sérialiser un champ que l'API server ne connaît pas, ou en ignorer un qu'elle attend. On ne voit pas une erreur, on voit un objet qui n'a pas la forme attendue.

Le constat du 2026-09-07 (#213)

Deux endroits installent kubectl, et un seul était tenu :

Comment Version obtenue
CI (.gitlab-ci-infra.yml) KUBECTL_VERSION: v1.35.0, épinglée 1.35.0
Devcontainer curl $(curl -s .../stable.txt) 1.37.0

Le poste de travail était donc à deux mineures du cluster, là où une seule est garantie. La CI était tenue pendant que le poste dérivait, et rien ne le signalait : stable.txt renvoie la dernière stable du jour, donc la valeur changeait à chaque rebuild d'image.

Ce qui tient la règle maintenant

scripts/check-version-compat.sh a une deuxième section qui refuse deux écarts :

  1. La divergence entre ARG KUBECTL_VERSION du devcontainer et KUBECTL_VERSION de la CI. Un poste qui diverge de la CI produit des diagnostics non reproductibles : le même geste ne donne pas le même résultat des deux côtés.
  2. Le skew de plus d'une mineure entre kubectl et eks_cluster_version.
REFUS : kubectl 1.37.0 est a plus d'une mineure de Kubernetes 1.35.

Le contrôle tourne dans le job version-compat de chaque MR et dans le before_script d'infra-start, aux mêmes deux points que le contrôle EKS / Cilium.

Pourquoi kubectl n'est pas suivi par Renovate

Les autres outils du devcontainer le sont (helm, aws-vault, AWS CLI). Pas kubectl, et c'est une décision.

Sa version est dérivée, pas amont. Elle ne doit pas suivre la dernière release de Kubernetes, elle doit suivre le cluster. Une MR Renovate qui proposerait 1.37 serait rouge par construction tant que le cluster est en 1.35, et ce bruit se répéterait à chaque release.

C'est eks_cluster_version que Renovate surveille (#204). Quand cette valeur monte, le contrôle rappelle que kubectl doit monter avec, aux deux endroits.

Le devcontainer ne lance pas les tests backend

Corollaire tranché dans la même issue. python3 -m venv échoue dans le devcontainer, en 24.04 comme en 26.04, faute du paquet python3-venv. Son python3 vient de la base Ubuntu et n'est pas épinglé.

Ce n'est pas un manque : les tests passent par docker-compose-dev.yaml, qui porte python:3.12-slim au digest, la même image que backend/Dockerfile et que le job run-tests. Le python3 du devcontainer ne sert qu'aux scripts du dépôt, comme scripts/gen_dashboard.py, insensibles à la mineure.

⚠️ Figer Ubuntu ne serait pas la réponse : la base finira par bouger de toute façon.


Ce qui a changé dans le dépôt

Fichier Changement
terraform/modules/eks/variables.tf eks_cluster_version par défaut 1.321.35, avec le raisonnement
terraform/ephemeral/variables.tf idem, c'est ce défaut qui gouverne les montages
.gitlab-ci-infra.yml KUBECTL_VERSION: v1.35.0, définie une seule fois
docs/infra-eks-summary.md, docs/adr/006 version affichée
.devcontainer/Dockerfile ARG KUBECTL_VERSION=v1.35.0 et les autres outils épinglés (#213)
scripts/check-version-compat.sh refuse la divergence CI / poste et le skew de plus d'une mineure (#213)

Le client kubectl était écrit en dur dans deux before_script distincts, et rien n'aurait signalé qu'un bump n'en corrigeait qu'un. Il est désormais dans le bloc variables:, sur le motif déjà utilisé pour les digests d'images.


Reste à faire

  • [ ] Un montage complet en 1.35, application déployée et joignable. Tant que ce n'est pas fait, la version cible est un choix, pas un résultat.
  • [ ] Cilium 1.20 puis Kubernetes 1.36, en issue séparée, avec validation en direct et cilium-pre-flight-check.
  • [ ] Le curl -LO qui télécharge kubectl n'a ni -f ni --retry. Sur une erreur HTTP transitoire, il enregistre le corps HTML de l'erreur et l'installe comme binaire. C'est la famille d'INC-057, corrigée ailleurs mais pas ici.

Sources