DevOps

kube-proxy en nftables et Gateway API : le plan de migration réseau à boucler avant Kubernetes 1.37

Deux migrations du chemin de données arrivent à échéance en même temps : le mode ipvs de kube-proxy est en fin de vie au profit de nftables, et le contrôleur Ingress NGINX ne reçoit plus aucun correctif de sécurité depuis mars 2026. Voici un plan d'audit, de bascule progressive et de vérification, avec les pièges concrets et un calendrier tenable sur un parc multi-clusters.

| | | 11 min read
Schéma d'un cluster Kubernetes montrant le passage des règles ipvs vers nftables et d'un objet Ingress vers une Gateway API

Les équipes plateforme ont l’habitude des dépréciations : une API v1beta1 qui disparaît, un flag de kubelet qui change de nom. Ce cycle-ci est différent, parce que les deux échéances qui tombent simultanément touchent le chemin de données de production : la façon dont les paquets atteignent vos Services, et la façon dont le trafic externe entre dans le cluster. Aucune des deux ne se règle par un kubectl apply de plus.

Deux ruptures simultanées : ce qui est supprimé, ce qui est seulement déprécié

Il faut séparer proprement les deux dossiers, car ils n’ont ni le même niveau d’urgence ni le même profil de risque.

Dossier 1 — kube-proxy. Le mode nftables est stable depuis la lignée 1.33 et constitue désormais la cible recommandée sur Linux. Le mode ipvs est entré en dépréciation avec 1.35, avec une suppression annoncée pour la lignée 1.36/1.37. Le mode iptables, lui, reste supporté : ne le mélangez pas dans la même urgence, même si sa trajectoire à moyen terme est claire. Point de vigilance : la disponibilité réelle du mode dépend de votre distribution. Vérifiez les release notes de votre fournisseur managé ou de votre installeur avant de fixer une date, car certaines distributions figent encore la valeur par défaut ou embarquent un kube-proxy patché.

Dossier 2 — Ingress NGINX. Ici il n’y a pas de dépréciation progressive : le contrôleur est retiré depuis mars 2026 et ne reçoit plus aucun correctif de sécurité. Un contrôleur d’entrée qui termine les connexions TLS, parse des en-têtes non fiables et tourne souvent avec des capacités réseau élevées est exactement le composant qu’on ne laisse pas non maintenu. C’est un dossier de risque, pas un dossier de dette technique.

Conséquence pratique : le chantier kube-proxy est un chantier d’exécution (technique, contenue, réversible) ; le chantier Ingress est un chantier de conception (choix d’implémentation, réécriture de manifestes, arbitrages fonctionnels). Menez-les en parallèle, mais pas avec les mêmes personnes ni les mêmes fenêtres de changement.

Auditer l’existant avant de toucher à quoi que ce soit

Commencez par le mode réellement actif sur les nœuds, qui n’est pas toujours celui de la ConfigMap — un DaemonSet patché à la main, un node pool créé avant un changement de politique, et l’écart s’installe.

# Ce que dit la configuration
kubectl -n kube-system get cm kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -E '^mode:|^ *strictARP|masqueradeAll'

# Ce que font réellement les nœuds (kube-proxy expose /proxyMode sur 10249)
for n in $(kubectl get nodes -o name | cut -d/ -f2); do
  echo -n "$n: "
  kubectl debug node/$n -q --image=curlimages/curl -- \
    curl -s http://localhost:10249/proxyMode
done

Côté Ingress, l’inventaire qui compte n’est pas le nombre d’objets mais la surface d’annotations propriétaires. C’est elle qui détermine le coût de migration.

kubectl get ingress -A -o json | jq -r '
  .items[] as $i
  | ($i.metadata.annotations // {}) | keys[]
  | select(startswith("nginx.ingress.kubernetes.io"))' \
  | sort | uniq -c | sort -rn

Traitez à part les annotations de type configuration-snippet et server-snippet : ce sont des blocs de configuration NGINX arbitraires, sans aucun équivalent déclaratif ailleurs. Chacun doit être requalifié individuellement en besoin fonctionnel (« ajouter un en-tête », « réécrire avec capture regex », « appliquer un rate limit par IP ») avant de chercher une cible. Ajoutez à l’inventaire les ConfigMap globales du contrôleur, les certificats gérés hors cert-manager et les Services de type LoadBalancer associés, dont vous devrez conserver ou migrer les adresses IP.

Migration kube-proxy : ipvs → nftables

Prérequis : un noyau Linux récent avec le sous-système nf_tables (5.13 ou plus récent en pratique) et un binaire nft en version 1.0.1 minimum sur l’hôte. Sur des nœuds à base d’images immuables (Flatcar, Bottlerocket, COS, Talos), le prérequis est généralement satisfait ; sur un parc RHEL 8 ou Ubuntu 20.04 résiduel, vérifiez-le explicitement, nœud par nœud.

La bascule s’opère par node pool, jamais globalement. Le schéma qui fonctionne :

  1. Créer un node pool cible avec un kube-proxy en mode: nftables (ConfigMap et DaemonSet distincts, sélecteur de nœuds dédié).
  2. Y basculer une charge non critique, puis un service critique à faible volume.
  3. Drainer progressivement les anciens nœuds.
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: nftables
nftables:
  syncPeriod: 30s
  minSyncPeriod: 1s

Le piège numéro un est le nettoyage des règles héritées. Un nœud recyclé conserve des chaînes KUBE-* iptables ou des virtual servers ipvs orphelins, qui continueront à intercepter du trafic sans être resynchronisés. Sur un nœud converti en place, prévoyez une étape explicite :

# Après arrêt de l'ancien kube-proxy, avant démarrage du nouveau
kube-proxy --cleanup

# Contrôles
ipvsadm -Ln | head                       # doit être vide
iptables-save | grep -c 'KUBE-SVC'       # doit tendre vers 0
nft list table ip kube-proxy | head -30  # la nouvelle table

Si vous le pouvez, préférez le remplacement de nœud à la conversion en place : c’est la seule méthode qui garantit l’absence de résidu.

Deux différences de comportement documentées méritent un test dédié. D’abord, en mode nftables, les Services de type NodePort ne sont pas joignables depuis 127.0.0.1 sur le nœud — un raccourci que certains scripts de healthcheck ou sidecars de legacy utilisent encore. Ensuite, la sélection des interfaces exposant les NodePorts suit nodePortAddresses de manière plus stricte : si vous vous appuyiez sur un comportement implicite avec plusieurs interfaces, explicitez la valeur.

Vérifier avant / après : les quatre signaux qui comptent

Un changement de dataplane ne se valide pas au « ça a l’air de marcher ». Établissez une base de mesure avant la bascule, sur les mêmes charges.

  • Latence de programmation. kubeproxy_sync_proxy_rules_duration_seconds et kubeproxy_network_programming_duration_seconds : c’est ici que nftables apporte son gain le plus tangible sur les clusters à plusieurs milliers de Services. Regardez les quantiles p99, pas la moyenne, et pendant un déploiement massif (scale-up d’un Deployment à forte cardinalité d’endpoints).
  • conntrack. Comparez nf_conntrack_count et le taux de nf_conntrack_drop / insert failed avant et après. Une saturation de conntrack se manifeste par des timeouts intermittents, jamais par une erreur claire.
  • sessionAffinity. ClientIP est supporté, mais son implémentation change de mécanisme. Vérifiez explicitement la persistance et son expiration (sessionAffinityConfig.clientIP.timeoutSeconds) sur les applications à état.
  • Trafic hairpin et masquerade. Un pod qui joint son propre Service, ou deux pods du même nœud qui communiquent via ClusterIP, sont les cas classiques de régression. Testez-les depuis un pod, pas depuis le nœud.

Enfin, si vous utilisez trafficDistribution: PreferSameZone sur des Services, revalidez le comportement de débordement : le routage préférentiel de zone n’est utile que s’il bascule proprement vers les autres zones quand les endpoints locaux disparaissent. Coupez une zone en test et mesurez.

Ingress NGINX retiré : choisir une cible, pas seulement un remplaçant

La tentation est de chercher le remplaçant qui minimise la réécriture. C’est une erreur de cadrage : vous allez réécrire vos manifestes de toute façon, autant le faire vers un modèle durable. Gateway API (Gateway, GatewayClass, HTTPRoute et GRPCRoute stables) est la cible par défaut ; l’Ingress reste supporté mais son évolution fonctionnelle est gelée.

Cible Quand la choisir Coût / réserve
Envoy Gateway Vous voulez Gateway API « pur », avec des politiques déclaratives (SecurityPolicy, BackendTrafficPolicy, ClientTrafficPolicy) Nouveau composant à opérer ; extensions spécifiques à l’implémentation
Istio Un service mesh est déjà présent ou prévu Complexité opérationnelle si vous n’utilisez que l’ingress
Cilium Cilium est déjà votre CNI ; consolidation du dataplane Couplage fort CNI / ingress lors des upgrades
NGINX Gateway Fabric Continuité de compétences NGINX Modèle de configuration différent d’Ingress NGINX : ce n’est pas un drop-in
HAProxy (Unified Gateway) Expertise HAProxy interne, besoins L4/L7 mixtes Vérifiez la couverture Gateway API pour vos cas d’usage
Offre commerciale / API gateway managée Besoins forts en authN/Z, quotas, monétisation d’API Verrouillage, coût, sortie du modèle Kubernetes natif

Traduire Ingress → Gateway API sans se raconter d’histoires

ingress2gateway (kubernetes-sigs) fait 80 % du travail mécanique. Les 20 % restants sont le sujet.

Le changement structurant est la séparation des rôles : l’équipe plateforme possède la Gateway (listeners, TLS, adresse exposée), les équipes applicatives possèdent les HTTPRoute et se rattachent à la Gateway via parentRefs. Cela supprime la ConfigMap globale partagée par tous — et donc la classe entière d’incidents où une annotation d’une équipe reconfigure le proxy de tout le monde.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: edge
  namespace: infra-gateway
spec:
  gatewayClassName: envoy
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: "*.example.com"
      tls:
        mode: Terminate
        certificateRefs:
          - name: wildcard-example-com
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels: { gateway-access: "true" }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: checkout
  namespace: shop
spec:
  parentRefs:
    - name: edge
      namespace: infra-gateway
  hostnames: ["shop.example.com"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /api/checkout }
      filters:
        - type: URLRewrite
          urlRewrite:
            path: { type: ReplacePrefixMatch, replacePrefixMatch: / }
      backendRefs:
        - name: checkout-v1
          port: 8080
          weight: 90
        - name: checkout-v2
          port: 8080
          weight: 10

Ce qui n’a pas d’équivalent direct, et qu’il faut traiter cas par cas :

  • Les snippets NGINX. Rien ne les remplace tel quel. Chaque snippet doit devenir soit un filtre standard (RequestHeaderModifier, ResponseHeaderModifier, RequestMirror), soit une politique propre à l’implémentation, soit une fonctionnalité déplacée dans l’application.
  • L’authentification externe (auth-url, auth-signin). Elle existe partout, mais sous des formes différentes : SecurityPolicy avec extAuth chez Envoy Gateway, AuthorizationPolicy / ext_authz côté Istio. Sémantique des redirections et propagation des en-têtes à revalider fonctionnellement.
  • Les réécritures exotiques. Le filtre URLRewrite couvre ReplacePrefixMatch et ReplaceFullPath. Les réécritures à captures regex de rewrite-target sortent du cadre standard : elles imposent une extension spécifique à l’implémentation, ou une simplification des routes.
  • Le rate limiting, CORS avancé, le mTLS client. Disponibles, mais via des CRD propres à chaque implémentation. C’est ici que se joue votre portabilité réelle : documentez chaque écart.

Bascule sans coupure

Le motif éprouvé reste le double ingress. Vous déployez la nouvelle Gateway en parallèle, avec sa propre adresse d’entrée, et vous déplacez le trafic par le DNS avec des poids progressifs (1 %, 10 %, 50 %, 100 %), en gardant l’ancien chemin chaud. Pour les hostnames critiques, doublez d’un canary interne côté route via les weight des backendRefs.

Fixez les critères de rollback avant la première bascule, et rendez-les mesurables : taux de 5xx par route au-delà d’un seuil sur 5 minutes, p99 de latence dégradée d’un facteur défini, échec des sondes de bout en bout, ou toute anomalie sur les métriques de connexions actives. Le rollback lui-même doit être un changement DNS ou un poids de route, jamais un redéploiement.

Deux points souvent oubliés : les TTL DNS (abaissez-les une semaine avant, pas la veille) et les adresses IP sources vues par les backends, qui changent avec le proxy. Si des allowlists, du rate limiting applicatif ou de l’audit s’appuient sur X-Forwarded-For, validez la chaîne de confiance sur la nouvelle Gateway avant d’envoyer du trafic réel.

Ce que v1.37 ajoute à votre plan

La sortie prévue le 26 août 2026 apporte, d’après le sneak peek du 31 juillet, plusieurs éléments qui recoupent ce chantier :

  • Kubelet rootless en bêta. À évaluer après la migration réseau, pas pendant : les contrôleurs d’entrée qui s’appuient sur hostNetwork ou des capacités privilégiées sont précisément ceux qui frottent avec ce modèle. C’est un argument supplémentaire pour choisir une Gateway qui n’exige pas hostNetwork.
  • Ulimits par conteneur. Utile pour les proxies : les limites de descripteurs de fichiers d’un composant qui gère des dizaines de milliers de connexions n’ont plus à être héritées de l’hôte ou bricolées par initContainer.
  • Metrics API en GA et taints/tolerations DRA au niveau device en GA : à intégrer dans la revue d’observabilité et de scheduling, indépendamment du réseau.
  • Sortie KYAML pour kubectl. Anecdotique en apparence, mais appréciable quand vous relisez des manifestes Gateway API générés automatiquement : moins d’ambiguïté de citation et d’indentation dans les diffs de revue.

Checklist et calendrier réaliste

Sur un parc multi-clusters, comptez douze semaines, en séquençant ainsi :

  1. S1–S2 — Audit : mode kube-proxy réel par node pool, inventaire des annotations, requalification fonctionnelle des snippets, versions de noyau et de nft.
  2. S2–S3 — Décision de la cible Gateway API et POC sur un cluster de développement, avec les cas difficiles en premier (auth externe, réécritures, mTLS) — jamais avec un « hello world ».
  3. S3–S5 — Bascule nftables : dev, préproduction, puis un node pool de production non critique. Base de mesure établie avant chaque étape.
  4. S5–S8 — Double ingress en préproduction, réécriture des routes par équipe applicative, tests de bout en bout.
  5. S8–S11 — Bascule DNS pondérée en production, service par service, avec fenêtres de stabilisation.
  6. S11–S12 — Décommissionnement : suppression des anciens contrôleurs, nettoyage des règles résiduelles, retrait des node pools ipvs, mise à jour de la documentation d’exploitation.

Un dernier conseil : ne fusionnez pas les deux bascules dans la même fenêtre de changement. Si un service dégrade après un changement simultané de dataplane L4 et de proxy L7, le diagnostic vous coûtera plus cher que les deux migrations réunies. Le calendrier serré pousse à paralléliser les chantiers ; il ne doit pas pousser à confondre les causes. La vraie valeur de ces douze semaines n’est d’ailleurs pas d’être conforme à la 1.37 — c’est de sortir avec un chemin de données dont chaque comportement est mesuré, et une configuration d’entrée où le périmètre de chaque équipe est explicite dans l’API plutôt qu’implicite dans une ConfigMap partagée.

Unlock the full article

Get full access to in-depth technical content written from real-world experience, not tutorials copied from documentation.