コンテンツへスキップ
← ブログ

ipvs モードに削除の期限がついた

Kubernetes 1.37 に含まれた非推奨はちょうど 1 つ、kube-proxy の ipvs モードだ。1.40 でデフォルト無効、1.43 でソースが削除される。nftables への切り替え、カーネルの下限、変わる 4 つの挙動、そして ipvs をまったく使っていないクラスタこそ引っかかる罠をまとめる。

·24分で読めます
  • Kubernetes
  • ネットワーク
  • Linux
  • nftables

Kubernetes v1.37 は 2026年8月26日に 67 個の拡張機能とともに公開され、そのうち非推奨はちょうど 1 つ——kube-proxy の ipvs モードである。gang scheduling や Pod 証明書ほど見出しにはならないが、運用項目としてははるかに大きい。今この瞬間に本番で動いているデータプレーンからコードが削除されて終わる、このリリース唯一の項目だからだ。日程はすでに文書になっている。1.37 でフィーチャーゲート、1.40 でデフォルト無効、1.43pkg/proxy/ipvs の削除。

3 枚のパネルからなるカバー画像。左のパネルは「mode: ipvs」と題され、3 層が積み重なっている。上から IPVS の仮想サーバ、ClusterIP を抱える kube-ipvs0 ダミーインターフェース、そしてその下により幅の広い iptables と ipset の層。全体に「1.43 でソース削除」と書かれた赤いスタンプが押されている。中央の列は 5 つの停留所からなる時系列で、1.35 の警告、1.37 のフィーチャーゲート、1.40 のデフォルト無効、1.43 のコード削除、1.46 のゲート削除が並ぶ。右のパネルは「mode: nftables」と題され、table ip kube-proxy と table ip6 kube-proxy という 2 つの箱が並び、その下に「Linux 5.13 が必要」の 1 行、さらに下に「次のデフォルト」と書かれた緑のバーが引かれている。
実際に退場するもの:3 層のカーネル状態のうち、IPVS なのは最上段だけだ。置き換わるのは 2 つの nftables テーブルと、1 本のカーネル要件である。

これには ConfigMap を 1 行書き換えて終わる版と、ひどい火曜日になる版があり、その差は最初に何を確認したかだけで決まる。以下で扱うのは全体像だ。KEP-5495 の 5 段階と、そのうちどれが意向でどれが日付なのか、自分のクラスタが本当は何で動いているかの調べ方(ipvs より厄介な答えを含む)、このモードが世間の記憶にあるような「iptables からの脱出」では一度もなかった理由、IPVS のスケジューラが後生大事に残されている仕事をしていない理由、カーネルの下限とそれを満たすノードイメージ、文書化された 4 つの挙動変化、1 ノードずつの展開、手で片づけるしかない残留状態、問題があれば非ゼロで終了する検証スクリプト、そして本当に 2 コマンドで済む切り戻し。

1.37 に入った非推奨はちょうど 1 つ、これだ

以下の症状はどれも非推奨とは名乗らない。だから気づくのが遅れる——たいていは 1.40 のアップグレードを担当した人が、最初の警告から 3 リリース遅れて見つけることになる。このモードは劣化しない。存在しなくなるリリースまで完璧に動き続け、その間の警告は誰も grep しないログに流れていく。[rel137]

見えている現象実際の意味どこで扱うか
kube-proxy が起動のたびに The ipvs proxier is now deprecated とログに書くKEP-5495 の段階 1、Kubernetes 1.35 で入った。壊れてはいない。時計が 2 リリース前に動き出しただけだ。5 つの段階
kube-proxy の ConfigMap で mode: が空このクラスタはデータプレーンを選んでいない。上流は推奨デフォルトを iptables から nftables に変える予定で、固定していないクラスタはそれに追随する。モードの確認
1.40 にアップグレードしたあと、kube-proxy が有効なモードを列挙してエラー終了する段階 3。KubeProxyIPVS ゲートのデフォルトが false になり、誰も戻していない。5 つの段階
切り替え直後から、セカンダリインターフェース上の NodePort が応答しなくなったnftables モードのデフォルトは --nodeport-addresses primary。この移行で最も多いリグレッションであり、2 位以下を大きく引き離している。変わる挙動
ある 1 つの ClusterIP 宛の通信が、1 台のノードでだけ捨てられるIPVS ルールの裏付けを失った kube-ipvs0 に、古いアドレスが残っている。もう転送しないアドレスに対して、ノードはまだ ARP を返している。残留物

この表の 2 行目は 2 度読む価値がある。IPVS とは何の関係もないのに、ipvs を一度も使っていないクラスタを捕まえるからだ。Kubernetes 1.37 のデフォルトモードは今も iptables であり、ドキュメントは将来のリリースでそのデフォルトを nftables に変えると明記している。kube-proxy の設定がモードを名指ししていないなら、あなたは判断して iptables にいるのではない。上流がその時点で推奨するものの上にいるのであり、データプレーンの入れ替えが他人のカレンダーに予定されている。[vips]

上流自身の推奨であり、この記事で最も安上がりな作業でもある。アップグレード中にクラスタのプロキシバックエンドが予期せず変更されるのを避けるため、すべてのクラスタでどのモードを使うかを明示的に示す kube-proxy 設定を持つべきである。5 分で終わる変更であり、最終的にどのモードへ落ち着くにせよ、今日やっておく価値がある。

5 つの段階と、そのうち日付なのはどれか

KEP-5495 はこれを 5 つの段階に分けている。有用な規律は、告知にすぎない段階と日付である段階を分けて読むことだ。段階 1 と 2 はすでに起きていて、クラスタの動き方は何も変えていない。段階 3 はデフォルトを変える——つまり、誰も何もしなかったクラスタに何が起きるかを変える。段階 4 はコードを削除する。不可逆なのはこれだけだ。[kep5495]

段階Kubernetes何が変わるか何を求められるか
11.35ipvs モードの kube-proxy が起動時に警告を出す。ドキュメントに非推奨の印が付く。古いクラスタでも移行できるよう、nftables の修正が 1.33 と 1.34 にバックポートされる。気づくこと。それだけ。
21.37KubeProxyIPVS フィーチャーゲートが追加される。GA、デフォルト true。挙動は変わらない。計画すること。静かな最後のリリースだ。
31.40ゲートのデフォルトが false に反転する。上書きせずに ipvs モードで起動した kube-proxy は、有効なモードを列挙してエラー終了する。終わっていること——あるいはゲートを立てて、マイナー 3 リリース分を買う。
41.43pkg/proxy/ipvs がツリーから削除される。フィーチャーゲートではもう戻せない。残っていた記述もドキュメントから消える。何も。ipvs モードは存在しない。
51.46フィーチャーゲートそのものが削除される。ゲートを検査するツールにだけ関係する。

この表のうち 2 行には注釈が要る。1.46 のクリーンアップ段階は KEP には存在するが、この非推奨を扱った記事のほとんどに載っていない。フィーチャーゲートを検査するツールを書いているのでなければ影響はないが、それまで KubeProxyIPVS は実在する、一覧に載った GA のゲートである。もう 1 つ、SIG Network がこれをやる理由として挙げたものは知っておく価値がある。性能ではないからだ。このグループには ipvs バックエンドのコードに詳しいメンテナがおらず、しばらく前から ipvs のバグ報告に対しては nftables へ移るよう案内してきた。誰も直していないバックエンドは、過去のベンチマークが何を言っていようと負債である。[depol][k8srel]

このクラスタが実際に動いているモード

手順書に書いてあることではなく、実際に設定されているものから始める。2 年より長く生きているクラスタなら、その 2 つは別の文書だからだ。答えは 3 通りあり、面白いのは 3 番目だ。ipvsiptables、そして何も書かれていない。[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

次に警告を探す。Kubernetes 1.35 以降ずっとログに出ているはずで、これが「この話は自分に関係がある」ことの最も安上がりな証拠になる。1.37 で追加されたフィーチャーゲートも同じ作業のついでに場所を確認しておく。設定する必要があるからではない——デフォルトで有効で、このリリースでは何も変わらない——1.40 で差し出されるレバーがこれだからだ。そしてそれが期限を消すのではなく、マイナー 3 リリース分だけ延ばすものだと、事前に知っておくほうがいい。[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'

ipvs モードの実体と、記憶されている姿

何が退場するのかは正確に述べておく価値がある。世間に流通している話が、判断を変えてしまう形で間違っているからだ。ipvs モードは Kubernetes 1.8 で、Service 数に比例するルールリストのコストから逃れるために追加された。その目的は果たした。果たさなかったのは iptables の置き換えだ。カーネルの IPVS API 単体では Kubernetes の Service API 全体を表現できない——マスカレードの判断、NodePort のフィルタリング、LoadBalancer の送信元レンジ、エンドポイントのない Service に対する reject の挙動——ので、このモードは IPVS テーブルの下で iptables と ipset を駆動している。v1.37 のアナウンスもまさにそう書いている。他人の言葉を信じたくないなら、自分のノードでルールを数えればいい。[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.

世間の話の後半はスケジューラであり、今も ipvs に留まっているクラスタのほとんどは、これを理由に留まっている。何年も前に誰かが scheduler: "lc" を設定し、以来そのクラスタは最小コネクション数で負荷分散していると理解されてきた。していない。しかもそれはバグではなく設計上そうなる。kube-proxy は全ノードで動き、各インスタンスが自分の IPVS テーブルを持ち、そのテーブルはこのノードが開いたコネクションしか数えていない。つまり「最小コネクション」はクライアントノードごとに計算される。忙しいノードが 20 台あれば、独立したローカルな判断が 20 個あるだけで、1 つのグローバルな判断は存在しない。クラスタ外から届くトラフィックはそのどのカウントにも入っていない。SIG Network は非推奨を書くにあたって、この誤解を明示的に指摘している。[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"'
IPVS スケジューラ1 台のノードでの挙動クラスタにとっての意味
rr(デフォルト)このノードが持つエンドポイント一覧のラウンドロビン。機能的には、iptables モードと nftables モードがバックエンドをランダムに選ぶのと同じこと。ここで失うものは無い。
lcwlcsednqアクティブなコネクションが最も少ないもの。ただしこのノードが数えた範囲で。クラスタ全体の最小コネクションではない。忙しいクライアントノードが N 台あれば独立したローカル判断が N 個できるだけで、クラスタ外のトラフィックはそのどれにも入っていない。
shdh送信元または宛先アドレスのハッシュ。セッションアフィニティと取り違えられやすい。サポートされた等価物は .spec.sessionAffinity: ClientIP で、こちらは Service 単位であり移行後も残る。
lblclblcrローカリティを考慮した最小コネクション。Kubernetes におけるローカリティは .spec.internalTrafficPolicy: Local だ。API が実際に理解しているもので、スケジューラからは見えない。
mh(Maglev)コンシステントハッシュ。kube-proxy は常に mh-port を設定し、mh-fallback を有効にしない。だから実際にはポート込みの送信元ハッシュとして振る舞う。

Service ごとのスケジューラも存在しないし、これまで存在したこともない。ipvs.scheduler は kube-proxy 設定の単一フィールドであり、すべてのノードのすべての Service に適用される——Service API の一部ではなく、アノテーションで変わるものでもない。ここは少し立ち止まる価値がある。人がスケジューラに求めている 2 つのもの、つまりクライアントの固定とノードローカル性には、この移行を無傷で生き延びるサポート済みの答えが API 側にあるからだ。sessionAffinity: ClientIPinternalTrafficPolicy: Local である。[svc][stp]

モード1.37 時点の状態カーネル下で iptables を使うか行き着く先
iptablesデフォルト。非推奨ではない。問わない使う。定義上そうなるいずれデフォルトではなくなる。削除は告知されていない。
ipvs1.35 で非推奨、1.37 でフィーチャーゲート追加問わない使う——マスカレード、NodePort のフィルタリング、LoadBalancer の送信元レンジ、reject ルール1.40 でデフォルト無効、1.43 でソース削除。
nftables1.33 で安定版。ipvs の推奨移行先5.13 以上使わない将来のリリースでデフォルトになる。
kernelspaceWindows ノード専用この件の影響を受けない。

このノード群は nftables モードで動くのか

ここからは唯一の厳しい制約だ。nftables モードは Linux 5.13 以上を必要とし、部分的なサポートもフォールバックもない——それ未満では kube-proxy はそのモードで起動しない。この移行が ConfigMap の編集で済むのか、ノードイメージの作り直しという別プロジェクトになるのかを決めるのがこのチェックなので、計画を書く前にやること。[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
ノードイメージ既定で載るカーネルnftables モード
RHEL 9、Rocky Linux 9、AlmaLinux 95.14
RHEL 106.12
RHEL 8、CentOS 74.18 以前不可——ノードイメージの作り直し
Ubuntu 24.04 LTS6.8
Ubuntu 22.04 LTS5.15
Ubuntu 20.04 LTSGA は 5.4、HWE なら 5.15HWE カーネルのときだけ可
Debian 126.1
Debian 115.10不可
Amazon Linux 20236.1

下限を満たさないノードがあるなら、正直な読み方はたいてい「カーネルにパッチを当てる必要がある」ではなく「そのノードイメージがサポート終了している」であり、正しい対処は作り直しだ。1.40 までに作り直せないなら、逃げ道はフィーチャーゲートではなく iptables モードである。iptables バックエンドは非推奨ではないし、ipvs モードの登場後に性能が大幅に改善しており、上流はまさにこのケースについて ipvs より iptables を勧めている。時期を量っている人のために付け加えると、非推奨の文書は nftables モードに対して古すぎるカーネルはすべて 2026年末までに長期サポートを離れると指摘している。カーネルを理由にした反論には、それ自体に期限があるという主張だ。[kernrel][kep5495]

移行後に挙動が変わる 4 つのこと

挙動は 4 つ変わり、そのうち 3 つはiptables モードから nftables への移行という見出しの下に書かれている。ipvs のユーザーがそれを読んでいる理由はどこにもない。ここでもまったく同じように適用される。移行元ではなく移行先の性質だからだ。[vips]

挙動iptables / ipvs モードnftables モードどうするか
NodePort の待ち受けアドレス絞っていなければローカルの全アドレスデフォルトで --nodeport-addresses primary実際に使われているアドレスを棚卸しし、プライマリ以上が必要なら明示的に設定する。
127.0.0.1 上の NodePortiptables モードではデフォルトで動く利用不可。1.37 でアルファのゲート付きで復活要らないと結論する前に kubeproxy_iptables_localhost_nodeports_accepted_packets_total を読む。
ローカルファイアウォールkube-proxy が NodePort ごとに accept ルールを足す何もしないホストファイアウォール側で NodePort のレンジを許可する。
6.1 未満の conntrack リセットバグ回避策が入るデフォルトでは入らないkubeproxy_iptables_ct_state_invalid_dropped_packets_total を確認し、非ゼロなら --conntrack-tcp-be-liberal を引き継ぐ。
kube-proxy のチェインに載せた自作ルールiptables に KUBE-SERVICESKUBE-SEP-* が存在する状態は table ip kube-proxy / table ip6 kube-proxy にあるもともと API ではなかった。同じ netfilter の優先度に自分のテーブルを差し込む。
  • NodePort がすべてのローカルアドレスで待ち受けなくなる。 iptables モードと ipvs モードでは、type: NodePort の Service は明示的に絞っていない限りノード上のすべての IP で到達できた。nftables モードのデフォルトは --nodeport-addresses primary、つまり Node オブジェクトが持つプライマリの IPv4 と IPv6 アドレスだけで、それ以外はない。セカンダリ NIC、管理用アドレス、フローティング VIP 経由で NodePort に届いていたものはすべて止まる。以前の到達範囲が必要なら明示的に設定する——0.0.0.0/0 で元に戻る——が、先に棚卸しすること。上流はこのデフォルトのほうが本来望まれていたものだと考えている。
  • localhost の NodePort は別問題で、1.37 で状況が変わった。 127.0.0.1:<nodePort> への接続は iptables モードでは動き、nftables モードではまったく動かなかった。Kubernetes 1.37 以降は動かせる。アルファの KubeProxyNFTablesLocalhostNodePorts ゲートを有効にし、nodePortAddressesprimary,localhost に設定するという 2 段構えだ。自分に関係があるかを判断する前にカウンタを読むこと。kube-proxy はループバック経由で NodePort に受け入れたパケットをずっと数えてきた。
  • kube-proxy がファイアウォールに穴を開けてくれなくなる。 iptables モードは NodePort ごとに accept ルールを追加する。過剰に厳しいローカルファイアウォールがなければ通してしまう、という前提に立った動作だ。nftables ベースのファイアウォールに対してこの手は使えないので、nftables モードはここで何もしない。ホストファイアウォールを運用しているなら、NodePort のレンジは自分で許可する必要がある——本来そうあるべきだったとも言えるが、それでも変更は変更だ。
  • conntrack の回避策がデフォルトで入らない。 6.1 より前のカーネルには、Service IP 宛の長寿命な TCP コネクションをリセットしうるバグがある。iptables モードは回避策を入れるが、その回避策自体が別の問題を起こすと後に判明したため、nftables モードは入れない。必要かどうかは判断ではなく計測の問題だ——カウンタがメトリクスにある——必要なら --conntrack-tcp-be-liberal がサポートされた戻し方である。
  • kube-proxy のチェインを参照していた自作のものはすべて壊れる。 ファイアウォールスクリプト、CNI のフック、監視ルールが KUBE-SERVICESKUBE-SEP-* を名前で参照していたなら、そのチェインはもう無い。kube-proxy の状態は table ip kube-proxytable ip6 kube-proxy に移った。これはもともとサポートされた使い方ではなく——上流にはまさにその題名の記事が常設されている——それでも展開のあとではなく前に grep しておく価値がある。静かに失敗するからだ。
# 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

5 つに共通するパターンは同じだ。どれも起動失敗ではない。kube-proxy は立ち上がり、健全だと報告し、大半のトラフィックを正しくさばき、特定の経路だけが止まる。だからこそ下の事前チェックのほうが展開そのものより価値があり、だからこそ 1 台目は丸 1 営業日ひとりにしておく。[reset][ctsysctl]

切り替えは 1 ノードずつ

編集は 3 行だ。規律は、ipvs ブロックを新しいモードの隣に残さずに削除するところにある。死んだ設定は、次にこのクラスタを見た人が「まだ ipvs のままだ」と結論する原因そのものだからだ。kubeadm で作ったクラスタでは 1 つ確認すべきことがある。この ConfigMap は kubeadm upgrade によって再生成されるので、kubectl だけで加えた変更は次のアップグレードで静かに戻される。クラスタ設定の側にも同じ変更を入れること。[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 -

そのうえで、全ノードのデータプレーンを握るものを展開するときのやり方どおりに進める。つまり 1 台、次にプール、そして全台だ。今回はカナリアが異例に安く済む。状態が完全にノード単位で、起動のたびに API サーバから作り直されるからだ。壊せる共有物は存在せず、新しいモードで動けないノードは起動時にカーネルバージョンを添えて失敗する。[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.

ipvs が残していくもの

後始末はこの移行が過去のどの kube-proxy モード変更とも違う部分であり、しかも良い意味で違う。kube-proxy はかつて他モードの後片付けを試みていたが、やめた——KEP-2448 がそのロジックを削除した——理由は、iptables、ipvs、userspace の各バックエンドが同じチェインの一部に書き込んでいたため、片方のモードのルールを消すと動作中のモードのルールまで消してしまったからだ。nftables モードは何も共有しない。状態はすべて自分の 2 つのテーブルの中にある。だからこの向きの切り替えは起動時に古いルールを削除する設計になっているし、必要なときに手で片づけても安全なのだ。[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

残留物の中で 1 つだけ、他より注意を払う価値があるものがある。kube-ipvs0 は ipvs モードがすべての ClusterIP——そしてすべての LoadBalancer IP——を /32 としてノードに束縛するために使うダミーインターフェースであり、MetalLB のレイヤー 2 モードが ipvs クラスタに対して strictARP を要件として文書化している理由そのものだ。nftables モードはそのようなインターフェースを作らないので、要件は適用されなくなり、設定は死んだ設定になる。ところが、アドレスを載せたままこのインターフェースが切り替えを生き延び、裏に IPVS ルールが無い状態になると、ノードはその ClusterIP を主張し続け、ARP に応答し続けながらトラフィックを捨てる。1 台のノードだけにできたブラックホールであり、アプリケーションの断続的な障害として現れる。後始末を「したはず」で済ませず検証すべき、最大の理由がこれだ。[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.

祈るのではなく検証する

検証はこの移行に特有の形をしていて、それは障害モードが決めている。うまくいかないことのほとんどが、kube-proxy を動いたまま、健全なままにしておくのだ。だから Pod が Running であることを確認しても何も証明されない。下のスクリプトは、ConfigMap に書いてあるモードではなく kube-proxy が実際に選んだモード、ipvs の状態が残っていないこと、ダミーインターフェースが消えていること、ルールの同期が成功していることを確認し、そのうえで、点検では絶対に確認できない 1 つのことをやる。そのノード上の Pod から実在の Service へ、実際に接続を張ることだ。[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

その後は 2 週間、3 つのカウンタにアラートを張る価値がある。この一覧に何が無いかにも注目してほしい。Kubernetes のメトリクスリファレンスには iptables 固有のカウンタと nftables 固有のカウンタがあり、ipvs 用は 1 つも無い。今日あなたが ipvs モードに対して持っている監視は、何であれ ipvsadm を使って自分で組んだものだ——この非推奨全体を支えている保守性の議論を、小さく具体的に示している。[metrics]

メトリクス何が分かるかアラートの価値
kubeproxy_sync_proxy_rules_nftables_sync_failures_totalkube-proxy がルールセットを書けていない。データプレーンが API サーバから乖離していく。増加したら常に。恒久的に。
kubeproxy_sync_proxy_rules_nftables_cleanup_failures_total古いと判断したルールを削除できていない。たいていは旧モードの残留物か、同じテーブルに書き込んでいる別の何かだ。切り替え後 2 週間、増加したら。
kubeproxy_sync_proxy_rules_last_timestamp_secondsノードのルールが最後に API サーバと一致した時刻。静かな失敗を捕まえるのはこれだ。5 分より過去になったら。
kubeproxy_iptables_localhost_nodeports_accepted_packets_total何かがループバック経由で NodePort に接続している。切り替えに非ゼロなら、それは着手条件を満たしていない。
kubeproxy_sync_proxy_rules_duration_secondsフル同期にかかる時間。Service 数が十分にあるなら、この移行の性能面の論拠になる。前後でベースラインを取るだけ。アラートは不要。
# 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.

切り戻しと、それで買えるもの

ここでの切り戻しは本当に安い。今年同じノード群に降ってくる他の移行では、そうはいかない。変換すべきデータがなく、ダウングレードすべきディスク上のフォーマットもなく、再起動をまたいで残る状態もない。変更の全体は ConfigMap のキー 1 つと、kube-proxy が起動のたびに API サーバから作り直すノード単位のカーネル状態だけだ。古いキーを戻し、そのノードの Pod を消せば、1 分足らずで元の場所に戻る。[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.

留保が 2 つ、いずれも仕組みではなく範囲の話だ。ipvs モードの kube-proxy は起動時に見つけた nftables のルールを削除する設計だが、切り戻しが最も重要になるのはまさに新しいバックエンドが不調なときなので、手動版を知っておく価値がある。kube-proxy が所有するルールはすべて自分のテーブルの中にあり、nft delete table を 2 回実行すればすべて消える。そして切り戻しが実際に何を買うのかははっきりさせておくこと。悪いメンテナンス枠を終わらせる。1.40 は動かせないし、1.43 も動かせない。[skew]

もう 1 つの答え:kube-proxy を動かさない

この非推奨には名前を挙げておくべき第 2 の答えがあり、クラスタによってはそちらのほうが正しい。kube-proxy を動かすのをやめることだ。Cilium と Calico はどちらも Kubernetes の Service を eBPF で直接実装しており、ルールリストではなくハッシュマップの参照になる。そして両者とも、kube-proxy と並走させるのではなく取り除いた構成を文書化している。すでにどちらかを動かしているなら、ipvs の非推奨は、どのみちやるつもりだった変更に踏み切る誘いかもしれない。[cilium]

ただしこの記事が説明している変更よりはるかに大きな変更であり、そのように評価すべきだ。kube-proxy の置き換えは Service の実装を CNI 側へ移すことであり、データプレーンをそのプロジェクトのリリース周期、カーネル要件——5.13 より低いのではなく高い——そしてデバッグ手法に縛り付ける。締め切りに追われた非推奨の窓の中でやることではない。ほとんどのクラスタにとって正直な順序はこうだ。ConfigMap の編集で済むのだから今 nftables へ移り、eBPF の話はそれ自体の是非と、それ自体の日程で評価する。[calico][netpol]

どの順番でやるか

圧縮すれば、判断はこの記事よりずっと小さい。カーネルが 5.13 以上なら、これは慎重な展開を伴う設定変更だ。そうでないならノードイメージのプロジェクトであり、当面の答えはフィーチャーゲートではなく iptables モードになる。そして kube-proxy の設定がモードを名指ししていないなら、ここに書いた他の何をおいても、今週それを直すこと。[kep5495]

いまの状況やること
ipvs モード、全ノードが 5.13 以上nftables へ切り替える。この記事の全体がそれであり、作業は ConfigMap の編集と慎重な展開だ。
ipvs モード、一部のノードが 5.13 未満そのノードイメージを作り直す。1.40 までに間に合わないなら、フィーチャーゲートを立てるのではなく iptables モードへ移す——iptables は非推奨ではなく、古いカーネルについて上流は ipvs より iptables を勧めている。
iptables モード、計画なしアップグレードで変わらないよう、今日 mode: iptables を明示的に固定する。そのうえでカーネルの下限が許すようになったら nftables へ移る。
モードがまったく設定されていない今週のうちに、どれでもいいので固定する。組織の誰も選んでいない期限が付いているのはこの項目だ。
CNI がすでに kube-proxy を置き換えているやることは無い。ただし想定で済ませず、kube-proxy が本当に動いていないことを確認する。部分的な置き換えも存在する。
  1. どのモードであれ、まず固定する。 kube-proxy の ConfigMap で mode: が空なら、今日のうちに明示的に設定する。この一覧の中で唯一、ipvs がどこにも無いクラスタにも当てはまる項目であり、自分で選んだ覚えのない期限が付いているのもこれだ。
  2. 計画を書く前にカーネルを確認する。 全ノードに対するコマンド 1 本が、これが午後の作業か四半期の案件かを決める。5.13 未満のノードに必要なのはパッチではなく新しいイメージだ。
  3. 鋭い角を決める 2 つのカウンタを計測する。 localhost の NodePort と conntrack の invalid 状態による破棄は、どちらも何かを変える前に kube-proxy 自身のメトリクスから答えが出る。NodePort の待ち受けアドレスの棚卸しも同じ作業でやること。
  4. 1 台、丸 1 営業日、それからプール。 ここで起きるリグレッションは起動失敗ではない。セカンダリインターフェース上のクライアント 1 つが届かなくなるという形なので、表面化するには 1 シフトかかる。1 台目では kube-ipvs0 と ipset の後始末を手で確認する。
  5. ipvs ブロックと strictARP の設定を削除する。 nftables モードではどちらも何もしないし、残しておくことが「移行など無かった」と次の人に結論させる原因になる。そのうえで ConfigMap の生成元にも同じ変更を入れること。でなければ次の kubeadm upgrade が元に戻す。

これは同じ年に同じノード群へ降ってくる複数の変更のひとつで、まとめて読むほうが分かりやすい。Ingress NGINX から Gateway API への移行 はネットワーク側のもう半分で、同じ展開の規律を必要とする。containerd 1.7 から 2.x への移行 は、その下で動くランタイムが独自の退役日程を持っているという話。cgroup v1 から cgroup v2 への移行 は Kubernetes がすでに必須にしてしまったノードレベルの変更だ。そもそもこれらがどれだけ必要なのかを量っているなら、Kubernetes を使うべきでないとき が議論のもう一方の側になる。

よくある質問

Kubernetes 1.37 で kube-proxy の ipvs モードは削除されましたか?

いいえ。Kubernetes 1.37 が追加したのは KubeProxyIPVS フィーチャーゲートで、デフォルトは true です。したがって ipvs モードの挙動は以前とまったく同じです。効いてくる日付は 2 つあります。1 つは 1.40 で、ゲートのデフォルトが false になり、上書きしない限り ipvs モードの kube-proxy はエラー終了します。もう 1 つは 1.43 で、pkg/proxy/ipvs が削除され、ゲートではもう助けられなくなります。ゲート自体は 1.46 で削除されます。

クラスタがどのプロキシモードで動いているか確認するには?

kubeadm で作ったクラスタなら kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}' | grep 'mode:' です。ここが空で返ってきた場合、そのクラスタはモードを選んでおらず、kube-proxy がその時点で推奨するものの上で動いています——1.37 では iptables であり、将来のリリースで nftables になります。この ConfigMap を使わないクラスタでは、代わりに DaemonSet の引数を読むか、ノード上で ipvsadm -L -n を実行してください。

nftables に移行する必要がありますか、それとも iptables モードでもよいですか?

iptables モードは正当な移行先です。非推奨ではなく、カーネルの下限もなく、ipvs モードが登場した後の年月で性能も大幅に改善しました——nftables モードを動かせないほど古いシステムについて、上流は明示的に ipvs より iptables を勧めています。カーネルが許すなら nftables のほうが良い着地点ですし、デフォルトが向かっている先でもあります。それでも「ipvs から iptables へ」は古いノードイメージに対する本物の答えであって、逃げではありません。

kube-proxy の nftables モードに必要なカーネルバージョンは?

Linux 5.13 以上で、対象は Linux ノードのみです。部分的なサポートはありません。それ未満では kube-proxy は nftables モードで起動しません。実際には RHEL 8、CentOS 7、Debian 11、GA カーネルの Ubuntu 20.04 が除外され、RHEL 9 と 10、Debian 12、Ubuntu 22.04 と 24.04、Amazon Linux 2023 は条件を満たします。ディストリビューション名で判断せず、kubectl get nodes -o custom-columns=NAME:.metadata.name,KERNEL:.status.nodeInfo.kernelVersion で確認してください。

設定していた IPVS スケジューラは失われますか?

設定は失われますが、あなたが得られていると思っていた挙動はほぼ確実に失われません。ipvs.scheduler は Service 単位ではなくクラスタ全体で 1 つのフィールドですし、各ノードの IPVS テーブルはそのノードが開いたコネクションしか数えていません——つまり lc はクラスタ全体ではなくクライアントノードごとの最小コネクションです。欲しかったのがクライアントの固定なら、それは Service の .spec.sessionAffinity: ClientIP です。ノードローカル性なら .spec.internalTrafficPolicy: Local です。どちらもこの移行の影響を受けません。

nftables モードに移ったあとも strictARP は必要ですか?

いいえ。strictARP が存在するのは、ipvs モードがすべての ClusterIP と LoadBalancer IP を kube-ipvs0 ダミーインターフェースに束縛し、その結果ノードが MetalLB の管理下にあるアドレスに対して ARP を返してしまうからです。nftables モードはそのインターフェースを作らないので、根本の問題そのものが無くなり、設定は死んだ設定になります——ipvs ブロックごと削除してください。ただし移行した各ノードで kube-ipvs0 が本当に消えているかは確認してください。古いアドレスを載せたインターフェースが残っていると、そのノードでトラフィックがブラックホールに落ちます。

稼働中のクラスタでモードを切り替えても安全ですか?

iptables 系のモードと nftables の間の切り替えは、安全になるよう設計されています。nftables モードは状態をすべて自分の 2 つのテーブルに保持し、起動時に見つけた iptables と ipvs のルールを削除します。切り戻しでも同じことが成り立ちます。これは iptables と ipvs の間の切り替えとは明確に異なります。あちらはチェインを共有していて、だからこそ kube-proxy の自動クリーンアップが KEP-2448 で削除されました。とはいえ触れるノードすべてでデータプレーンを変更する作業には変わりないので、cordon して drain し、kube-proxy の Pod を 1 つだけ再起動し、検証し、そのノードを 1 営業日そのままにしてください。

この移行で最もよく壊れるのは何ですか?

NodePort への到達性です。nftables モードのデフォルトは --nodeport-addresses primary なので、セカンダリ NIC、管理用アドレス、フローティング VIP、あるいは 127.0.0.1 経由で届いていた NodePort が応答しなくなります——その間 kube-proxy は健全なままで、他はすべて動き続けます。どちらのケースも、kube-proxy 自身のメトリクスと現在の nodePortAddresses 設定から、事前に計測できます。

kube-proxy をやめて Cilium や Calico の eBPF に置き換えるべきですか?

現実的な選択肢ですし、クラスタによってはそちらが正解です。ただしモードの切り替えよりはるかに大きな変更です。Service の実装を CNI 側へ移すことになり、データプレーンがそのプロジェクトのリリース、カーネル要件、デバッグ手法に縛られます。非推奨の圧力の下で決めるのは順序が逆です。nftables への移行は期限を消す ConfigMap の編集です。eBPF の話はその後で、それ自体の日程で評価してください。

nftables モードで問題が起きたときの切り戻し方法は?

以前の ConfigMap のキーを戻し、そのノードの kube-proxy の Pod を削除してください。古いモードは起動時に API サーバから状態を作り直すので、1 分足らずでノードは元に戻ります。kube-proxy 自身のクリーンアップが走らない場合——切り戻しが本当に必要なのはまさにそのケースです——kube-proxy が所有するルールはすべて自分のテーブルの中にあるので、nft delete table ip kube-proxynft delete table ip6 kube-proxy ですべて消えます。切り戻しが終わらせるのは悪いメンテナンス枠であって、1.40 も 1.43 も動かせません。

リリース単位の補足をひとつ。1.37 の帳簿は、多くの記事が言っているものとは違う。実際に Pod を ContainerCreating で止めうるのは SELinuxMount の GA 昇格であって、cgroup v1 の失敗は 1.35、Static Pod の制限は 1.34、containerd の崖はこれから来る 1.38 だ。Kubernetes 1.37 へのアップグレードで実際に壊れるものでは、この三つの列を分けたうえで、アップグレードの後ではなく前に走らせるべき監査を示している。

参考資料

一次資料のみ、2026年8月27日に確認した。この非推奨に関するバージョン番号で Kubernetes の公式ドキュメントと二次記事が食い違っている場合、信じるべきはドキュメントと KEP のほうだ——段階の割り当ては一度すでに改訂されている。

  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

Was this useful?