ClusterIP に ping が通らないのに、なぜ curl は届くのか?
あなたも昨日、Service の manifest を書いて Pod 間の通信を成立させたはずだ。動作確認のつもりで Pod の中から ping <ClusterIP> を打ったら応答が返らず、Service が壊れているのかと肝を冷やした経験はないだろうか。実際には壊れていない。同じ IP に curl は問題なく届くのに、ping だけが失敗するのには理由がある。
- Service の
ClusterIPは実在するネットワークインターフェースを持たない仮想 IP で、実体は各ノードで動くkube-proxyが書き込む iptables の DNAT ルールにすぎない kube-proxyは Service と EndpointSlice の変更を監視してルールを書き換えるだけで、パケットの転送自体はカーネルの netfilter が行う。専用のロードバランサープロセスを経由するわけではない- DNAT ルールは Service が使う TCP/UDP のプロトコル・ポートにしか作られないため、ICMP を使う
pingは最初からこの経路に乗らない。curlが届くのはプロトコルの違いがそのまま挙動の違いになった結果だ
ClusterIP は「存在しない」IP
Service を作成すると、Kubernetes は ClusterIP というアドレスを自動で割り当てる。だがこの IP は Pod の IP と違って、実在するネットワークインターフェースに紐づいているわけではない。ノードの中で ip addr を実行しても、この IP を持つ NIC はどこにも出てこない。kubeadm ベースのクラスタでは既定で 10.96.0.0/12 という Service 用の CIDR から払い出される、宛先としてのみ意味を持つ番号だ。
この IP を実際に機能させているのが kube-proxy だ。すべてのノードで DaemonSet として動き、API サーバーが持つ Service と EndpointSlice(Service の selector にマッチする Pod の IP 一覧を保持するオブジェクト)の変更を常時監視している。変更を検知するたびに、自分が動いているノードのカーネルにパケット転送用のルールを書き込む。ノードごとに独立して同じ処理をするので、クラスタ全体を統括する単一のロードバランサー機構は存在しない。
実体は iptables の DNAT ルール
Linux 版 kube-proxy の既定モードは iptables だ。Service ごとに KUBE-SERVICES チェーンへ「この ClusterIP:port 宛なら」というルールを足し、そこから Service 専用のチェーンを経由して、EndpointSlice に載っている Pod の IP へ DNAT(Destination NAT。パケットの宛先アドレスを別の実アドレスへ書き換える処理)する。宛先の Pod が複数あれば、確率ベースでランダムにどれか 1 つへ振り分ける。
Pod A → ClusterIP:80(仮想 IP)
│ iptables の DNAT
▼
KUBE-SVC-xxxx(確率で分散)
├─▶ Pod B:8080 へ書き換え
└─▶ Pod C:8080 へ書き換え
ここで重要なのは、パケットが別のマシンや別プロセスを経由するわけではないという点だ。DNAT は送信元 Pod が乗っているそのノードのカーネル内で完結する。kube-proxy はルールを事前に書き込んでおくだけで、実際にパケットが流れる瞬間には一切関与しない。
ClusterIP は私書箱の番号のようなものだ。私書箱番号そのものは誰の自宅でもなく、宛先として登録されているだけの番号にすぎない。実際の配達は、各地域の郵便局員(各ノードの kube-proxy)があらかじめ持っている「この私書箱番号宛の荷物は、今日は誰の家に転送する」という差し替え表(iptables ルール)に従って行う。荷物が届くたびに本局へ問い合わせているわけではなく、局員が手元の表を見て即座に判断している。
EndpointSlice の更新と readiness probe
DNAT の転送先リストは、EndpointSlice に載っている Pod の IP から作られる。ここに載るのは selector にマッチした Pod のうち、readiness probe(Pod がリクエストを受け付けられる状態かをチェックする仕組み)が成功していて Ready と判定された Pod だけだ。起動直後でまだ準備が終わっていない Pod や、readiness probe が失敗し始めた Pod は EndpointSlice から外れ、それを見た kube-proxy がノードの iptables ルールからもその Pod 宛の転送先を削除する。
裏を返すと、iptables モードには「転送先が応答しなかったら別の Pod に自動で振り直す」というフォールバックが無い。確率で選ばれた転送先が実際に応答できるかどうかまでは、DNAT ルール自体は関知しない。異常な Pod をリストから追い出す責任は readiness probe 側にあり、これが正しく設定されていないと、Ready のまま固まった Pod へ延々とトラフィックが送られ続けることになる。
なぜ ping は失敗して curl は届くのか
ここまでの仕組みを踏まえると、冒頭の疑問には機構レベルの答えが出せる。kube-proxy が iptables に書き込む DNAT ルールは、Service が定義する プロトコルとポート(多くは TCP か UDP)にしかマッチしない。ping が使う ICMP はこの単位に属さないプロトコルなので、そもそも ClusterIP 宛の ICMP パケットを Pod へ転送するルールが存在しない。パケットは実体のない仮想 IP に届いたまま行き場を失い、応答が返らない。
一方 curl は TCP でリクエストを送るため、Service の port にマッチする DNAT ルールにそのまま乗り、いずれかの Pod へ書き換えられて届く。同じ IP に向けた通信でも、プロトコルが違うだけで経路の有無そのものが変わるということだ。「応答が無い=落ちている」と直感的に判断したくなるが、この場合は Service にも Pod にも何の異常もない。
Pod 内から他サービスの疎通確認をするとき、反射的に ping を打って応答が無く焦る場面が典型例だ。ClusterIP の疎通確認には curl や nc、DNS 解決の確認には nslookup を使うのが正しい手段になる。またサービスメッシュや Ingress コントローラーの挙動を追うときも、まず素の Service がこの iptables ベースの転送で動いていることを踏まえておくと、どのレイヤーで問題が起きているかを切り分けやすい。
ping が失敗し curl が届くという非対称な挙動を、実際の kind クラスタで確かめる。
kind create cluster --name svc-demo 2>/dev/null || true
kubectl create deployment web --image=nginx:alpine
kubectl expose deployment web --port=80
kubectl run tester --image=busybox --restart=Never \
--command -- sleep 3600
kubectl wait --for=condition=Ready pod/tester --timeout=60s
CIP=$(kubectl get svc web -o jsonpath='{.spec.clusterIP}')
echo "ClusterIP=$CIP"
kubectl exec tester -- sh -c "ping -c2 -W1 $CIP; wget -qO- $CIP | head -1"
ping は 2 回とも応答なしでタイムアウトするが、直後の wget は nginx のトップページ HTML を取得できるはずだ。同じ IP に対してプロトコルを変えただけで結果が反転する様子を、その場で確認できる。
- ClusterIP はどこかのノードやプロセスが実際に持っている IP アドレスだ — 実体はどのインターフェースにも紐づかない仮想 IP で、各ノードの iptables ルールが「その IP 宛のパケットをどう書き換えるか」を決めているだけだ。
- Service は常駐するロードバランサープロセスとしてトラフィックを中継している —
kube-proxymode=iptables ではルールを書き込むだけで、実際のパケット転送はカーネルの netfilter が行う。パケットが専用プロセスを経由することはない。 - Pod が異常になれば、Service は即座にそのPodへの送信を止める — 実際には readiness probe が失敗と判定して EndpointSlice から外れるまではルールが残り続ける。probe の間隔やしきい値次第で、異常な Pod へのトラフィックがしばらく続くことがある。
- ClusterIP
- Service に割り当てられる、クラスタ内部からのみ到達できる仮想 IP。実在するネットワークインターフェースは持たない。
- kube-proxy
- 全ノードで動く DaemonSet。Service と EndpointSlice の変更を監視し、ノードのパケット転送ルールを書き換える。
- EndpointSlice
- Service の selector にマッチし、かつ Ready と判定された Pod の IP 一覧を保持するオブジェクト。
- DNAT
- Destination NAT。パケットの宛先アドレスを別の実アドレスへ書き換える処理。ClusterIP から Pod IP への転送に使われる。
- readiness probe
- Pod がリクエストを受け付けられる状態かを定期的にチェックする仕組み。失敗すると EndpointSlice から除外される。
- Virtual IPs and Service Proxies — Kubernetes docs — kube-proxy の各モード(iptables / IPVS / nftables)と DNAT の仕組みを解説する公式リファレンス
- Service — Kubernetes docs — Service の役割と EndpointSlice との関係を定義した公式ドキュメント
- Learn why you can't ping a Kubernetes service — Daniele Polencic — ping が失敗し curl が届く理由を iptables ルールのプロトコル単位まで踏み込んで解説
- Comparing kube-proxy modes: iptables or IPVS? — Tigera — iptables のルール数増加によるスケール限界と、代替モードでの解決アプローチ