Aller au contenu
← Blog

Le mode ipvs a une date de suppression.

Kubernetes 1.37 n’apporte qu’une seule obsolescence, et c’est le mode ipvs de kube-proxy. Désactivé par défaut en 1.40, code supprimé en 1.43. Voici le basculement vers nftables, le plancher de noyau, les quatre comportements qui changent et le piège qui attrape les clusters qui n’utilisent pas ipvs du tout.

·24 min de lecture
  • Kubernetes
  • Réseau
  • Linux
  • nftables

Kubernetes v1.37 est sorti le 26 août 2026 avec 67 améliorations, dont exactement une obsolescence — et c’est le mode ipvs de kube-proxy. Le titre est plus petit que celui du gang scheduling ou des certificats de Pod, et l’enjeu opérationnel est bien plus grand, parce que c’est la seule chose de cette version qui se termine par la suppression de code dans un plan de données qui tourne en production en ce moment même. Le calendrier est déjà écrit : une feature gate en 1.37, désactivée par défaut en 1.40, pkg/proxy/ipvs supprimé en 1.43.

Image de couverture en trois panneaux. Le panneau de gauche, intitulé « mode : ipvs », montre trois couches empilées : les serveurs virtuels IPVS, l’interface factice kube-ipvs0 qui porte les ClusterIP, et en dessous une couche plus large intitulée iptables et ipset — avec un tampon rouge indiquant « code supprimé en 1.43 ». La colonne centrale est une frise à cinq étapes : 1.35 avertissement, 1.37 feature gate, 1.40 désactivé par défaut, 1.43 code supprimé, 1.46 gate retirée. Le panneau de droite, intitulé « mode : nftables », montre deux blocs intitulés table ip kube-proxy et table ip6 kube-proxy au-dessus d’une ligne indiquant « exige Linux 5.13 », avec en dessous une barre verte portant la mention « le prochain défaut ».
Ce qui part réellement à la retraite : trois couches d’état noyau, dont seule la première est IPVS — remplacées par deux tables nftables et un plancher de version de noyau.

Il existe une version de cette migration qui tient en une ligne de ConfigMap et une autre qui tient en un mardi raté, et toute la différence se joue sur ce que vous vérifiez d’abord. Ce qui suit couvre l’ensemble : les cinq étapes du KEP et lesquelles sont des dates plutôt que des intentions, comment savoir ce que votre cluster exécute réellement (y compris la réponse qui est pire que ipvs), pourquoi ce mode n’a jamais été l’échappatoire à iptables dont on se souvient, pourquoi les scheduler IPVS ne font pas le travail pour lequel on les garde, le plancher de noyau et les images de nœuds qui le franchissent, les quatre changements de comportement documentés, un déploiement nœud par nœud, l’état qu’il faut nettoyer à la main, un script de vérification dont le code de sortie est non nul, et un retour arrière qui tient sincèrement en deux commandes.

Une seule obsolescence en 1.37, et c’est celle-ci

Aucun des symptômes ci-dessous ne dit obsolescence, et c’est précisément pour cela qu’il est remarqué tard — en général par la personne qui pilote la montée en 1.40, trois versions après l’apparition du premier avertissement. Le mode ne se dégrade pas. Il continue de fonctionner parfaitement jusqu’à la version où il n’existe plus, et les avertissements de l’intervalle partent dans un journal que personne ne relit.[rel137]

Ce que vous voyezCe que cela signifie vraimentOù c’est traité
kube-proxy consigne The ipvs proxier is now deprecated à chaque démarrageÉtape 1 de KEP-5495, livrée dans Kubernetes 1.35. Rien n’est cassé ; le compte à rebours a démarré il y a deux versions.Les cinq étapes
mode: est vide dans la ConfigMap kube-proxyLe cluster n’a pas choisi de plan de données. L’amont fera passer le défaut recommandé d’iptables à nftables, et un cluster non figé suivra.Quel mode
Après la montée en 1.40, kube-proxy se termine sur une erreur listant les modes validesÉtape 3. La gate KubeProxyIPVS vaut désormais false par défaut et personne ne l’a remise.Les cinq étapes
Un NodePort a cessé de répondre sur une interface secondaire juste après le basculementLe mode nftables prend --nodeport-addresses primary par défaut. C’est de loin la régression la plus fréquente de cette migration.Ce qui change
Le trafic vers une ClusterIP est jeté, sur un seul nœudAdresses résiduelles sur un kube-ipvs0 oublié, sans plus aucune règle IPVS derrière. Le nœud répond encore à l’ARP pour une adresse qu’il ne sert plus.Les reliquats

La deuxième ligne de ce tableau mérite d’être lue deux fois, parce qu’elle n’a rien à voir avec IPVS et qu’elle attrape des clusters qui ne l’ont jamais utilisé. Le mode par défaut dans Kubernetes 1.37 est toujours iptables, et la documentation dit sans détour qu’une version future basculera ce défaut vers nftables. Si votre configuration kube-proxy ne nomme aucun mode, vous n’êtes pas sur iptables par décision : vous êtes sur ce que l’amont recommande à l’instant présent, et vous avez un remplacement de plan de données programmé dans l’agenda de quelqu’un d’autre.[vips]

La recommandation de l’amont lui-même, et la chose la moins coûteuse de tout cet article : pour éviter que le backend du proxy d’un cluster ne soit modifié de façon inattendue pendant une mise à niveau, assurez-vous que tous les clusters disposent d’une configuration kube-proxy qui indique explicitement le mode à utiliser. C’est un changement de cinq minutes, et il vaut d’être fait aujourd’hui quel que soit le mode sur lequel vous atterrirez.

Les cinq étapes, et lesquelles sont des dates

KEP-5495 découpe l’affaire en cinq étapes, et la discipline utile consiste à séparer celles qui sont des annonces de celles qui sont des dates. Les étapes 1 et 2 ont déjà eu lieu et n’ont rien changé au fonctionnement d’un cluster. L’étape 3 change une valeur par défaut, ce qui revient à changer ce qui arrive à un cluster où personne n’a rien fait. L’étape 4 supprime le code, et c’est la seule qui soit irréversible.[kep5495]

ÉtapeKubernetesCe qui changeCe que cela vous demande
11.35kube-proxy avertit au démarrage en mode ipvs ; la documentation est marquée dépréciée. Les correctifs nftables sont rétroportés en 1.33 et 1.34 pour que les clusters plus anciens puissent encore migrer.En prendre note. Rien d’autre.
21.37La feature gate KubeProxyIPVS est ajoutée : GA, valeur par défaut true. Le comportement ne change pas.Planifier. C’est la dernière version tranquille.
31.40La gate passe à false par défaut. Un kube-proxy démarré en mode ipvs sans la forcer se termine sur une erreur listant les modes valides.Avoir fini — ou poser la gate et acheter trois versions mineures.
41.43pkg/proxy/ipvs est retiré de l’arbre. La feature gate ne peut plus le ramener ; les dernières mentions quittent la documentation.Rien. Il n’y a plus de mode ipvs.
51.46La feature gate elle-même est supprimée.Ne concerne que l’outillage qui inspecte les gates.

Deux de ces lignes méritent une note. L’étape de nettoyage en 1.46 figure dans le KEP et dans à peu près aucune couverture de cette obsolescence, ce qui ne compte que si vous écrivez de l’outillage qui inspecte les feature gates — jusque-là, la gate est une gate réelle, listée et en GA. Et la raison invoquée par SIG Network pour faire tout cela mérite d’être connue, parce que ce n’est pas la performance : le groupe n’a plus de mainteneurs familiers du code du backend ipvs, et invite depuis un moment déjà les personnes qui remontent des bugs ipvs à passer à nftables. Un backend que personne ne corrige est un passif, quels que soient les chiffres que ses tests de performance affichaient jadis.[depol][k8srel]

Dans quel mode ce cluster tourne-t-il vraiment

Commencez par ce qui est réellement configuré, et non par ce que dit le runbook, parce que sur tout cluster de plus de deux ans ce sont deux documents différents. Il y a trois réponses possibles, et c’est la troisième qui est intéressante : ipvs, iptables, ou rien du tout.[kpcfg]

# The one-liner from the v1.37 release announcement. On a kubeadm-built cluster
# the whole of kube-proxy's configuration lives in one ConfigMap key.
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep 'mode:'
# mode: ipvs

# Careful with the empty answer, because it is the most common one and it does
# NOT mean "iptables forever". An unset mode means "whatever kube-proxy decides
# is the recommended default", and upstream says in as many words that a future
# release will change that default from iptables to nftables. If this comes back
# blank, you have a data-plane change scheduled that nobody in your team chose.
#   mode: ""     <- pin it, whichever mode you intend to be on

# Managed clusters and non-kubeadm installers do not necessarily use that
# ConfigMap. Ask the process instead - this is true wherever the config came from.
kubectl -n kube-system get ds kube-proxy \
  -o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n'

# And from the node, which is the only answer that cannot be out of date:
ps -o args= -C kube-proxy
ipvsadm -L -n --daemon 2>/dev/null; ipvsadm -L -n | head -20
# IP Virtual Server version 1.2.1 (size=4096)
# Prot LocalAddress:Port Scheduler Flags
# TCP  10.96.0.1:443 rr
#   -> 192.168.4.11:6443            Masq    1      3          0

# Not every cluster runs kube-proxy at all. If this returns nothing, your CNI
# is implementing Services itself and none of this deprecation reaches you.
kubectl -n kube-system get ds -l k8s-app=kube-proxy

Cherchez ensuite l’avertissement, qui est dans vos journaux depuis Kubernetes 1.35 et qui constitue la preuve la moins chère que le sujet vous concerne. La feature gate ajoutée en 1.37 vaut d’être localisée dans la même passe, non parce qu’il faut la positionner — elle est activée par défaut et ne change rien dans cette version — mais parce que c’est le levier qu’on vous proposera en 1.40, et il vaut mieux savoir à l’avance qu’il ne fait que repousser l’échéance de trois versions mineures au lieu de la supprimer.[gates]

# Stage 1 of the deprecation landed in Kubernetes 1.35: kube-proxy logs a
# warning on every start in ipvs mode. It has been in your logs for two releases.
kubectl -n kube-system logs ds/kube-proxy --tail=-1 --prefix \
  | grep -i -m5 'ipvs.*deprecat'
# The ipvs proxier is now deprecated and may be removed in a future release.
# Please use 'nftables' instead.

# Stage 2 is what 1.37 added: a feature gate. Today it is on by default, so
# nothing changes yet. In 1.40 the default flips and kube-proxy in ipvs mode
# exits with an error unless the gate is set back by hand; in 1.43 the gate
# cannot save you because the code is gone.
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -A3 featureGates

# What the 1.40 override would look like. Write this down as the thing you are
# choosing NOT to depend on, rather than as a plan:
#
#   featureGates:
#     KubeProxyIPVS: true
#
# It buys three minor releases, or roughly nine months at the current cadence,
# and it stops working entirely in 1.43.

# Count how much of the fleet this is really about, because in most clusters
# the answer is "some node pools, historically".
kubectl get nodes -o custom-columns=\
'NODE:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,'\
'KERNEL:.status.nodeInfo.kernelVersion,OS:.status.nodeInfo.osImage'

Ce qu’est le mode ipvs, par opposition au souvenir qu’on en garde

Il vaut la peine d’être précis sur ce qui part à la retraite, parce que la version populaire de l’histoire est fausse d’une manière qui change la décision. Le mode ipvs a été ajouté dans Kubernetes 1.8 pour échapper au coût d’une liste de règles proportionnelle au nombre de Service. Cela, il l’a fait. Ce qu’il n’a jamais fait, c’est remplacer iptables : l’API IPVS du noyau ne sait pas exprimer à elle seule toute l’API Service de Kubernetes — décisions de masquerade, filtrage des NodePort, plages sources des LoadBalancer, comportement de rejet pour un Service sans endpoint — si bien que le mode pilote iptables et ipset sous la table IPVS. L’annonce de la v1.37 le dit exactement ainsi. Comptez les règles sur votre propre nœud si vous préférez ne croire personne sur parole.[kep3866]

# Three separate pieces of kernel state, which is the first surprise for anyone
# who believed ipvs mode meant "no iptables".

# (a) The IPVS virtual servers. This part is what people think of as ipvs mode.
ipvsadm -L -n | wc -l

# (b) A dummy interface holding every ClusterIP - and every LoadBalancer IP -
#     as a /32 on the node. This is why the node answers ARP for addresses it
#     does not own, and therefore why MetalLB in layer-2 mode needs strictARP.
ip -brief addr show kube-ipvs0 | head
# kube-ipvs0  DOWN  10.96.0.1/32 10.96.0.10/32 10.107.44.9/32 ...

# (c) The iptables rules and ipsets that ipvs mode drives underneath, because
#     the kernel IPVS API on its own cannot express masquerade decisions,
#     LoadBalancer source ranges, NodePort filtering or the reject rules for a
#     Service with no endpoints. Count them before you claim to be iptables-free.
ipset list -name | grep -c '^KUBE-'
iptables-save -t nat | grep -c '^-A KUBE-'

# The scheduler in use, which is the setting this whole mode is usually kept for:
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -A4 '^ipvs:'
# ipvs:
#   scheduler: "lc"
#   strictARP: true

# One field, for the whole cluster. There is no per-Service scheduler in
# Kubernetes: it is not in the Service API, and kube-proxy does not read one.
# Whatever is on that line is what every Service on every node gets.

La seconde moitié de la version populaire, ce sont les scheduler, et c’est la raison pour laquelle la plupart des clusters encore en ipvs y sont restés. Quelqu’un a posé scheduler: "lc" il y a des années, et le cluster est compris depuis lors comme faisant de la répartition au moindre nombre de connexions. Il n’en fait pas, et la raison est architecturale plutôt qu’un bug. kube-proxy tourne sur chaque nœud et chaque instance tient sa propre table IPVS, qui ne compte que les connexions ouvertes par ce nœud-là. Le « moindre nombre de connexions » est donc calculé par nœud client : avec vingt nœuds chargés vous avez vingt décisions locales indépendantes et non une décision globale, et le trafic qui arrive de l’extérieur du cluster ne figure dans aucun de ces compteurs. SIG Network a signalé ce malentendu explicitement en rédigeant l’obsolescence.[vips]

# The claim to test: "we run lc so connections go to the least loaded pod".
#
# Every node runs its own kube-proxy with its own IPVS table, and that table
# only counts connections this node opened. Ask two nodes about the same
# Service and the connection counts will not agree - because they are answers
# to different questions.

SVC_IP=$(kubectl get svc -n prod api -o jsonpath='{.spec.clusterIP}')
for node in $(kubectl get nodes -o name | head -3); do
  echo "== ${node#node/}"
  kubectl debug "$node" -q -it --image=busybox --profile=general -- \
    chroot /host ipvsadm -L -n -t "$SVC_IP:8080" 2>/dev/null | tail -n +4
done
# == node-01
#   -> 10.244.1.7:8080   Masq  1  118  4
#   -> 10.244.2.4:8080   Masq  1    0  0      <- zero, from THIS node
# == node-02
#   -> 10.244.1.7:8080   Masq  1    2  1
#   -> 10.244.2.4:8080   Masq  1   96  3      <- the other pod, same Service

# Neither node is wrong. "Least connections" is being computed per client node,
# so with N busy client nodes you get N independent local decisions, not one
# global one. Add a client outside the cluster, or a client behind a
# LoadBalancer that lands on a different node, and IPVS never sees it at all.
#
# Clean up the debug pods. `kubectl debug node/...` names them
# node-debugger-<node>-<suffix> and sets no label, so match on the name.
kubectl get pods -o name | grep '^pod/node-debugger-' | xargs -r kubectl delete

# The two things people actually want from a scheduler have supported answers
# that survive this migration, and neither of them is an IPVS scheduler:
#   sticky clients   -> Service .spec.sessionAffinity: ClientIP
#   keep it local    -> Service .spec.internalTrafficPolicy: Local
kubectl get svc -A -o json | jq -r '
  .items[] | select(.spec.sessionAffinity == "ClientIP")
  | "\(.metadata.namespace)/\(.metadata.name) sessionAffinity=ClientIP"'
scheduler IPVSCe qu’il fait sur un nœudCe que cela signifie pour le cluster
rr (le défaut)Tourniquet sur la liste d’endpoints de ce nœud.Fonctionnellement ce que font déjà les modes iptables et nftables en tirant un backend au hasard. Rien n’est perdu ici.
lc, wlc, sed, nqLe moins de connexions actives, telles que comptées par ce nœud.Ce n’est pas un moindre nombre de connexions à l’échelle du cluster. N nœuds clients chargés prennent N décisions locales indépendantes, et le trafic hors cluster n’est dans aucune d’elles.
sh, dhHachage sur l’adresse source ou destination.Fréquemment confondu avec la persistance de session. L’équivalent supporté est .spec.sessionAffinity: ClientIP, qui est par Service et survit à la migration.
lblc, lblcrMoindre nombre de connexions avec localité.La localité dans Kubernetes, c’est .spec.internalTrafficPolicy: Local, que l’API connaît réellement et que le scheduler ne peut pas voir.
mh (Maglev)Hachage cohérent.kube-proxy pose toujours mh-port et n’active jamais mh-fallback, donc en pratique il se comporte comme un hachage de source avec les ports.

Il n’y a pas non plus de scheduler par Service, et il n’y en a jamais eu. ipvs.scheduler est un champ unique de la configuration de kube-proxy et il s’applique à chaque Service sur chaque nœud — il ne fait pas partie de l’API Service, et aucune annotation ne le modifie. Il vaut la peine de s’arrêter là-dessus un instant, parce que cela signifie que les deux choses que l’on va réellement chercher dans ces scheduler, la persistance de session et la localité de nœud, ont des réponses supportées dans l’API qui traversent cette migration sans une égratignure : sessionAffinity: ClientIP et internalTrafficPolicy: Local.[svc][stp]

ModeÉtat en 1.37NoyauUtilise iptables en dessousComment cela se termine
iptablesLe défaut. Non déprécié.indifférentOui, par constructionCesse d’être le défaut à un moment donné ; aucune suppression n’est annoncée.
ipvsDéprécié depuis 1.35 ; feature gate ajoutée en 1.37indifférentOui — masquerade, filtrage des NodePort, plages sources des LoadBalancer, règles de rejetDésactivé par défaut en 1.40, code supprimé en 1.43.
nftablesStable depuis 1.33 ; le remplaçant recommandé d’ipvs5.13 ou plus récentNonDevient le défaut dans une version future.
kernelspaceNœuds Windows uniquementRien de tout cela ne le concerne.

Ces nœuds peuvent-ils faire tourner le mode nftables

Vient maintenant la seule contrainte dure. Le mode nftables exige Linux 5.13 ou plus récent, sans support partiel et sans repli — en dessous, kube-proxy ne démarrera pas dans ce mode. C’est la vérification qui décide si cette migration est une modification de ConfigMap ou un projet d’images de nœuds, alors faites-la avant d’écrire le moindre plan.[vips]

#!/usr/bin/env bash
# Precheck. nftables mode needs Linux 5.13 or newer; there is no partial
# support and no fallback - kube-proxy will not start in nftables mode below it.
set -euo pipefail

need_major=5 need_minor=13
fail=0

while read -r node kernel os; do
  ver=${kernel%%-*}                       # 6.8.0-51-generic -> 6.8.0
  IFS=. read -r maj min _ <<<"$ver"
  if (( maj > need_major )) || { (( maj == need_major )) && (( min >= need_minor )); }; then
    printf '  ok    %-22s %s\n' "$node" "$kernel"
  else
    printf '  TOO OLD %-20s %-24s %s\n' "$node" "$kernel" "$os"
    fail=1
  fi
done < <(kubectl get nodes -o custom-columns=\
'NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion,OS:.status.nodeInfo.osImage' \
  --no-headers)

(( fail == 0 )) && echo "all nodes can run nftables mode" \
                || echo "some nodes cannot: rebuild the image or stay on iptables mode"

# On a node that reports too old, the answer is almost never "patch the kernel".
# It is "this node image is end of life". Check what the distro offers before
# planning anything: an Ubuntu 20.04 node on the HWE kernel is fine, the same
# release on the GA kernel is not.
uname -r
nft --version
Image de nœudNoyau livré par défautMode nftables (5.13 minimum)
RHEL 9, Rocky Linux 9, AlmaLinux 95.14Oui
RHEL 106.12Oui
RHEL 8, CentOS 74.18 et antérieursNon — c’est une reconstruction d’image de nœud
Ubuntu 24.04 LTS6.8Oui
Ubuntu 22.04 LTS5.15Oui
Ubuntu 20.04 LTS5.4 en GA, 5.15 avec HWEUniquement avec le noyau HWE
Debian 126.1Oui
Debian 115.10Non
Amazon Linux 20236.1Oui

Si certains nœuds ne franchissent pas le plancher, la lecture honnête est en général que l’image de nœud est en fin de vie, et non que le noyau a besoin d’un correctif ; le bon correctif est une reconstruction. Si cette reconstruction ne peut pas avoir lieu avant 1.40, le repli est le mode iptables et non la feature gate : le backend iptables n’est pas déprécié, ses performances se sont nettement améliorées après l’introduction du mode ipvs, et l’amont le recommande explicitement à la place d’ipvs dans ce cas précis. À ajouter pour qui pèse le calendrier : l’obsolescence note que tous les noyaux trop anciens pour le mode nftables quittent le support à long terme d’ici la fin de 2026, ce qui est l’argument selon lequel l’objection du noyau a elle aussi une date de péremption.[kernrel][kep5495]

Quatre comportements changent ensuite

Quatre comportements changent, et trois d’entre eux sont documentés sous un titre — migrer du mode iptables vers nftables — que les utilisateurs d’ipvs n’avaient aucune raison de lire. Ils s’appliquent ici à l’identique, parce que ce sont des propriétés de la destination et non de la source.[vips]

ComportementMode iptables / ipvsMode nftablesQuoi en faire
Adresses d’écoute des NodePortToutes les adresses locales, sauf restriction--nodeport-addresses primary par défautAuditer les adresses réellement utilisées, puis positionner l’option explicitement s’il vous faut plus que la primaire.
NodePort sur 127.0.0.1Fonctionne par défaut en mode iptablesIndisponible ; rétabli en 1.37 derrière une gate alphaLire kubeproxy_iptables_localhost_nodeports_accepted_packets_total avant de conclure que vous n’en avez pas besoin.
Pare-feu localkube-proxy ajoute des règles d’acceptation par NodePortNe fait rienAutoriser la plage NodePort dans votre propre pare-feu d’hôte.
Bug de réinitialisation conntrack avant 6.1Contournement installéNon installé par défautVérifier kubeproxy_iptables_ct_state_invalid_dropped_packets_total ; si non nul, conserver --conntrack-tcp-be-liberal.
Vos propres règles sur les chaînes de kube-proxyKUBE-SERVICES et KUBE-SEP-* existent dans iptablesL’état vit dans table ip kube-proxy / table ip6 kube-proxyCela n’a jamais été une API. Accrochez plutôt votre propre table à la même priorité netfilter.
  • Les NodePort cessent d’écouter sur toutes les adresses locales. En mode iptables comme en mode ipvs, un Service type: NodePort est joignable sur toutes les IP locales tant que vous n’avez pas restreint la liste. Le mode nftables prend par défaut --nodeport-addresses primary, c’est-à-dire l’adresse IPv4 et/ou IPv6 primaire du nœud telle qu’elle figure dans l’objet Node, et rien d’autre. Tout ce qui atteignait un NodePort par une carte réseau secondaire, une adresse d’administration ou une VIP flottante cesse de fonctionner. Positionnez l’option explicitement si vous avez besoin de la portée d’avant — 0.0.0.0/0 la rétablit — mais faites l’audit d’abord, parce que ce défaut est celui que l’amont considère comme réellement souhaité.
  • Les NodePort sur localhost sont un cas à part, et 1.37 l’a changé. Se connecter à 127.0.0.1:<nodePort> fonctionnait en mode iptables et ne fonctionnait pas du tout en mode nftables. Depuis Kubernetes 1.37 c’est possible, derrière la gate alpha KubeProxyNFTablesLocalhostNodePorts et avec nodePortAddresses réglé sur primary,localhost. Avant de décider si le sujet vous concerne, lisez le compteur : kube-proxy compte depuis toujours les paquets acceptés sur les NodePort en boucle locale.
  • kube-proxy cesse d’ouvrir votre pare-feu à votre place. Le mode iptables ajoute des règles d’acceptation pour chaque NodePort, en partant du principe qu’un pare-feu local trop zélé les bloquerait sinon. Cette approche ne peut pas fonctionner face à un pare-feu basé sur nftables, donc le mode nftables ne fait plus rien ici. Si vous avez un pare-feu d’hôte, c’est désormais à lui d’autoriser la plage NodePort — ce qui est sans doute la façon dont cela aurait toujours dû se passer, et reste un changement.
  • Le contournement conntrack n’est pas installé par défaut. Les noyaux antérieurs à 6.1 ont un bug qui peut réinitialiser des connexions TCP longues vers des IP de Service. Le mode iptables installe un contournement ; on a découvert ensuite que ce contournement causait ses propres problèmes, donc le mode nftables ne le pose pas. Savoir si vous en avez besoin se mesure au lieu de se juger — le compteur est dans les métriques — et si c’est le cas, --conntrack-tcp-be-liberal est la manière supportée de retrouver le comportement.
  • Tout ce qui, chez vous, s’accrochait aux chaînes de kube-proxy est cassé. Si un script de pare-feu, un hook CNI ou une règle de supervision référençait KUBE-SERVICES ou KUBE-SEP-* par leur nom, ces chaînes ont disparu : l’état de kube-proxy vit maintenant dans table ip kube-proxy et table ip6 kube-proxy. Cela n’a jamais été supporté — l’amont a un billet de blog permanent intitulé exactement ainsi — mais cela vaut d’être cherché avant le déploiement plutôt qu’après, parce que l’échec est silencieux.
# The behaviour difference most likely to page you. In ipvs and iptables mode,
# NodePort Services are reachable on every local address unless you said
# otherwise. nftables mode defaults to --nodeport-addresses primary: the node's
# primary IPv4 and/or IPv6 address from the Node object, and nothing else.
#
# So the question to answer before the switch is: does anything reach a NodePort
# on a secondary address, a VIP, a management NIC, or on loopback?

# What is currently configured, if anything:
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' | grep -i nodePortAddresses
# (empty means "all local addresses")

# Which NodePorts exist at all, and who might be pointed at them:
kubectl get svc -A -o json | jq -r '
  .items[] | select(.spec.type == "NodePort" or .spec.type == "LoadBalancer")
  | .spec.ports[]? | select(.nodePort)
  | "\(.nodePort)\t\(.protocol)"' | sort -u

# Localhost NodePorts are the sharp edge: health checks, sidecars and a
# surprising number of monitoring agents connect to 127.0.0.1:<nodePort>.
# kube-proxy counts them for you, and a non-zero value means something out
# there depends on it.
kubectl -n kube-system exec ds/kube-proxy -- \
  wget -qO- http://127.0.0.1:10249/metrics \
  | grep kubeproxy_iptables_localhost_nodeports_accepted_packets_total
# kubeproxy_iptables_localhost_nodeports_accepted_packets_total 41822

# Kubernetes 1.37 gives that case a way out, as an alpha feature gate. If you
# need it, you need it on 1.37+ and you need both halves:
#   featureGates:
#     KubeProxyNFTablesLocalhostNodePorts: true
#   nodePortAddresses: ["primary", "localhost"]

# And the conntrack workaround: iptables mode installs one for a pre-6.1 kernel
# bug that resets long-lived TCP connections. nftables mode does not, by default.
# Non-zero here means you are relying on it - carry --conntrack-tcp-be-liberal.
kubectl -n kube-system exec ds/kube-proxy -- \
  wget -qO- http://127.0.0.1:10249/metrics \
  | grep kubeproxy_iptables_ct_state_invalid_dropped_packets_total

Le motif est le même dans les cinq cas : aucun n’est un échec au démarrage. kube-proxy démarre, se déclare en bonne santé, sert correctement l’essentiel du trafic, et un chemin précis cesse de fonctionner. C’est pour cela que la vérification préalable ci-dessous vaut plus que le déploiement lui-même, et pour cela que le premier nœud reste seul pendant une journée de travail complète.[reset][ctsysctl]

Le basculement, un nœud à la fois

La modification tient en trois lignes. La discipline consiste à supprimer le bloc ipvs au lieu de le laisser à côté du nouveau mode, parce que la configuration morte est exactement ce qui fait conclure à la personne suivante que le cluster est toujours en ipvs. Un point à vérifier sur les clusters bâtis avec kubeadm : cette ConfigMap est régénérée par kubeadm upgrade, donc un changement fait uniquement avec kubectl peut être silencieusement annulé à la prochaine mise à niveau. Faites-le aussi dans la configuration du cluster.[kubeadm]

# The change is three lines, and the discipline is in what you delete.
kubectl -n kube-system get configmap kube-proxy \
  -o jsonpath='{.data.config\.conf}' > kube-proxy.conf.bak
cp kube-proxy.conf.bak kube-proxy.conf

# --- before ----------------------------------------------------------------
#   mode: ipvs
#   ipvs:
#     scheduler: "lc"
#     strictARP: true
#     syncPeriod: 30s
#
# --- after -----------------------------------------------------------------
#   mode: nftables
#   nftables:
#     minSyncPeriod: 1s
#     syncPeriod: 30s
#
# Delete the whole ipvs block rather than leaving it. In nftables mode it is
# dead configuration: scheduler has no equivalent and no effect, and strictARP
# was only ever there to stop the node answering ARP for the ClusterIPs that
# ipvs mode bound onto kube-ipvs0 - an interface nftables mode never creates.
# Leaving it behind is how the next person concludes the cluster is still ipvs.

# Apply. Note that on a kubeadm cluster this ConfigMap is regenerated by
# `kubeadm upgrade`, so make the same change in the cluster configuration or
# the next upgrade will quietly put ipvs back.
kubectl -n kube-system create configmap kube-proxy \
  --from-file=config.conf=kube-proxy.conf \
  --dry-run=client -o yaml | kubectl apply -f -

Déployez ensuite comme vous déployeriez n’importe quoi qui détient le plan de données sur chaque nœud, c’est-à-dire un nœud, puis un pool, puis le parc. Celui-ci est inhabituellement peu coûteux à passer en canari, parce que l’état est entièrement local au nœud et reconstruit depuis l’API server à chaque démarrage : il n’y a rien de partagé à corrompre, et un nœud incapable d’exécuter le nouveau mode échoue au démarrage avec la version du noyau dans le message.[drain]

# Do not restart the DaemonSet across the fleet. kube-proxy owns the data plane
# on every node it runs on; a bad rollout is a cluster-wide outage, and this one
# is cheap to canary because the state is per node.

# Pause the DaemonSet so the ConfigMap change does not roll on its own.
kubectl -n kube-system patch ds kube-proxy \
  -p '{"spec":{"updateStrategy":{"rollingUpdate":{"maxUnavailable":1}}}}'

# One node. Cordon it, move the workloads off, restart only that pod.
NODE=node-07
kubectl cordon "$NODE"
kubectl drain "$NODE" --ignore-daemonsets --delete-emptydir-data --timeout=5m
kubectl -n kube-system delete pod \
  --field-selector "spec.nodeName=$NODE" -l k8s-app=kube-proxy

# Watch it come up in the new mode. A node that cannot run nftables mode fails
# here, loudly, with the kernel version in the message - which is the correct
# place to find that out.
kubectl -n kube-system logs -f --tail=40 \
  "$(kubectl -n kube-system get pod -l k8s-app=kube-proxy \
     --field-selector "spec.nodeName=$NODE" -o name)"

kubectl uncordon "$NODE"

# Give it real traffic and a working day before the second node. The failures
# this migration produces are not startup failures; they are "one client on a
# secondary interface stopped reaching a NodePort", and that takes a shift to
# surface. Then a pool, then the fleet.

Ce que le mode ipvs laisse derrière lui

Le nettoyage est l’endroit où cette migration diffère de tous les changements de mode kube-proxy précédents, et c’est en bien. kube-proxy essayait autrefois de faire le ménage après les autres modes, puis a cessé — KEP-2448 a supprimé cette logique — parce que les backends iptables, ipvs et userspace écrivaient tous dans une partie des mêmes chaînes, si bien que nettoyer les règles d’un mode supprimait aussi celles du mode en cours d’exécution. Le mode nftables ne partage rien : tout son état tient dans ses deux tables à lui. C’est pour cela que basculer dans ce sens est conçu pour retirer les anciennes règles au démarrage, et pour cela que nettoyer à la main est sans danger quand il le faut.[kep2448][ipvsadm]

# kube-proxy in nftables mode is designed to remove the iptables and ipvs rules
# it finds on startup - the modes do not share state, which is exactly why this
# direction is safe when switching between the iptables-family modes was not.
# Verify rather than assume, on the first node, before the second one.

ipvsadm -L -n | tail -n +4 | wc -l         # want 0
ip link show kube-ipvs0 2>/dev/null        # want "does not exist"
ipset list -name | grep -c '^KUBE-'        # want 0
iptables-save -t nat | grep -c '^-A KUBE-' # want 0 (or only your own rules)

# If something survived - an older kube-proxy, a node that was rebooted mid-way,
# a third party that wrote into those chains - clear it explicitly. Every one of
# these is safe once kube-proxy is confirmed running in nftables mode on the node.
ipvsadm --clear
ip link delete kube-ipvs0                  # recreated only by ipvs mode
for s in $(ipset list -name | grep '^KUBE-'); do ipset destroy "$s"; done

# The stale kube-ipvs0 addresses are the ones that actually hurt. Left in place
# with no IPVS rules behind them, the node still claims those ClusterIPs and
# still answers ARP for them, and traffic that lands there is dropped rather
# than redirected. That is a black hole that looks like an application problem.

# The strictARP sysctls are set at runtime and do not revert on their own. They
# are harmless, but if you want the node back to stock:
sysctl -w net.ipv4.conf.all.arp_ignore=0
sysctl -w net.ipv4.conf.all.arp_announce=0

# There is also a supported flush. It is documented as cleaning up iptables and
# ipvs rules, so it is the right tool for this direction and the wrong one for
# the other. Run it with kube-proxy stopped on that node.
kube-proxy --cleanup

Un reliquat mérite plus d’attention que les autres. kube-ipvs0 est une interface factice que le mode ipvs utilise pour lier chaque ClusterIP — et chaque IP de LoadBalancer — en /32 sur le nœud, ce qui est toute la raison pour laquelle MetalLB en mode couche 2 documente strictARP comme un prérequis des clusters ipvs. Le mode nftables ne crée aucune interface de ce genre, donc ce prérequis cesse de s’appliquer et le réglage devient de la configuration morte. Mais si l’interface survit au basculement avec les adresses encore posées dessus et plus aucune règle IPVS derrière, le nœud continue de revendiquer ces ClusterIP et de répondre à l’ARP pour elles tout en jetant le trafic. C’est un trou noir sur un seul nœud, qui se présente comme une panne applicative intermittente, et c’est la meilleure raison qui soit de vérifier le nettoyage au lieu de le supposer.[metallb][chains]

# Everything kube-proxy now owns lives in two tables of its own, one per IP
# family, which is the property that makes the whole thing inspectable.
nft list tables
# table ip kube-proxy
# table ip6 kube-proxy

# The Service map, which is the equivalent of what `ipvsadm -L -n` used to show.
# Note that it is a map lookup rather than a rule chain: this is the performance
# argument for the new backend, and it is visible in the output.
nft list table ip kube-proxy | head -40

# One Service end to end:
SVC_IP=$(kubectl get svc -n prod api -o jsonpath='{.spec.clusterIP}')
nft list table ip kube-proxy | grep -A3 "$SVC_IP"

# Live rule changes, which is the closest thing to watching kube-proxy think:
nft monitor rules

# What kube-proxy does NOT own any more - and must not, if the cleanup worked:
nft list ruleset | grep -c 'KUBE-SVC\|KUBE-SEP'   # want 0
ipvsadm -L -n | tail -n +4 | wc -l               # want 0

# A standing caution that predates all of this: kube-proxy's chains and tables
# are not API. If something of yours matched on KUBE-SERVICES by name, it is
# broken now, and it was unsupported before. Hook your own table into the same
# netfilter priorities instead of writing into kube-proxy's.

Vérifier plutôt qu’espérer

La vérification a une forme précise sur cette migration, dictée par les modes de défaillance : presque tout ce qui tourne mal laisse kube-proxy en marche et en bonne santé. Constater que le pod est Running n’établit donc strictement rien. Le script ci-dessous contrôle le mode que kube-proxy a réellement choisi plutôt que celui de la ConfigMap, qu’aucun état ipvs n’a survécu, que l’interface factice a disparu, que la synchronisation des règles réussit — puis fait la seule chose qu’aucune inspection ne peut faire, à savoir ouvrir une vraie connexion vers un vrai Service depuis un pod de ce nœud.[dbgsvc]

#!/usr/bin/env bash
# Run on a migrated node, from a machine with kubectl and cluster access.
# Exits non-zero on anything that would be silently wrong. The failures this
# migration produces do not stop kube-proxy, so "the pod is Running" proves
# nothing at all.
set -uo pipefail
NODE=${1:?usage: verify.sh <node>}
NS=${NS:-default}
rc=0
say() { printf '%-46s %s\n' "$1" "$2"; }
chk() { if [[ $2 == "$3" ]]; then say "$1" "ok"; else say "$1" "FAIL ($2 != $3)"; rc=1; fi; }

POD=$(kubectl -n kube-system get pod -l k8s-app=kube-proxy \
      --field-selector "spec.nodeName=$NODE" -o jsonpath='{.items[0].metadata.name}')

# 1. the mode kube-proxy actually chose, not the one in the ConfigMap
mode=$(kubectl -n kube-system logs "$POD" | grep -om1 'Using .* Proxier' | awk '{print $2}')
chk "proxy mode" "$mode" "nftables"

# 2. no ipvs state left on the node
left=$(kubectl debug "node/$NODE" -q --image=busybox --profile=general -- \
       chroot /host sh -c 'ipvsadm -L -n 2>/dev/null | tail -n +4 | wc -l' 2>/dev/null | tr -d ' ')
chk "ipvs virtual servers remaining" "${left:-0}" "0"

# 3. the dummy interface is gone, so no stale ClusterIP black holes
iface=$(kubectl debug "node/$NODE" -q --image=busybox --profile=general -- \
        chroot /host sh -c 'ip link show kube-ipvs0 >/dev/null 2>&1 && echo present || echo absent' \
        2>/dev/null | tr -d ' ')
chk "kube-ipvs0" "${iface:-absent}" "absent"

# 4. sync is succeeding, which is the counter that replaces "is it up"
m=$(kubectl -n kube-system exec "$POD" -- wget -qO- http://127.0.0.1:10249/metrics)
fails=$(awk '/^kubeproxy_sync_proxy_rules_nftables_sync_failures_total/ {s+=$2} END{print s+0}' <<<"$m")
chk "nftables sync failures" "$fails" "0"
awk '/^kubeproxy_sync_proxy_rules_last_timestamp_seconds/ {print "  last successful sync:", $2}' <<<"$m"

# 5. and the part no inspection can establish: a real connection to a real
#    Service, from a pod on this node, plus a NodePort from off-box.
kubectl -n "$NS" run "nftcheck-$$" --rm -i --restart=Never \
  --overrides="{\"spec\":{\"nodeName\":\"$NODE\"}}" \
  --image=curlimages/curl -- \
  curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 \
  http://kubernetes.default.svc.cluster.local:443 >/dev/null 2>&1 \
  && say "in-cluster Service connect" "ok" \
  || { say "in-cluster Service connect" "FAIL"; rc=1; }

exit $rc

Ensuite, trois compteurs méritent une alerte pendant une quinzaine de jours. Notez ce qui manque à cette liste : la référence des métriques Kubernetes contient des compteurs propres à iptables et des compteurs propres à nftables, et absolument rien pour ipvs. Quelle que soit la supervision que vous avez aujourd’hui pour le mode ipvs, vous l’avez construite vous-même autour d’ipvsadm — ce qui est une petite illustration très concrète de l’argument de maintenance qui sous-tend toute l’obsolescence.[metrics]

MétriqueCe qu’elle vous ditVaut une alerte
kubeproxy_sync_proxy_rules_nftables_sync_failures_totalkube-proxy n’arrive pas à écrire le jeu de règles, donc le plan de données dérive par rapport à l’API server.Toute augmentation, en permanence.
kubeproxy_sync_proxy_rules_nftables_cleanup_failures_totalIl n’arrive pas à retirer des règles qu’il croit périmées — en général des reliquats de l’ancien mode, ou quelque chose d’autre qui écrit dans les mêmes tables.Toute augmentation pendant la quinzaine qui suit le basculement.
kubeproxy_sync_proxy_rules_last_timestamp_secondsQuand les règles du nœud ont pour la dernière fois correspondu à l’API server. C’est celle qui attrape la panne silencieuse.Plus de cinq minutes dans le passé.
kubeproxy_iptables_localhost_nodeports_accepted_packets_totalQuelque chose se connecte à un NodePort par la boucle locale.Valeur non nulle avant le basculement, comme point bloquant.
kubeproxy_sync_proxy_rules_duration_secondsDurée d’une synchronisation complète. L’argument de performance de la migration, si vous avez assez de Service pour qu’il existe.À mesurer avant et après ; pas d’alerte.
# kube-proxy serves these on 10249 on every node. Three of them are worth an
# alert for the fortnight after the migration; the rest are for the postmortem.

# Sync is failing on this node - the data plane is drifting from the API server:
sum by (node) (rate(kubeproxy_sync_proxy_rules_nftables_sync_failures_total[5m])) > 0

# Cleanup is failing - usually leftovers from the previous mode, or something
# else writing into the same tables:
sum by (node) (rate(kubeproxy_sync_proxy_rules_nftables_cleanup_failures_total[5m])) > 0

# Rules are stale. This is the one that catches the silent failure, because
# kube-proxy stays Running while it happens:
time() - max by (node) (kubeproxy_sync_proxy_rules_last_timestamp_seconds) > 300

# And the pre-migration baseline worth keeping: sync duration before and after.
# In clusters with a few thousand Services the improvement is the point; in a
# cluster with forty Services there is nothing to see and that is fine too.
histogram_quantile(0.99, sum by (le) (rate(kubeproxy_sync_proxy_rules_duration_seconds_bucket[5m])))

# Note what is not in this list. The Kubernetes metrics reference has
# iptables-specific counters and nftables-specific counters and no ipvs-specific
# counter at all. Whatever observability you have for ipvs mode today, you built
# it yourself out of ipvsadm - which is its own argument about maintenance.

Revenir en arrière, et ce que cela achète

Le retour arrière est ici sincèrement peu coûteux, ce qui n’est pas vrai des autres migrations qui atterrissent sur ces nœuds cette année. Il n’y a pas de données à convertir, pas de format sur disque à rétrograder, et aucun état qui survive à un redémarrage : tout le changement tient en une clé de ConfigMap plus un état noyau local au nœud que kube-proxy reconstruit depuis l’API server à chaque démarrage. Remettez l’ancienne clé, supprimez le pod sur ce nœud, et le nœud est revenu où il était en moins d’une minute.[kep3866]

# Rollback here is genuinely cheap, which is not true of most migrations on
# these nodes. There is no data to convert and no format to downgrade: the whole
# of the change is one ConfigMap key and per-node kernel state that is rebuilt
# from the API server on every start.

kubectl -n kube-system create configmap kube-proxy \
  --from-file=config.conf=kube-proxy.conf.bak \
  --dry-run=client -o yaml | kubectl apply -f -

kubectl -n kube-system delete pod \
  --field-selector "spec.nodeName=$NODE" -l k8s-app=kube-proxy

# kube-proxy in ipvs mode removes the nftables rules it finds on startup. If it
# does not - and rollback matters most exactly when the new backend is
# misbehaving - the manual version is two commands, because every rule
# kube-proxy owns is inside its own tables:
nft delete table ip kube-proxy
nft delete table ip6 kube-proxy

# Then confirm the old data plane is actually back, rather than assuming:
ipvsadm -L -n | tail -n +4 | wc -l     # want non-zero again
ip -brief addr show kube-ipvs0         # the dummy interface returns

# Be honest about what the rollback bought. ipvs mode is off by default in
# Kubernetes 1.40 and the code is deleted in 1.43. A rollback ends a bad
# maintenance window; it does not move the date.

Deux réserves, toutes deux sur la portée plutôt que sur le mécanisme. kube-proxy en mode ipvs est conçu pour supprimer au démarrage les règles nftables qu’il trouve, mais le retour arrière compte surtout précisément quand le nouveau backend se comporte mal, alors autant connaître la version manuelle : chaque règle que kube-proxy possède est dans ses propres tables, et deux commandes nft delete table retirent l’ensemble. Et soyez clair sur ce que le retour arrière achète réellement. Il met fin à une mauvaise fenêtre de maintenance. Il ne déplace pas 1.40, et il ne déplace pas 1.43.[skew]

L’autre réponse : plus de kube-proxy du tout

Il existe une seconde réponse à cette obsolescence, qui mérite d’être nommée parce que pour certains clusters c’est la meilleure : cesser de faire tourner kube-proxy. Cilium et Calico implémentent tous deux les Service de Kubernetes directement en eBPF, avec une recherche dans une table de hachage au lieu d’une liste de règles, et tous deux documentent un fonctionnement avec kube-proxy retiré plutôt qu’à côté. Si vous exécutez déjà l’un des deux, l’obsolescence d’ipvs est peut-être l’invitation à faire un changement que vous alliez faire de toute façon.[cilium]

C’est un changement bien plus lourd que celui décrit dans cet article, et il doit être pesé comme tel. Remplacer kube-proxy déplace l’implémentation des Service dans le CNI, ce qui couple votre plan de données au rythme de publication de ce projet, à ses exigences de noyau — qui sont plus hautes que 5.13, pas plus basses — et à ses outils de débogage, et ce n’est pas une chose à décider dans une fenêtre d’obsolescence sous pression du calendrier. Le séquencement honnête pour la plupart des clusters est : passer à nftables maintenant parce que c’est une modification de ConfigMap, et évaluer la question eBPF sur ses propres mérites et à son propre rythme.[calico][netpol]

L’ordre dans lequel faire tout ça

Résumée, la décision est bien plus petite que l’article. Si vos noyaux sont en 5.13 ou plus récents, c’est un changement de configuration assorti d’un déploiement soigneux. S’ils ne le sont pas, c’est un projet d’images de nœuds, et la réponse intermédiaire est le mode iptables plutôt que la feature gate. Et si votre configuration kube-proxy ne nomme aucun mode, c’est cela qu’il faut corriger cette semaine, indépendamment de tout le reste.[kep5495]

Votre situationCe qu’il faut faire
Mode ipvs, tous les nœuds en 5.13 ou plus récentPasser à nftables. C’est tout l’objet de cet article, et cela tient en une modification de ConfigMap plus un déploiement soigneux.
Mode ipvs, certains nœuds en dessous de 5.13Reconstruire ces images de nœuds. Si cela ne peut pas se faire avant 1.40, basculez-les en mode iptables plutôt que de poser la feature gate — iptables n’est pas déprécié et l’amont le recommande devant ipvs pour les vieux noyaux.
Mode iptables, aucun planFiger mode: iptables explicitement dès aujourd’hui pour qu’une mise à niveau ne puisse pas le changer, puis passer à nftables quand votre plancher de noyau le permettra.
Aucun mode positionnéLe figer cette semaine, quel qu’il soit. C’est le point dont personne dans votre organisation n’a choisi l’échéance.
Le CNI remplace déjà kube-proxyRien à faire. Confirmer que kube-proxy ne tourne réellement pas plutôt que de le supposer, parce que les remplacements partiels existent.
  1. Figez le mode, quel qu’il soit. Si mode: est vide dans la ConfigMap kube-proxy, positionnez-le explicitement dès aujourd’hui. C’est le seul point de la liste qui s’applique aux clusters sans le moindre ipvs, et c’est celui dont l’échéance n’a pas été choisie par vous.
  2. Vérifiez les noyaux avant d’écrire un plan. Une commande sur tout le parc décide si l’affaire prend un après-midi ou un trimestre. Les nœuds en dessous de 5.13 ont besoin d’une nouvelle image, pas d’un correctif.
  3. Mesurez les deux compteurs qui décident des arêtes vives. Les NodePort sur localhost et les paquets jetés en état invalide par conntrack se lisent tous deux dans les métriques de kube-proxy, avant de changer quoi que ce soit. Auditez les adresses d’écoute des NodePort dans la même passe.
  4. Un nœud, une journée de travail entière, puis un pool. Les régressions ne sont pas des échecs au démarrage ; ce sont des clients isolés sur une interface secondaire, et il faut une journée pour les faire remonter. Vérifiez à la main le nettoyage de kube-ipvs0 et des ipset sur ce premier nœud.
  5. Supprimez le bloc ipvs et le réglage strictARP. Ni l’un ni l’autre ne fait quoi que ce soit en mode nftables, et les laisser est ce qui fera conclure à la personne suivante que la migration n’a jamais eu lieu. Reportez ensuite le même changement là où la ConfigMap est générée, sinon le prochain kubeadm upgrade l’annulera.

C’est l’un de plusieurs changements qui atterrissent sur les mêmes nœuds la même année, et ils se lisent mieux ensemble : le passage d’Ingress NGINX à la Gateway API, qui est l’autre moitié du travail réseau et partage la même discipline de déploiement ; la migration de containerd 1.7 vers 2.x, le runtime en dessous, sur son propre calendrier de retraite ; et la migration de cgroup v1 vers cgroup v2, le changement au niveau du nœud que Kubernetes a déjà rendu obligatoire. Si vous pesez la quantité de tout cela dont vous avez réellement besoin, quand ne pas utiliser Kubernetes est l’autre versant de l’argument.

Questions fréquentes

Le mode ipvs de kube-proxy est-il supprimé dans Kubernetes 1.37 ?

Non. Kubernetes 1.37 ajoute la feature gate KubeProxyIPVS, dont la valeur par défaut est true, si bien que le mode ipvs se comporte exactement comme avant. Les dates qui comptent sont 1.40, quand la gate passe à false par défaut et que kube-proxy en mode ipvs se termine sur une erreur si vous ne la forcez pas, et 1.43, quand pkg/proxy/ipvs est supprimé et que la gate ne peut plus rien. La gate elle-même disparaît en 1.46.

Comment savoir quel mode de proxy utilise mon cluster ?

Sur un cluster bâti avec kubeadm : kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}' | grep 'mode:'. Si la réponse est vide, le cluster n’a choisi aucun mode et tourne sur ce que kube-proxy recommande à cet instant — c’est iptables en 1.37, et ce deviendra nftables dans une version future. Sur un cluster qui n’utilise pas cette ConfigMap, lisez plutôt les arguments du DaemonSet, ou lancez ipvsadm -L -n sur un nœud.

Suis-je obligé de migrer vers nftables, ou puis-je rester en mode iptables ?

Le mode iptables est une destination légitime. Il n’est pas déprécié, il n’a pas de plancher de noyau, et ses performances se sont nettement améliorées dans les années qui ont suivi l’introduction du mode ipvs — l’amont le recommande explicitement devant ipvs pour les systèmes trop anciens pour le mode nftables. nftables est la meilleure cible là où le noyau le permet, et c’est vers lui que va le défaut, mais « d’ipvs vers iptables » est une vraie réponse pour les vieilles images de nœuds, pas une dérobade.

Quelle version de noyau exige le mode nftables de kube-proxy ?

Linux 5.13 ou plus récent, et sur des nœuds Linux uniquement. Il n’y a pas de support partiel : kube-proxy ne démarrera pas en mode nftables en dessous. En pratique cela écarte RHEL 8, CentOS 7, Debian 11 et Ubuntu 20.04 sur le noyau GA, et cela laisse passer RHEL 9 et 10, Debian 12, Ubuntu 22.04 et 24.04, et Amazon Linux 2023. Vérifiez avec kubectl get nodes -o custom-columns=NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion plutôt que par nom de distribution.

Vais-je perdre le scheduler IPVS que j’ai configuré ?

Vous perdrez le réglage, et très probablement pas le comportement que vous croyiez en tirer. ipvs.scheduler est un champ unique pour tout le cluster, pas par Service, et la table IPVS de chaque nœud ne compte que les connexions ouvertes par ce nœud — donc lc, c’est le moindre nombre de connexions par nœud client et non à l’échelle du cluster. Si ce que vous vouliez était la persistance de session, c’est .spec.sessionAffinity: ClientIP sur le Service. Si c’était la localité de nœud, c’est .spec.internalTrafficPolicy: Local. Ni l’un ni l’autre n’est affecté par la migration.

Ai-je encore besoin de strictARP après le passage au mode nftables ?

Non. strictARP existe parce que le mode ipvs lie chaque ClusterIP et chaque IP de LoadBalancer sur l’interface factice kube-ipvs0, ce qui fait répondre le nœud à l’ARP pour des adresses que MetalLB essaie de contrôler. Le mode nftables ne crée jamais cette interface, donc le problème sous-jacent disparaît et le réglage devient de la configuration morte — supprimez-le avec le reste du bloc ipvs. Vérifiez tout de même que kube-ipvs0 a bien disparu sur chaque nœud migré, parce qu’une interface résiduelle avec des adresses périmées fera un trou noir dans le trafic.

Est-il sûr de changer de mode sur un cluster en production ?

Passer d’un mode de la famille iptables à nftables est conçu pour l’être : le mode nftables garde tout son état dans ses deux tables à lui et retire au démarrage les règles iptables et ipvs qu’il trouve, et la réciproque vaut au retour arrière. C’est spécifiquement différent d’un passage entre iptables et ipvs, qui partageaient des chaînes, et c’est pourquoi le nettoyage automatique de kube-proxy a été supprimé par KEP-2448. Cela reste un changement de plan de données sur chaque nœud touché : passez le nœud en cordon, lancez un drain, redémarrez un seul pod kube-proxy, vérifiez, faites uncordon et laissez ce nœud tranquille pendant une journée de travail.

Qu’est-ce qui casse le plus souvent dans cette migration ?

L’accessibilité des NodePort. Le mode nftables prend --nodeport-addresses primary par défaut, donc un NodePort qui était atteint par une carte réseau secondaire, une adresse d’administration, une VIP flottante ou sur 127.0.0.1 cesse de répondre — pendant que kube-proxy reste en bonne santé et que tout le reste continue de fonctionner. Les deux cas se mesurent à l’avance dans les métriques de kube-proxy et dans le réglage nodePortAddresses actuel.

Faut-il plutôt remplacer kube-proxy par Cilium ou Calico en eBPF ?

C’est une vraie option, et pour certains clusters la meilleure, mais c’est un changement bien plus lourd qu’un changement de mode : il déplace l’implémentation des Service dans le CNI et couple votre plan de données aux versions, aux exigences de noyau et aux outils de débogage de ce projet. Le décider sous la pression d’une obsolescence, c’est prendre le problème à l’envers. Passer à nftables est une modification de ConfigMap qui supprime l’échéance ; évaluez la question eBPF ensuite, à son propre rythme.

Comment revenir en arrière si le mode nftables se comporte mal ?

Restaurez la clé précédente de la ConfigMap et supprimez le pod kube-proxy sur ce nœud ; l’ancien mode reconstruit son état depuis l’API server au démarrage, donc le nœud est revenu en moins d’une minute. Si le nettoyage propre à kube-proxy ne s’exécute pas — ce qui est exactement le cas où le retour arrière compte —, chaque règle qu’il possède est dans ses propres tables, donc nft delete table ip kube-proxy et nft delete table ip6 kube-proxy retirent l’ensemble. Le retour arrière met fin à une mauvaise fenêtre de maintenance ; il ne déplace ni 1.40 ni 1.43.

Une note au niveau de la version, car le bilan de la 1.37 n’est pas celui que l’on lit partout : ce qui peut réellement laisser des pods en ContainerCreating, c’est le passage de SELinuxMount en GA - l’échec cgroup v1 date de la 1.35, la restriction des pods statiques de la 1.34, et la falaise containerd est encore devant vous, en 1.38. ce qui casse réellement lors du passage à Kubernetes 1.37 sépare les trois colonnes et donne l’audit à mener avant la mise à niveau, pas après.

Sources

Sources primaires uniquement, vérifiées le 27 août 2026. Là où la documentation Kubernetes et un article secondaire divergent sur un numéro de version dans cette obsolescence, ce sont la documentation et le KEP qu’il faut croire — le calendrier a déjà été révisé une fois.

  1. Kubernetes - Virtual IPs and Service Proxies: the reference page for every kube-proxy mode. It carries the deprecation notice for ipvs mode with the 1.40 and 1.43 dates, the kernel 5.13 requirement for nftables mode, the full list of IPVS schedulers and the ipvs.scheduler field they are set through, the four documented behaviour differences when migrating to nftables, and the sentence that matters most to clusters that are not on ipvs at all: the default mode is iptables in 1.37 and a future release will change it to nftables
  2. Kubernetes v1.37: Garhwal - the release announcement of 26 August 2026. One of the 67 enhancements is a deprecation and it is this one. The deprecation section states the timeline, gives the jsonpath one-liner for finding out which mode a cluster is running, and says outright that ipvs mode continues to use iptables underneath because the kernel IPVS API alone cannot implement Kubernetes Services
  3. KEP-5495: Deprecate ipvs mode in kube-proxy - the enhancement proposal itself, and the only document that carries all five stages. Stage 2 in 1.37 adds the KubeProxyIPVS feature gate; stage 3 in 1.40 flips it off by default; stage 4 in 1.43 removes pkg/proxy/ipvs; the cleanup stage in 1.46 removes the gate. It also records why: SIG Network has no maintainers familiar with the ipvs backend, and the kernels too old for nftables mode will be out of LTS by the end of 2026
  4. KEP-3866: Add an nftables-based kube-proxy backend - the design document for the mode you are migrating to, including the section titled "The ipvs mode of kube-proxy will not save us", the reasoning behind switching modes being safe in this direction when it was not between the iptables-family modes, and the two nft commands that remove every rule kube-proxy owns
  5. KEP-265: IPVS load balancing mode in Kubernetes - the original 2017 proposal, worth reading now mainly to see which of its promises were kept and which were quietly not
  6. KEP-2448: Remove kube-proxy automatic clean-up logic - why kube-proxy stopped trying to tidy up after the other modes, and therefore why the cleanup step in this migration is something you do rather than something that happens
  7. kube-proxy command line reference: --proxy-mode, --cleanup, --nodeport-addresses, --conntrack-tcp-be-liberal, --ipvs-scheduler, --ipvs-strict-arp and the rest. Note that --cleanup is documented as cleaning up iptables and ipvs rules, which is the direction that matters here
  8. KubeProxyConfiguration API reference: the schema of the config.conf that lives in the kube-proxy ConfigMap, including the mode field, the ipvs and nftables sections, and which options are read in which mode
  9. Kubernetes feature gates: where KubeProxyIPVS and KubeProxyNFTablesLocalhostNodePorts are listed with their stage and default, and the reference for how a gate is passed to a component that is not the API server
  10. Kubernetes metrics reference: the exact names of the kube-proxy counters used in this article, including kubeproxy_sync_proxy_rules_nftables_sync_failures_total, kubeproxy_iptables_ct_state_invalid_dropped_packets_total and kubeproxy_iptables_localhost_nodeports_accepted_packets_total. Also, by omission, the fact that there is no ipvs-specific counter anywhere in the list
  11. Kubernetes deprecation policy: what a deprecation of a component flag or behaviour actually commits the project to, which is the frame for reading the KEP-5495 stages as dates rather than intentions
  12. Kubernetes releases: the supported branches and their end-of-life dates, which is how you turn "1.40" and "1.43" into calendar quarters for your own cluster
  13. Kubernetes version skew policy: how far kube-proxy is allowed to lag the API server and the kubelet, which bounds how long a partially migrated fleet can stay partially migrated
  14. Kubernetes v1.35: Timbernetes - the release where stage 1 of this deprecation landed and kube-proxy started logging a warning on startup in ipvs mode. If nobody in your organisation noticed, that is the point
  15. Kubernetes v1.36: Haru - the release in between, useful for placing the deprecation on the same timeline as the other node-level changes of 2026
  16. Kubernetes v1.37 sneak peek: the pre-announcement of the same deprecation, published four weeks before the release
  17. Kubernetes - Service: the API that all of this implements, and the reference for sessionAffinity, which is the feature people mistakenly believe the IPVS sh scheduler is providing
  18. Kubernetes - EndpointSlices: the objects kube-proxy actually watches, and the reason rule-sync cost scales with endpoint churn rather than with Service count alone
  19. Kubernetes - Service internal traffic policy: the supported way to keep traffic on the local node, which is the thing IPVS locality-based schedulers are sometimes reached for instead
  20. Kubernetes - Cluster networking: where kube-proxy sits relative to the CNI plugin, which decides whether any of this applies to your cluster at all
  21. Kubernetes - Debug Services: the official checklist for a Service that does not answer, and the first thing to run when a node comes back on a new proxy mode
  22. Kubernetes - Safely drain a node: the cordon, drain and uncordon sequence this migration slots into, one node at a time
  23. Kubernetes - Upgrading kubeadm clusters: for kubeadm-built clusters, the place the kube-proxy DaemonSet and its ConfigMap come from, and the reason a config change can be reverted by the next upgrade if it is not also made in the cluster configuration
  24. Kubernetes blog - Kubernetes's iptables chains are not API: the standing warning that anything of yours which hooks into kube-proxy's own chains is unsupported. It is the single best predictor of what will break when the chains are replaced by nftables tables
  25. Kubernetes blog - IPVS-based in-cluster load balancing deep dive: the 2018 introduction to the mode being retired, including the kube-ipvs0 dummy interface and the ipset usage that this article tells you to go and clean up
  26. Kubernetes blog - kube-proxy subtleties, debugging an intermittent connection reset: the original write-up of the conntrack invalid-state problem whose workaround nftables mode does not install by default
  27. kubernetes/kubernetes, pkg/proxy/ipvs: the directory KEP-5495 stage 4 deletes. Worth a look if you want to see for yourself how much iptables the ipvs mode is driving
  28. kubernetes/kubernetes, pkg/proxy/nftables: the implementation you are moving to, and the authority on which table and chain names to expect on a migrated node
  29. nftables wiki: the syntax reference for reading what kube-proxy now writes, in particular sets, maps and verdict maps, which are the features the iptables API cannot express and the reason the new backend is faster
  30. nft(8) manual page: list, delete, monitor and the ruleset commands used in the verification section
  31. The netfilter project's nftables page: the upstream statement of what nftables replaces and why development moved there
  32. Linux kernel documentation - nf_conntrack sysctls: nf_conntrack_tcp_be_liberal, which is what --conntrack-tcp-be-liberal sets, and the surrounding timeouts kube-proxy also manages
  33. kernel.org - active kernel releases: the longterm branches and their projected end-of-life dates, which is how to check the KEP's claim that every kernel too old for nftables mode leaves LTS by the end of 2026
  34. ipvsadm(8): the tool for reading and clearing the IPVS table that kube-proxy leaves behind, including -L -n for inspection and -C for the flush used in the cleanup step
  35. MetalLB installation: the source of the strict ARP requirement for kube-proxy in ipvs mode. It is an ipvs-only requirement because it works around an ipvs-only behaviour, which is why it stops applying after this migration
  36. Cilium - Kubernetes without kube-proxy: the other answer to this deprecation, which is to stop running kube-proxy at all and let an eBPF data plane implement Services
  37. Calico - enabling the eBPF data plane: the same answer from the other major CNI, including the requirement to disable kube-proxy rather than run both

Cet article vous a été utile ?