はじめに
kube-proxy に nftables モードが入って、GA してからもうしばらく経ちます。ノード数・Pod数が多いクラスタでは iptables のルール同期が遅れるという長年の悩みがあり、それを直すための変更なので、影響の大きさが気になって調べてみました。
この記事では、その状況に対する Kubernetes 本体の答えである nftables モードについて、なぜ作られたのか・現状どこまで進んでいるのか・今後どういうスケジュールでデフォルトが変わっていく想定なのかを、KEP とソースコードで裏取りしながら整理します。結論からいうと、nftables モードはまだデフォルトではありません。1.33 で GA していますが、既定の iptables モードを置き換える計画は別の KEP として切り出されていて、しかもその KEP はまだ provisional(検討段階)のステータスです。あわせて、iptables モードの性能問題に対して EKS・GKE・AKS がそれぞれどう対応しているかも見ていきます。
対象コンポーネントとバージョン
本記事の記載内容は、次のバージョン・時点を対象にしています。
| コンポーネント | バージョン/時点 | リポジトリ | 本記事での役割 |
|---|---|---|---|
| Kubernetes | v1.37.1 | https://github.com/kubernetes/kubernetes | kube-proxy の nftables backend 実装、feature gate |
| kind | v0.33.0 | https://github.com/kubernetes-sigs/kind | 検証環境。ノードイメージは kindest/node:v1.37.0
|
KEP-5343「Make nftables the default kube-proxy backend」は、本記事執筆時点(2026-09-29)で kep.yaml の status が provisional です。つまりまだ「実装可能」として承認されたKEPではなく、Alpha/Beta/GA として書かれているマイルストーンも確定した計画ではなく 提案中の目標です。本記事の「今後」の章はこの前提で読んでください。
TL;DR
- iptables モードには、カーネル側の仕組みに起因する直しようのない性能問題があります。 ルールを1本足すだけでもロックを取って全ルールをダウンロード・再アップロードする制御プレーンの問題と、Service 数に比例して1パケットあたりの処理コストが増えるデータプレーンの問題の両方があります。
-
iptablesコマンドの実体は、Debian 10 以降・Ubuntu 22.04 以降・RHEL 9 時点などですでに nftables です。 ただし kube-proxy のiptablesモードはこの恩恵を受けません。Service ごとに個別のチェーンとjumpを積む構造自体は変わらないため、保存形式が nftables でも O(n) の性能特性のままです。 -
つまり「nftables モード」のコアな変更は「nftables 技術を使うかどうか」ではありません。 カーネル側での nftables 化はモード設定と無関係にもう起きています。kube-proxy の
nftablesモードが実際に切り替えているのは、iptables の語彙(chain + jump)を捨てて、verdict map という nftables 独自のルール構造を使うかどうかです。 - nftables モードは KEP-3866 で Kubernetes 1.29 に Alpha、1.31 に Beta、1.33 に GA(ロック済み)で入りました。 verdict map という仕組みで、Service 数によらずほぼ O(1) でパケットを振り分けます。
-
ipvsモードは 1.37 で非推奨になりました。 1.40 でデフォルト無効、1.43 で削除予定です。nftables モードはiptablesだけでなくipvsの後継でもあります。 -
それでも 2026-09 時点の既定値は依然として
iptablesです。 デフォルトを切り替える計画(KEP-5343)は 1.37 で警告ログを出し始めた段階で、GA(デフォルト変更)は 1.40 を目標にしていますが、この計画自体がまだprovisionalです。 - iptables にあって nftables モードに無かった機能も埋まりつつあります。 localhost 経由の NodePort アクセスは KEP-6032 で userspace TCP プロキシとして追加中(1.37 Alpha → 1.38 Beta)です。
- クラウド事業者の対応は割れています。 EKS と AKS は kube-proxy の nftables モードをそのまま公式サポートしていますが、GKE は kube-proxy 自体を Cilium ベースの Dataplane V2 に置き換える方向で、nftables モードへの言及は見当たりません。しかも Dataplane V2 は新規クラスタ作成時にしか選べず、既存クラスタへの後付けはできません。
iptables の概要と課題
kube-proxy の iptables モードは、Service ごと・Endpoint ごとに iptables ルールを作成し、宛先 IP・ポートへのマッチで対象の Pod へ宛先 NAT する仕組みです。
When kube-proxy on a node sees a new Service, it installs a series of iptables rules which redirect from the virtual IP address to more iptables rules, defined per Service. The per-Service rules link to further rules for each backend endpoint, and the per-endpoint rules redirect traffic (using destination NAT) to the backends.
言葉で説明されてもピンとこないので、実際に kind でクラスタを立てて確認してみます。
cat <<'EOF' > kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
EOF
kind create cluster --config kind-config.yaml --image kindest/node:v1.37.0
# Endpoint が2つの ClusterIP Service を1つ作る
kubectl create deployment demo --image=registry.k8s.io/pause:3.10 --replicas=2
kubectl expose deployment demo --port=80 --target-port=80 --type=ClusterIP
このとき実際に作られた Service と EndpointSlice はこうなっていました(本題と関係の薄いメタデータは省略しています)。
apiVersion: v1
kind: Service
metadata:
name: demo
namespace: default
spec:
clusterIP: 10.96.82.126
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: demo
type: ClusterIP
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: demo-fn7dg
namespace: default
labels:
kubernetes.io/service-name: demo
addressType: IPv4
ports:
- port: 80
protocol: TCP
endpoints:
- addresses: ["10.244.0.3"]
conditions:
ready: true
- addresses: ["10.244.0.4"]
conditions:
ready: true
kube-proxy はこの2つのオブジェクトを見て、iptables-save -t nat の出力にこう反映します(コメントの default/demo に注目してください)。
-A KUBE-SERVICES -d 10.96.82.126/32 -p tcp -m comment --comment "default/demo cluster IP" -m tcp --dport 80 -j KUBE-SVC-73JNOU5FOFXIWZLE
-A KUBE-SVC-73JNOU5FOFXIWZLE ! -s 10.244.0.0/16 -d 10.96.82.126/32 -p tcp -m comment --comment "default/demo cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-73JNOU5FOFXIWZLE -m comment --comment "default/demo -> 10.244.0.3:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-XQYOYKNJY53JUFN6
-A KUBE-SVC-73JNOU5FOFXIWZLE -m comment --comment "default/demo -> 10.244.0.4:80" -j KUBE-SEP-2EIDLAYDFSWGXYQW
-A KUBE-SEP-XQYOYKNJY53JUFN6 -s 10.244.0.3/32 -m comment --comment "default/demo" -j KUBE-MARK-MASQ
-A KUBE-SEP-XQYOYKNJY53JUFN6 -p tcp -m comment --comment "default/demo" -m tcp -j DNAT --to-destination 10.244.0.3:80
-A KUBE-SEP-2EIDLAYDFSWGXYQW -s 10.244.0.4/32 -m comment --comment "default/demo" -j KUBE-MARK-MASQ
-A KUBE-SEP-2EIDLAYDFSWGXYQW -p tcp -m comment --comment "default/demo" -m tcp -j DNAT --to-destination 10.244.0.4:80
1行ずつ、Service・EndpointSlice のどのフィールドに対応するかを見ていきます。
-
-A KUBE-SERVICES -d 10.96.82.126/32 -p tcp ... --dport 80 -j KUBE-SVC-73JNOU5FOFXIWZLE
Service.spec.clusterIP(10.96.82.126)と.spec.ports[0].port(80)がそのまま-d・--dportに埋め込まれた、この Service 宛パケットの入口です。マッチしたら Service 単位のチェーンKUBE-SVC-73JNOU5FOFXIWZLEへjumpします。 -
-A KUBE-SVC-73JNOU5FOFXIWZLE ! -s 10.244.0.0/16 -d 10.96.82.126/32 ... -j KUBE-MARK-MASQ
送信元が Pod ネットワーク(10.244.0.0/16)の外から来た場合だけ、後で SNAT(masquerade)するための印を付けます。クラスタ外や NodePort 経由のトラフィックが対象です。 -
-A KUBE-SVC-73JNOU5FOFXIWZLE ... -m statistic --mode random --probability 0.5 -j KUBE-SEP-XQYOYKNJY53JUFN6
EndpointSlice.endpoints[0].addresses(10.244.0.3)に対応する1本目の振り分けルールです。1/2 の確率でマッチし、この Endpoint 専用のチェーンへjumpします。 -
-A KUBE-SVC-73JNOU5FOFXIWZLE ... -j KUBE-SEP-2EIDLAYDFSWGXYQW
EndpointSlice.endpoints[1].addresses(10.244.0.4)に対応する2本目の振り分けルールです。確率指定が無いので「1本目でマッチしなかった残り全部」、つまり実質的にもう1/2を担います。Endpoint が増えるほど、このパターンのルールが1本ずつ増えていきます。 -
-A KUBE-SEP-XQYOYKNJY53JUFN6 -s 10.244.0.3/32 ... -j KUBE-MARK-MASQ
送信元がこの Endpoint 自身(10.244.0.3)だった場合、つまり Pod が自分の属する Service 経由で自分自身にアクセスする(hairpin)場合に、masquerade の印を付けます。 -
-A KUBE-SEP-XQYOYKNJY53JUFN6 -p tcp ... -j DNAT --to-destination 10.244.0.3:80
実際に宛先を書き換える行です。EndpointSlice.endpoints[0].addresses(10.244.0.3)とEndpointSlice.ports[0].port(80)がそのまま--to-destinationになり、パケットは Service の ClusterIP から Pod の実 IP:port へ DNAT されます。 -
-A KUBE-SEP-2EIDLAYDFSWGXYQW -s 10.244.0.4/32 ... -j KUBE-MARK-MASQ
2つ目の Endpoint(10.244.0.4)についての、同じ意味の masquerade 判定です。 -
-A KUBE-SEP-2EIDLAYDFSWGXYQW -p tcp ... -j DNAT --to-destination 10.244.0.4:80
2つ目の Endpoint(10.244.0.4)への DNAT です。
つまり KUBE-SEP-* チェーンは Endpoint 1つにつき2行(masquerade 判定 + DNAT)で構成されるため、EndpointSlice の要素数が増えると KUBE-SVC-* チェーンの行数と KUBE-SEP-* チェーンの数が両方とも線形に増えていきます。
たった1つの Service・2つの Endpoint だけで、チェーンが3階層(KUBE-SERVICES → KUBE-SVC-* → KUBE-SEP-*)、ルールが8本できています。この Service・Endpoint と同じ数だけこのセットが繰り返されるので、Service 数が増えるほどチェーン数・ルール数がそのまま線形に増えていきます(この検証環境では、CoreDNS 用の Service を含めても nat テーブル全体で 86 行でした)。
この仕組み自体は長年「十分実用的」とされてきましたが、KEP-3866 はこれを置き換える理由を次のように整理しています。
制御プレーン側の問題
The control plane is problematic because the iptables API does not support making incremental changes to the ruleset. If you want to add a single iptables rule, the iptables binary must acquire a lock, download the entire ruleset from the kernel, find the appropriate place in the ruleset to add the new rule, add it, re-upload the entire ruleset to the kernel, and release the lock. This becomes slower and slower as the ruleset increases in size (ie, as the number of Kubernetes Services grows).
iptables の API 自体に「ルールセットへの差分更新」という概念がなく、1本ルールを足すだけでも「ロック取得 → ルールセット全体をダウンロード → 適切な位置を探す → 追加 → ルールセット全体を再アップロード → ロック解放」という手順を踏みます。Service 数が増えるほどこの往復が重くなります。
データプレーン側の問題
The data plane is problematic because (for the most part), the number of iptables rules used to implement a set of Kubernetes Services is directly proportional to the number of Services. And every packet going through the system then needs to pass through all of these rules, slowing down the traffic.
— 同上
Service 数に比例してルール数が増え、パケットは(マッチするまで)それを順番に評価されるため、処理コストは実質 O(n) になります。
ipvs モードも解決にならない
「ipvs モードを使えばよいのでは」という案について、KEP はこれも否定しています。
the kernel IPVS APIs alone do not provide enough functionality to fully implement Kubernetes services, and so the
ipvsbackend also makes heavy use of the iptables API. Thus, if we are worried about iptables deprecation, then in order to switch to usingipvsas the default mode, we would have to port the iptables parts of it to use nftables anyway.— 同上
ipvs モードは負荷分散のコア部分こそ IPVS の機能を使いますが、Service 機能のかなりの部分は結局 iptables API に依存しています。iptables 自体の非推奨(後述)を心配するなら、ipvs モードもいずれ書き直しが必要になるため、根本的な解決にはなりません。
iptables バイナリの nftables 実装(iptables-nft、2018年の iptables 1.8.0 で追加)についても、KEP は「既存の iptables 構文との互換性を保つ制約があるため、nftables の新機能や性能改善の恩恵を受けられない」としています(詳しくは次章)。
ディストリビューション側の非推奨の動き
KEP-3866 は、RHEL 9 で iptables が非推奨になったこと、Debian 11 で iptables が「必須パッケージ」から外れたことも、nftables backend が必要な理由として挙げています。これは「upstream の開発自体が iptables から nftables へ移っている」という節の中で、その裏付けとして挙げられているものです。KEP がモチベーションの筆頭に置いているのは、あくまで前述の制御プレーン・データプレーンの性能問題で、ディストリビューション側の非推奨化はそれを補強する材料という位置づけです。
iptables コマンドの実体は、すでに nftables になっている
ここまで「iptables モード」と書いてきましたが、実は多くの環境で、iptables コマンドを実行したときにカーネル内部で実際に使われているのは、すでに nftables です。 これは kube-proxy の話ではなく、Linux ディストリビューション側の話です。
xtables-nft という互換レイヤー
2018年の iptables 1.8.0 で、iptables/ip6tables と同じコマンド構文を受け付けながら、内部では nftables の netlink API を叩く実装(iptables-nft、frontend 名は xtables-nft)が追加されました。従来の実装(iptables-legacy)と共存し、update-alternatives でどちらを使うか切り替えられます。
これを支えているカーネル側の仕組み(nf_tables 本体と、legacy な xt_* match/target をラップする互換レイヤー nft_compat)は、どちらも Linux 3.13(2014年1月リリース)で同時に追加されています。
$ git describe --contains <nf_tablesを追加したコミット>
v3.13-rc1~105^2~186^2~14
$ git describe --contains <nft_compatを追加したコミット>
v3.13-rc1~105^2~186^2~8
(参考: torvalds/linux を git log --diff-filter=A -- net/netfilter/nf_tables_core.c net/netfilter/nft_compat.c で確認)
つまり「カーネルが nftables を話せるようになったのは 3.13 から」ですが、「iptables コマンドがその上で動くようになったのは iptables 1.8.0(2018年)から」で、ここには4年ほどの差があります。ディストリビューションが実際に iptables の既定を nft-backed に切り替えたのは、後述のとおりさらにその後です。
主要ディストリビューションは、すでにこちらをデフォルトにしています。
nftables is the default framework in use in Debian (since Debian 10 Buster)
This means: ... if installed, the iptables utility will use by default the nf_tables backend by means of the iptables-nft layer (i.e, using iptables syntax with the nf_tables kernel subsystem).
RHEL は少し先を行っていて、9 の時点で iptables-nft パッケージ自体がすでに非推奨です。
The ipset and iptables-nft packages have been deprecated in RHEL 9 ... this driver is not recommended for new deployments. It continues to be supported in this RHEL release, but it is likely to be removed in the next major release.
つまり RHEL は「iptables コマンドの中身を nftables にする」段階をすでに終え、次は「iptables コマンドという互換レイヤー自体をいずれ無くす」方向に進んでいます。
Ubuntu も、現行 LTS である 22.04(Jammy)・24.04(Noble)では nftables が既定です。24.04 の iptables パッケージの changelog にこう明記されています。
Promote nftables from Suggests to Recommends for iptables since the nft backend has been the default for quite a while now
実際に確認してみる
kind (v0.33.0、node image kindest/node:v1.37.0、OS は Debian 13 trixie)のノードで確認すると、たしかにこうなっていました。
$ iptables --version
iptables v1.8.11 (nf_tables)
$ update-alternatives --display iptables
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
/usr/sbin/iptables-legacy - priority 10
/usr/sbin/iptables-nft - priority 20
kube-proxy が実際に呼び出すバイナリも確認すると、iptables-save/iptables-restore はどちらも xtables-nft-multi の実体を指していました(readlink -f $(which iptables-save) → /usr/sbin/xtables-nft-multi)。
この状態で kube-proxy を(前章の例と同じ)mode: iptables のまま動かし、nft list table ip nat でカーネルの中身を直接見ると、KUBE-SERVICES などの kube-proxy 由来のチェーンが、iptables の構文ではなく nftables のネイティブな構文で保存されていました。
chain KUBE-SERVICES {
meta l4proto tcp ip daddr 10.96.82.126 tcp dport 80 counter packets 0 bytes 0 jump KUBE-SVC-73JNOU5FOFXIWZLE
fib daddr type local counter packets 167 bytes 10020 jump KUBE-NODEPORTS
}
chain KUBE-SVC-73JNOU5FOFXIWZLE {
meta l4proto tcp ip saddr != 10.244.0.0/16 ip daddr 10.96.82.126 tcp dport 80 counter packets 0 bytes 0 jump KUBE-MARK-MASQ
meta random & 2147483647 < 1073741824 counter packets 0 bytes 0 jump KUBE-SEP-XQYOYKNJY53JUFN6
counter packets 0 bytes 0 jump KUBE-SEP-2EIDLAYDFSWGXYQW
}
chain KUBE-SEP-XQYOYKNJY53JUFN6 {
ip saddr 10.244.0.3 counter packets 0 bytes 0 jump KUBE-MARK-MASQ
meta l4proto tcp dnat to 10.244.0.3:80
}
前章の iptables-save の出力と見比べながら、チェーンごとに対応を見ていきます。
chain KUBE-SERVICES
-
meta l4proto tcp ip daddr 10.96.82.126 tcp dport 80 ... jump KUBE-SVC-73JNOU5FOFXIWZLE
iptables-saveの-d 10.96.82.126/32 -p tcp --dport 80 -j KUBE-SVC-73JNOU5FOFXIWZLEと同じ内容です。-d/-p/--dport/-jが、それぞれip daddr/meta l4proto/tcp dport/jumpという nftables のネイティブな式にそのまま置き換わっています。 -
fib daddr type local ... jump KUBE-NODEPORTS
iptables-save側の-m addrtype --dst-type LOCAL -j KUBE-NODEPORTSに対応します。-m addrtypeという xtables match が、fib daddr type localという nftables 組み込みの式に変換されています。
chain KUBE-SVC-73JNOU5FOFXIWZLE
-
meta l4proto tcp ip saddr != 10.244.0.0/16 ip daddr 10.96.82.126 tcp dport 80 ... jump KUBE-MARK-MASQ
iptables-saveの! -s 10.244.0.0/16 -d 10.96.82.126/32 -p tcp --dport 80 -j KUBE-MARK-MASQと同じ行です。! -sの否定条件がip saddr !=に対応しています。 -
meta random & 2147483647 < 1073741824 ... jump KUBE-SEP-XQYOYKNJY53JUFN6
iptables-saveの-m statistic --mode random --probability 0.5 -j KUBE-SEP-XQYOYKNJY53JUFN6に対応します。-m statisticという xtables match が使っていた確率判定が、meta random(0 から2147483647(RAND_MAX)までの乱数)を使った比較式& 2147483647 < 1073741824(ちょうど半分)という、nftables のネイティブな式に変換されています。 -
... jump KUBE-SEP-2EIDLAYDFSWGXYQW
iptables-save側で確率指定の無かった最後の行(2つ目の Endpoint への振り分け)と同じです。
chain KUBE-SEP-XQYOYKNJY53JUFN6
-
ip saddr 10.244.0.3 ... jump KUBE-MARK-MASQ
iptables-saveの-s 10.244.0.3/32 -j KUBE-MARK-MASQ(hairpin 用の masquerade 判定)と同じです。-sがそのままip saddrになっています。 -
meta l4proto tcp dnat to 10.244.0.3:80
iptables-saveの-p tcp -j DNAT --to-destination 10.244.0.3:80に対応します。-j DNAT --to-destinationという1つのターゲットが、dnat toという1つの式にまとまっています。
こうして並べてみると、-p tcp -m tcp --dport 80 は meta l4proto tcp ... tcp dport 80 に、-m statistic --mode random --probability 0.5 は meta random & 2147483647 < 1073741824 という乱数式に、-j DNAT --to-destination は dnat to にと、それぞれ nftables のネイティブな式へ直接変換されています。これらは古い xt_* match/target をそのままカーネルで動かすための互換レイヤー(nft_compat、2013年に追加)を経由しているわけではありません。nft_compat は、今でも native な nftables 表現が用意されていない一部の古い match/target のためのフォールバックとして存在しています。
(参考: 上記は自分で kind クラスタを立てて確認した出力です。nft_compat については net/netfilter/nft_compat.c のコメントを参照しました)
それでも性能問題は解決しない
ここが誤解しやすいところです。カーネルへの保存形式が nftables になっても、kube-proxy の iptables モードの性能特性は変わりません。 上の例を見ても分かるとおり、Service ごとに KUBE-SVC-*・Endpoint ごとに KUBE-SEP-* という個別のチェーンと jump が並ぶ構造は、iptables コマンドで作った当時のままです。nftables 本来の強みである verdict map(1回のマップ参照で O(1) にディスパッチする仕組み。次章で実例を見ます)は、iptables コマンドの構文には存在しないため使われません。
KEP-3866 が「既存の iptables 構文との互換性を保つ制約があるため、nftables の新機能や性能改善の恩恵を受けられない」と評しているのはこのことです。性能改善の恩恵を受けるには、kube-proxy 自体を --proxy-mode=nftables に切り替えて、ルール構造そのものを verdict map ベースに変える必要があります。 カーネルの保存形式が nftables になっているかどうかとは、別の話です。
kube-proxy 側で気にする必要があること
kube-proxy の iptables モードのコード自体は、実行するバイナリが iptables-legacy か iptables-nft かを判別していません。iptables-save/iptables-restore という同じコマンド名を呼ぶだけです。ただし、同じ操作に対するエラーメッセージの文字列が legacy と nft-backed とで異なるため、そこだけはバージョンごとに個別対応しています。
var iptablesNotFoundStrings = []string{
// iptables-legacy [-A|-I] BAD-CHAIN [...]
// iptables-legacy [-C|-D] GOOD-CHAIN [...non-matching rule...]
// iptables-legacy [-X|-F|-Z] BAD-CHAIN
// iptables-nft -X BAD-CHAIN
// NB: iptables-nft [-F|-Z] BAD-CHAIN exits with no error
"No chain/target/match by that name",
// iptables-legacy [...] -j BAD-CHAIN
// iptables-nft-1.8.0 [-A|-I] BAD-CHAIN [...]
// NB: also matches some other things like "-m BAD-MODULE"
"No such file or directory",
// iptables-legacy [-C|-D] BAD-CHAIN [...]
// iptables-nft [-C|-D] GOOD-CHAIN [...non-matching rule...]
"does a matching rule exist",
// iptables-nft-1.8.2 [-A|-C|-D|-I] BAD-CHAIN [...]
// iptables-nft-1.8.2 [...] -j BAD-CHAIN
"does not exist",
}
(参考: pkg/util/iptables/iptables.go#L509-L530 (kubernetes v1.37.1))
iptables-nft は同じバージョン内でもリリース(1.8.0 → 1.8.2)ごとにエラーメッセージが変わっており、kube-proxy はそのどれが来ても「チェーンが見つからない」と正しく判定できるよう、文字列を列挙して対応しています。「iptables コマンドの構文さえ合わせれば、その先はディストリビューション任せで気にしなくてよい」というわけではないということです。
nftables と kubernetes の nftables モードの概要
前章で見た通り、kube-proxy の nftables モードが実際に切り替えているのは「nftables 技術を使うかどうか」ではなく、verdict map を中心とした nftables 独自のルール構造を使うかどうかでした。この章では、その verdict map の実物を見ていきます。
nftables は netfilter サブシステムの新しい API で、iptables の後継として2014年の Linux 3.13 で最初に導入されました。iptables との大きな違いは、各コンポーネントが自分専用の table を持てることです。
Unlike iptables, nftables does not have any reserved/default tables or chains (eg,
nat,PREROUTING). Instead, each nftables user is expected to create and work with its own table(s), and to ignore the tables created by other components
kube-proxy は IPv4 用に ip family、IPv6 用に ip6 family でそれぞれ kube-proxy という名前の table を1つずつ作り、自分のルール・set・map はすべてその中に収めます。他のコンポーネント(firewalld 等)が自分の table を再作成しても、kube-proxy の table には影響しません。
verdict map による O(1) ディスパッチ
iptables モードでは Service ごとに個別のルールを順番に評価していましたが、nftables モードは verdict map(値が「次にどこへ飛ぶか」を表すマップ)を使って一括でディスパッチします。同様に kind で別クラスタを作り、今度は kube-proxy を mode: nftables に切り替えて、同じ default/demo Service(Endpoint 2つの ClusterIP Service)を作ってみます。
kind create cluster --config kind-config.yaml --image kindest/node:v1.37.0
# kube-proxy の ConfigMap を書き換えて nftables モードに切り替える
kubectl -n kube-system get cm kube-proxy -o jsonpath='{.data.config\.conf}' > config.conf
sed -i '' 's/^mode: iptables/mode: nftables/' config.conf
kubectl -n kube-system create cm kube-proxy \
--from-file=config.conf=config.conf --dry-run=client -o yaml \
| kubectl -n kube-system apply -f -
kubectl -n kube-system rollout restart ds kube-proxy
kubectl create deployment demo --image=registry.k8s.io/pause:3.10 --replicas=2
kubectl expose deployment demo --port=80 --target-port=80 --type=ClusterIP
nft list table ip kube-proxy を見ると、こう表現されていました。
map service-ips {
type ipv4_addr . inet_proto . inet_service : verdict
comment "ClusterIP, ExternalIP and LoadBalancer IP traffic"
elements = { 10.96.0.10 . tcp . 53 : goto service-NWBZK7IH-kube-system/kube-dns/tcp/dns-tcp,
10.96.180.187 . tcp . 80 : goto service-NETPOJGK-default/demo/tcp/,
10.96.0.1 . tcp . 443 : goto service-2QRHZV4L-default/kubernetes/tcp/https,
10.96.0.10 . tcp . 9153 : goto service-AS2KJYAD-kube-system/kube-dns/tcp/metrics }
}
chain services {
ip daddr @cluster-ips ip saddr != 10.244.0.0/16 meta mark set meta mark | 0x00004000 comment "masquerade clusterIP traffic from outside cluster"
ip daddr . meta l4proto . th dport vmap @service-ips
ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
}
chain service-NETPOJGK-default/demo/tcp/ {
meta l4proto tcp dnat ip to numgen random mod 2 map { 0 : 10.244.0.5 . 80, 1 : 10.244.0.6 . 80 }
}
こちらもブロックごとに見ていきます。
map service-ips
-
elements = { ... 10.96.180.187 . tcp . 80 : goto service-NETPOJGK-default/demo/tcp/, ... }
クラスタ内の Service を1つずつ列挙した verdict map です。default/demoの場合、キーはService.spec.clusterIP(10.96.180.187)・プロトコル(tcp)・.spec.ports[0].port(80)の組で、値は「どこへ飛ぶか」を表すgoto service-NETPOJGK-default/demo/tcp/という verdict です。Service が増えると、このelementsの中に1行追加されるだけです。iptables モードのように新しいKUBE-SERVICES/KUBE-SVC-*のチェーンが増えるわけではありません。
chain services
-
ip daddr @cluster-ips ip saddr != 10.244.0.0/16 ... comment "masquerade clusterIP traffic from outside cluster"
宛先が ClusterIP の集合(@cluster-ipsセット)で、送信元が Pod ネットワークの外から来た場合に、masquerade 用の mark を立てます。iptables モードのKUBE-SVC-*チェーンにあった! -s 10.244.0.0/16 ... -j KUBE-MARK-MASQに相当します。 -
ip daddr . meta l4proto . th dport vmap @service-ips
宛先 IP・プロトコル・宛先ポートの組をキーにしてservice-ipsmap を1回引き、該当する Service のチェーンへ飛びます。Service が何個あっても、このチェーン自体は1行のままです。 -
ip daddr @nodeport-ips meta l4proto . th dport vmap @service-nodeports
NodePort 宛のトラフィックについて、同様にservice-nodeportsmap を引いて振り分けます。
chain service-NETPOJGK-default/demo/tcp/
-
meta l4proto tcp dnat ip to numgen random mod 2 map { 0 : 10.244.0.5 . 80, 1 : 10.244.0.6 . 80 }
default/demoの Service 専用チェーンです。EndpointSlice.endpointsの2つのaddresses(10.244.0.5・10.244.0.6)が、この1行のmapの中に列挙されており、numgen random mod 2で 0 か 1 を引いて、対応する Endpoint へ直接dnatします。iptables モードでは Endpoint ごとに別のルール(-m statistic --mode random --probability ...)と別のKUBE-SEP-*チェーンが必要でしたが、ここでは Endpoint 選択と DNAT が1行にまとまっています。
iptables モードでは Service ごとに KUBE-SERVICES → KUBE-SVC-* → KUBE-SEP-* という3階層・8ルールが必要でしたが、nftables モードでは service-ips という1つの map に要素を1行足すだけで新しい Service が増えます。services チェーン自体は Service 数が増えても変わりません。しかも Endpoint の選択(numgen random mod 2 map {...})と DNAT が service-NETPOJGK-default/demo/tcp/ という1つのチェーンにまとまっていて、iptables モードにあった Endpoint ごとの KUBE-SEP-* チェーンに相当するものがありません。
(参考: KEP-3866 General Service dispatch の設計が実装でこう表れています)
daddr . protocol . dport というキーで1回引くだけで飛び先が決まるため、Service 数が増えてもディスパッチ自体のコストはほぼ変わりません。KEP はこれを「roughly O(1) rather than O(n) as in the iptables backend」と表現しています。
DNAT と SNAT(masquerade)の扱いも netfilter のフック単位に分かれている点は iptables と同じで、kube-proxy の実装コメントにも説明があります。
We do the DNAT for inbound traffic in
prerouting... We do the DNAT for outbound traffic inoutput... nftables has the same constraints on DNAT and masquerading as iptables does; you can only DNAT from the "prerouting" stage and you can only masquerade from the "postrouting" stage.
feature gate としての現在地
NFTablesProxyMode feature gate は、次のように 1.29 Alpha → 1.31 Beta → 1.33 GA(ロック済み)で進んでいます。
NFTablesProxyMode: {
{Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha},
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
{Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.GA, LockToDefault: true},
},
(参考: pkg/features/kube_features.go#L1882-L1886 (kubernetes v1.37.1))
「ロック済み(LockToDefault: true)」なので、v1.37.1 の時点で feature gate を無効化することはできません。ただし、これは「--proxy-mode nftables を選べるようになった」ことを意味するだけで、既定値が nftables になったわけではない点に注意が必要です。GA を発表した公式ブログの時点で、すでにこのことは明言されていました。
For compatibility reasons, even once nftables becomes GA, iptables will still be the default.
— NFTables mode for kube-proxy - Kubernetes Blog(Dan Winship, 2025-02-28)
ipvs モードは非推奨に
nftables モードは iptables の後継であると同時に、ipvs モードの後継でもあります。KubeProxyIPVS feature gate は 1.37 で Deprecated になりました。
KubeProxyIPVS: {
{Version: version.MustParse("1.0"), Default: true, PreRelease: featuregate.GA},
{Version: version.MustParse("1.37"), Default: true, PreRelease: featuregate.Deprecated},
},
(参考: pkg/features/kube_features.go#L1735-L1738 (kubernetes v1.37.1))
公式ドキュメントには、今後のスケジュールも明記されています。
The
ipvsproxy mode is deprecated. Support foripvsmode will be disabled by default from Kubernetes v1.40 (you can re-enable it with theKubeProxyIPVSfeature gate);ipvsmode will be fully removed in Kubernetes v1.43.
Kubernetes で nftables モードを有効化する方法
nftables モードは 1.33 で GA しているので、「実験的だからまだ使えない」という段階ではありません。ただし既知の課題がいくつか残っていて、その多くは特定の利用状況に限定されています。
-
localhost 経由の NodePort アクセス: 対応するには
KubeProxyNFTablesLocalhostNodePortsfeature gate(1.38 で Beta)を有効にする必要があります。エアギャップ環境でのイメージレジストリ利用など、限定的な用途で必要になる機能です(詳しくは後述の「今後」の章で扱います) -
大規模クラスタでの soft lockup: 後述する
kubernetes/kubernetes#135639は、Endpoint 数が数十万規模の巨大なクラスタでのみ報告されています - iptables モードにあった一部の挙動が無い: NodePort が既定で全 interface に開かない、firewall 向けの自動 accept ルールが無い、conntrack バグの回避策が無い、という3点です(次の節で詳しく扱います)。iptables モード時代の挙動に依存していた場合だけ影響します
一般的な用途であればすでに実用レベルですが、大規模クラスタや上記のような特殊な用途では、事前に確認しておいたほうが良いと思います。
前提条件
nftables モードには、iptables モードよりも新しいカーネルが必要です。具体的には Linux カーネル 5.13 以降が条件です。
// NFTablesKubeProxyKernelVersion is the lowest kernel version kube-proxy supports using
// nftables mode with by default.
const NFTablesKubeProxyKernelVersion = "5.13"
(参考: pkg/util/kernel/constants.go#L49-L52 (kubernetes v1.37.1))
公式ドキュメントでも同じ要件が説明されています。
the
nftablesmode of kube-proxy requires version 1.0.1 or later of the nft command-line tool, as well as kernel 5.13 or later.— kube proxy
nftablesproxy mode - Linux Kernel Version Requirements
kube-proxy 自身も、起動時にホストのカーネルバージョンを見てこれを検査します。
// Likewise, we want to ensure that the host filesystem has nft >= 1.0.1, so that
// it's not possible that *our* rules break *the system's* nft. ... we check the kernel
// version, under the assumption that the distro will have an nft binary that supports
// the same features as its kernel does, and so kernel 5.13 or later implies nft 1.0.1
// or later. https://issues.k8s.io/122743
//
// However, we allow the user to bypass this check by setting
// `KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK` to anything non-empty.
if os.Getenv("KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK") == "" {
kernelVersion, err := utilkernel.GetVersion()
...
if kernelVersion.LessThan(version.MustParseGeneric(utilkernel.NFTablesKubeProxyKernelVersion)) {
return nil, fmt.Errorf("kube-proxy in nftables mode requires kernel %s or later", ...)
}
}
(参考: pkg/proxy/nftables/supported.go#L60-L79 (kubernetes v1.37.1))
環境変数 KUBE_PROXY_NFTABLES_SKIP_KERNEL_VERSION_CHECK でこの検査を回避できますが、これは検証・開発目的のものです。本番環境で使うと、ホストの nft バイナリと kube-proxy が想定するバージョンの不整合による不具合を踏みかねないので避けたほうが良いと思います。
有効化の方法
--proxy-mode フラグ、または設定ファイルの mode フィールドに nftables を指定します。nftables 固有の設定は nftables: 以下にまとめられており、代表的なものは masquerade 用の fwmark ビットを指定する masqueradeBit です。
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "nftables"
nftables:
masqueradeBit: 14
nodePortAddresses:
- "primary"
syncPeriod: 30s
minSyncPeriod: 1s
nodePortAddresses・syncPeriod・minSyncPeriod は iptables モードとも共通のトップレベルのフィールドです。nodePortAddresses を明示していない場合の既定値は、モードによって異なります。
If unset, this defaults to 'all' in iptables and ipvs mode, and to 'primary' in nftables mode.
—
pkg/proxy/apis/config/types.go#L225-L233(kubernetes v1.37.1)
実行中のクラスタに適用する場合は、kube-proxy ConfigMap を書き換えて DaemonSet を再起動します(具体的な手順は後の章で実際に使ったコマンドを載せています)。
何も指定しなかった場合、v1.37.1 では次のように「iptables に既定されているが、1.40 でデフォルトが変わる」という警告がログとイベントの両方に出ます。
message := "No kube-proxy mode specified: defaulting to 'iptables'. Note that in Kubernetes 1.40 the default mode will switch to 'nftables'. If you wish to continue using 'iptables' mode, you must explicitly specify 'mode: iptables' in the kube-proxy config file, or '--proxy-mode iptables' on the kube-proxy command line."
logger.Error(nil, message)
s.Recorder.Eventf(s.NodeRef, nil, v1.EventTypeWarning, "KubeProxyDefaultMode", "StartKubeProxy", message)
(参考: cmd/kube-proxy/app/server_linux.go#L138-L144 (kubernetes v1.37.1))
iptables/ipvs モードから nftables モードへの切り替えは、kube-proxy を再起動するだけで安全に行えます。他のモードのルールを自動でクリーンアップしてくれるためです(ロールバックも同様)。
kube-proxy が変えるのは、そのノード上で Service 宛のパケットを実 Pod IP へ書き換える実装だけで、Pod 間の実際のルーティング(CNI プラグインが担当するオーバーレイ等)には触れません。そのため、ノードごとにモードが異なっていても安全だと KEP は明記しています。
it is expected to be safe to roll out the feature in a live cluster, even though this will result in different proxy modes running on different nodes, because Kubernetes service proxying is defined in such a way that no node needs to be aware of the implementation details of the service proxy implementation on any other node.
CNI そのものを Cilium に置き換える(AKS の節で触れた Azure CNI Powered by Cilium のような)移行とは、この点が対照的です。CNI の置き換えは Pod 間ルーティングそのものを含む変更なので、混在期間の挙動が構造的にはっきりしません。一方、kube-proxy のモード切り替えは Service の実装方式だけを変える、影響範囲の狭い変更です。
iptables モードとの挙動の違い
公式ドキュメントは、移行時に気をつけるべき挙動の違いを3点挙げています。
- NodePort interfaces: In
iptablesmode, by default, NodePort services are reachable on all local IP addresses. ... thenftablesmode defaults to--nodeport-addresses primary- NodePort interaction with firewalls: The
iptablesmode of kube-proxy tries to be compatible with overly-aggressive firewalls; for eachtype: NodePortservice, it will add rules to accept inbound traffic on that port... This approach will not work with firewalls based on nftables, so kube-proxy'snftablesmode does not do anything here- Conntrack bug workarounds: Linux kernels prior to 6.1 have a bug that can result in long-lived TCP connections to service IPs being closed with the error "Connection reset by peer". The
iptablesmode of kube-proxy installs a workaround for this bug... Thenftablesmode does not install any workaround by default— Migrating from
iptablesmode tonftables- Virtual IPs and Service Proxies
localhost(127.0.0.1/::1)経由での NodePort アクセスも、nftables モードでは長らくサポートされていませんでした。これは KEP-6032 で userspace の TCP プロキシとして追加が進んでいます(詳細は次章)。
nftable モードの今後
ここからは、まだ確定していない計画の話です。前提として、KEP-5343 は本記事執筆時点で status: provisional であることを踏まえて読んでください。
デフォルト変更のスケジュール(KEP-5343、provisional)
KEP-5343「Make nftables the default kube-proxy backend」が挙げているマイルストーンは次のとおりです。Beta(1.39)の時点ではまだ既定値は変わらず、実際に既定値が nftables になるのは GA(1.40)からです。
通常の feature gate の感覚だと「Beta = デフォルト有効」と思ってしまいますが、この KEP はそもそも feature gate を使っていません(前述の PRR セクションでも「機能の追加・削除が無いので feature gate の背後に置く理由が無い」としています)。KEP 自身も、この Alpha/Beta/GA という区分けが便宜的なものだと認めています。
This isn't really Alpha/Beta/GA, but let's pretend it is anyway just to structure the work.
| フェーズ | 対象バージョン | 内容 |
|---|---|---|
| Alpha | 1.37 |
iptables に既定された場合の警告ログ/イベントを追加(前章の内容) |
| Beta | 1.39 | kubeadm の system-validator がサポートするカーネルバージョンの下限を更新 |
| GA | 1.40 |
既定値を nftables に変更。kubeadm の既定値も変更 |
デフォルトをまだ変えていない理由として、KEP は主に2つを挙げています。1つはカーネルバージョンで、nftables モードが要求する 5.13 以降が普及するまで待つ必要があるという判断です。
The
nftablesbackend requires a much newer kernel (5.13) than theiptablesandipvsbackends. ... Thus, it seems reasonable to assume that as of Kubernetes 1.39, most users will have a new-enough kernel to supportnftables.
もう1つは、大規模クラスタでの起動の遅さです。KEP 自身が「直っているかもしれないが確証はない」というトーンで書いています。
One problem we know existed in the past that may still be there is that it definitely used to take a long time to start up a new node in a cluster with many services/endpoints. It is possible that kubernetes #135800 and kubernetes #136499 may have fixed this.
— 同上
古いカーネルのまま 1.40 に上げてしまった場合の挙動についても、KEP は率直です。
If someone running on a kernel that does not support kube-proxy in
nftablesmode upgrades to 1.40 without having explicitly setmode: iptablesin their config, then kube-proxy will try to start innftablesmode and fail, leaving the node broken.— 同上
1.40 へのアップグレードそのものに、明示的な mode 指定が必須になるわけではありません。 カーネルが 5.13 以降であれば、mode を指定していなくても、1.40 では既定値の nftables でそのまま起動します。問題が起きるのは、カーネルが古く nftables モードの要件を満たさないクラスタが、mode を指定しないまま 1.40 に上げてしまった場合です。この場合、既定値が nftables に変わったことで起動に失敗し、ノードが動かなくなります。1.37 の警告ログはこの事態を避けるための猶予期間で、古いカーネルのままなら mode: iptables を、nftables モードを使いたいなら mode: nftables を、あらかじめ明示しておくことが推奨されています。
localhost NodePort の穴埋め(KEP-6032)
nftables モードが localhost NodePort を扱えない、という差分は、KEP-3866 の設計時点で意図的に見送られた機能です。iptables モードの route_localnet sysctl はセキュリティホール(CVE-2020-8558)につながる仕組みで、nftables モードではそもそも採用しない方針でした。
KEP-6032 はこの穴を、DNAT ではなく userspace の TCP プロキシを追加することで埋めます。KubeProxyNFTablesLocalhostNodePorts feature gate を有効にし、--nodeport-addresses に localhost というキーワードを指定したときだけ有効になる、opt-in の機能です。
{"enabled": true, "mode": "NFTABLES"}
nodePortAddresses: ["primary", "localhost"]
| フェーズ | 対象バージョン |
|---|---|
| Alpha | 1.37 |
| Beta | 1.38(2026-09-16 に beta 昇格の提案がマージ済み) |
| Stable | 1.40 |
(参考: KEP-6032: nftables Localhost NodePort Userspace Proxy)
対応するのは TCP のみで、UDP/SCTP の localhost NodePort は対象外です。エアギャップ環境で 127.0.0.1 のイメージレジストリを NodePort 経由で使う、という用途(Zarf というツールの例が KEP に挙げられています)が主な動機です。
nft バイナリ依存からの脱却(NFTablesNetlink)
KEP-5343 は、デフォルト化を阻む残課題として「nft コマンド自体が、kube-proxy が想定するのと大きく異なるバージョンの nft で作られたルールに遭遇するとクラッシュする」問題を挙げています。
The biggest remaining stability problem is the fact that
nftitself crashes in some circumstances, notably when it encounters rules created by someone else using a much newer version ofnftthan the kube-proxy image contains. We are working on fixing this now by building our own patched version of nft that should be more stable (kubernetes #136786).
これと並行して、NFTablesNetlink という feature gate が 1.37 で Beta になっています。
// owner: @aojea
//
// Allows kube-proxy to use netlink directly for nftables operations,
// rather than executing the nft command-line binary.
NFTablesNetlink featuregate.Feature = "NFTablesNetlink"
(参考: pkg/features/kube_features.go#L774-L778 (kubernetes v1.37.1))
これは、外部コマンドとしての nft バイナリを都度実行するのではなく、netlink を直接叩いてカーネルと通信する方式です。ホストにインストールされた nft バイナリのバージョンに引きずられる問題そのものを避けられるため、上記の安定性課題への対応として位置づけられます。
残る性能課題
nftables モード自体にも、性能面の課題が報告された実績があります。GitHub の kubernetes/kubernetes#135639(2025-12-06 起票、本記事執筆時点で Closed)は、Service 数が1,000を超えるような大規模クラスタで、ルール同期に伴う nft プロセスの CPU 使用率が上昇し、同期時間が20分以上に伸びる"soft lockup"の事例を報告していました。Amazon EKS の公式ドキュメントもこの issue を参照しており、対策としてカーネル 6.18 以降(該当の改善が一部の安定版カーネルにバックポート予定)と kube-proxy v1.36 以降の組み合わせを推奨しています。この課題は次章のクラウド事業者の対応でも触れます。
この soft lockup の直接の原因は、カーネル側の nft_chain_validate()(chain の jump/goto 先を辿ってループが無いか検証する処理)が、同じチェーンを何度も重複して検証してしまうことです。torvalds/linux を実際に確認したところ、これを直接解消する修正コミット(netfilter: nf_tables: avoid chain re-validation if possible)は v6.19-rc2 から含まれており、v6.18 タグには含まれていませんでした(git describe --contains で確認)。EKS のドキュメントが挙げている「6.18」という数字とは食い違いがあるため、実際に対策済みのカーネルを使っているかどうかは、自分の環境のカーネルで該当コミットの有無を確認することをおすすめします。
各クラウドサービスで iptables モードで性能が問題になった際の対応方法
EKS
Amazon EKS のベストプラクティスガイドは、iptables モードの性能問題を明確に大規模クラスタ向けの注意事項として扱っています。
With
kube-proxyin nftables mode, Amazon EKS can resolve the network latency issue common in large clusters with over 1,000 services running kube-proxy in iptables mode. Iptables mode processes packet filtering rules sequentially for the first packet of each connection, which causes this performance issue.— Running kube-proxy in nftables Mode - Amazon EKS Best Practices Guides
デフォルトは依然として iptables です。
Even though the nftables backend is GA,
iptablesremains the defaultkube-proxymode for compatibility reasons. You must explicitly opt in to nftables mode.— 同上
有効化は EKS Add-on 経由か、kube-proxy-config ConfigMap の直接編集で行います(Kubernetes 1.33 以降のクラスタが必要)。
aws eks update-addon --cluster-name $CLUSTER_NAME --addon-name kube-proxy \
--configuration-values '{"mode": "nftables"}' \
--resolve-conflicts OVERWRITE
前章で触れた大規模クラスタでの性能課題についても、このガイドは明示的に version を挙げて注意喚起しています。
Before
kube-proxyv1.36.0, clusters with hundreds of thousands of endpoints can experience very slow nftables rule sync times and CPU soft lockups. ... For the best performance in large clusters, we recommend running a recent Linux kernel together withkube-proxyv1.36.0 or later.— 同上
Amazon VPC CNI については、nftables モードとの互換性が v1.23.0 以降で確保されているとされています。
GKE
GKE には、kube-proxy の nftables モードそのものへの言及は見当たりませんでした。GKE が示している解は、kube-proxy を丸ごと使わない GKE Dataplane V2(Cilium ベース)です。
GKE Dataplane V2 uses
ciliuminstead ofkube-proxyto implement Kubernetes Services.
Autopilot クラスタでは常に有効です。一方、Standard クラスタでは新規クラスタ作成時に選択するオプションという扱いで、次のような制約があります(2026-09-24 更新のページで確認)。
You can enable GKE Dataplane V2 only when you create a new GKE cluster. You can't modify this setting for an existing cluster.
これは、メモにあった「既存の環境を nftables モードに移行できるようになるのか」という疑問への1つの答えになると思います。GKE の Standard クラスタで iptables モードの性能問題を回避する公式な手段は、nftables モードへの切り替えではなく、新規クラスタを Dataplane V2 で作り直すことで、しかもそれは既存クラスタへの後付けができません。GKE 公式ドキュメントの中に、既存クラスタを Dataplane V2 へ移行する手順は見当たりませんでした。
「Kubernetes 本体の既定値が 1.40 で nftables に変わるなら、Dataplane V2 を使っていない GKE クラスタもそれに追従して自動的に nftables になるのでは」という疑問が浮かびますが、これは本記事執筆時点(GKE が対応している最新マイナーは 1.37)では確認できません。参考になるのは EKS の前例で、EKS は nftables が GA(1.33)した後も「iptables remains the default kube-proxy mode for compatibility reasons」と明記し、Kubernetes 本体の GA を理由に既定値を追従させてはいませんでした(EKS の章を参照)。マネージドサービスが上流の既定値変更をそのまま継承するとは限らない、という一例です。
AKS
AKS は、kube-proxy の nftables モードをプレビュー機能として公式にサポートしています。
nftablesis the successor to theiptablesAPI and is designed to provide better performance and scalability thaniptables. Thenftablesproxy mode is essentially a replacement for both theiptablesandIPVSmodes, with better performance than either of them, and is recommended as a replacement forIPVS.— Configure kube-proxy (iptables/IPVS/nftables) (Preview) - Azure Kubernetes Service
mode の既定値は IPTABLES で、nftables を使うには aks-preview CLI 拡張と KubeProxyConfigurationPreview フィーチャーフラグの登録、かつ API バージョン 2025-09-02-preview 以降が必要です。
{"enabled": true, "mode": "NFTABLES"}
AKS にも、kube-proxy を使わない選択肢として Azure CNI Powered by Cilium があります。GKE の Dataplane V2 と似た位置づけですが、既存クラスタへの適用に関する扱いが対照的です。
Does Azure CNI Powered by Cilium use kube-proxy? No, AKS clusters created with network data plane as Cilium don't use
kube-proxy. ... If you upgrade AKS clusters on Azure CNI Overlay or Azure CNI with dynamic IP allocation to AKS clusters running Azure CNI Powered by Cilium, workloads on new nodes are created withoutkube-proxy. Workloads on existing nodes are also migrated to run withoutkube-proxyas a part of this upgrade process.— Configure Azure CNI Powered by Cilium in Azure Kubernetes Service (AKS)
つまり AKS は、GKE と違って 既存クラスタを Cilium データプレーンへアップグレードする手段を公式に提供しています。AKS Automatic では、この Cilium データプレーンが最初から既定です。
この FAQ は「既存ノードのワークロードも、アップグレードの一環として kube-proxy 無しで動くよう移行される」とは書いていますが、移行の途中でノードプール内に Cilium 適用済みノードと未適用ノードが混在する期間が生じるか、その間の Pod 間通信がどう扱われるかは明記していません。 ノードを作り直すのか、既存ノードに Cilium エージェントを追加して kube-proxy を止めるだけなのかも含めて、この記事の一次情報の範囲では確認できませんでした。実運用で気になる場合は、事前に検証環境で移行手順そのものを試すことをおすすめします。
3社の比較
| デフォルトのプロキシモード | kube-proxy の nftables モード | kube-proxy を置き換える eBPF 系オプション | 既存クラスタへの適用 | |
|---|---|---|---|---|
| EKS | iptables | 公式サポート(GA、Add-on 経由) | 非公式(Cilium/Calico 等を CNI として個別導入) | nftables への切り替えは可能 |
| GKE | iptables(Dataplane V2 非利用時) | 言及なし | GKE Dataplane V2(Cilium) | 不可(新規クラスタのみ) |
| AKS | iptables(Preview 機能) | 公式サポート(Preview) | Azure CNI Powered by Cilium | 可能(既存クラスタのアップグレードをサポート) |
おわりに
iptabels の更新が大きく問題になっていない Kubernetes クラスタについては、デフォルトで nftables モードが有効化される 1.40 から普及していくと思います。
AKS が既存クラスタの Cilium への切り替えに対応しているのは意外でした。既存のクラスタで CNI を切り替えると、切り替え中のクラスタ内通信に問題が起こりそうですが、許容する方針なのか?対策がされているのか?
GKE みたいに新規クラスタを作成して、クラスタの Blue-Green デプロイメントを実施する必要があると言われる方が分かりやすくて理解できるので、AKS の方針はちょっと不思議でした。
逆に GKE のケースでは既存クラスタに対して、nftables モードへの変更が可能なのかが気になりました。
いずれにしても、Linux 側でもう iptables がメンテされていかない方向なので、nftables モードを使っていく流れにはなると思います。
参考
参考にした情報を記事に登場した順に紹介します。
- Virtual IPs and Service Proxies - Kubernetes
- KEP-3866: Add an nftables-based kube-proxy backend
- nftables - Debian Wiki
- How to migrate from iptables to nftables - Red Hat
- iptables package changelog - Ubuntu
- NFTables mode for kube-proxy - Kubernetes Blog(Dan Winship, 2025-02-28)
- NFTablesProxyMode - Kubernetes Feature Gates
- Linux Kernel Version Requirements - Kubernetes
- KEP-5343: Make nftables the default kube-proxy backend
- KubeProxyNFTablesLocalhostNodePorts - Kubernetes Feature Gates
- KEP-6032: nftables Localhost NodePort Userspace Proxy
- kube-proxy nftables performance issue - kubernetes/kubernetes#135639
- Running kube-proxy in nftables Mode - Amazon EKS Best Practices Guides
- About GKE Dataplane V2 - Google Kubernetes Engine
- Configure GKE Dataplane V2 - Google Kubernetes Engine
- Configure kube-proxy (iptables/IPVS/nftables) (Preview) - Azure Kubernetes Service
- Configure Azure CNI Powered by Cilium in Azure Kubernetes Service (AKS)