なぜあなたのパケットは迷子になるのか?kube-proxyが裏側で仕掛ける「NATの魔術」を解き明かす
クラウドネイティブな環境でKubernetesを運用していると、避けては通れないのが「ネットワークのブラックボックス化」です。特に、Service という抽象概念を叩いたとき、パケットが一体どこへ消え、どうやってPodに辿り着くのか。その裏側で起きている iptables や IPVS によるルーティングの魔法を知らなければ、本番環境で発生する不可解な通信断を解決することはできません。
今日は、SREの現場で幾多のパケットキャプチャを重ねてきた経験から、kube-proxy が担う「DNAT(Destination NAT)」の真実を、泥臭い実務の視点で紐解いていきましょう。
—
1. Serviceの正体:それは「実体のない仮想IP」である
まず大前提として、Kubernetesの ClusterIP は、どこかのNICにアサインされたIPアドレスではありません。あれは単なる「概念」です。
kube-proxy は、API Serverから Service や Endpoints の変更を監視し、各ノードのカーネル空間にある iptables(あるいは IPVS)のテーブルを書き換えます。我々が curl で ClusterIP にパケットを投げた瞬間、カーネルがパケットの宛先を強引に書き換えているのです。
なぜDNATが必要なのか?
Service のIP(Virtual IP)を、生のPodのIPアドレスへ突き通すためには、パケットの「宛先情報」を書き換える必要があります。これが DNAT です。
- 宛先:
10.96.0.10(Service IP) - → DNAT変換後:
10.244.1.5(実際のPod IP)
この変換処理こそが、kube-proxy がノードごとに行っている「ロードバランシングの正体」です。
—
2. iptablesモード:ルールという名の巨大な迷路
多くの環境でデフォルト設定されている iptables モード。これを理解するには、PREROUTING チェーンから辿るのが鉄則です。
以下のコマンドを、Kubernetesノード上で叩いてみてください。
# 現在のnatテーブルのルールをダンプする
# 読みづらいですが、ここが全ての起点です
sudo iptables -t nat -S
パケットが辿るシーケンス
1. PREROUTING: パケットがノードに入ると、まずここで KUBE-SERVICES チェーンに飛ばされます。
2. KUBE-SERVICES: 宛先IPが ClusterIP と一致するかを判定します。
3. KUBE-SVC-xxx: 該当するService専用のチェーンへジャンプします。
4. KUBE-SEP-xxx: ここで確率的にPodのIP(Endpoint)へ転送(DNAT)されます。
このとき、iptables はランダムな確率で SEP (Service Endpoint) を選択し、宛先IPを書き換えます。これが「Service経由のロードバランシング」の仕組みです。
—
3. IPVSモード:高負荷環境の切り札
もしあなたが、数千規模のServiceを抱える巨大クラスタを運用しているなら、iptables は遅すぎます。iptables はルールが増えるほど線形探索に近い挙動をするため、レスポンスが悪化します。
ここで登場するのが IPVS です。Linuxカーネルのロードバランサ技術である IPVS は、iptables とは異なり「ハッシュテーブル」で管理されるため、どれだけServiceが増えてもパフォーマンスが落ちません。
IPVSのデバッグコマンド
iptables の代わりに ipvsadm を使うと、現在のルーティング状況が一目瞭然です。
# 現在の仮想サーバーとバックエンドPodの対応を確認
sudo ipvsadm -Ln
# 出力例:
# TCP 10.96.0.10:80 rr
# -> 10.244.1.5:8080 Masq 1 0 0
# -> 10.244.2.3:8080 Masq 1 0 0
ここで Masq と表示されているのが重要です。これは「マスカレード(NAT)」を行うことを意味します。パケットがノードを離れる際、送信元IPをノード自身のIPに置き換えることで、Podが応答を返せるようにしているのです。
—
4. 実務的なデバッグTips:パケットはどこで消えた?
開発中に「Serviceに繋がらない」という事象に遭遇した際、私はまず以下の順序で確認します。
ステップ1: ServiceとEndpointの整合性確認
そもそも、Serviceの背後にPodが紐づいているか? これが最も多いミスです。
# Endpointが空になっていないか確認
kubectl get endpoints <service-name>
ステップ2: 接続テストの自動化(Pythonによる疎通確認)
簡単なPythonスクリプトで、接続先IPとポートを明示してテストします。
import socket
# Service IPとPort
target_ip = "10.96.0.10"
target_port = 80
def check_connection(ip, port):
try:
with socket.create_connection((ip, port), timeout=2) as sock:
print(f"成功: {ip}:{port} に接続できました")
except Exception as e:
print(f"失敗: {ip}:{port} - {e}")
# これをPodの中から実行し、iptablesの挙動を確認する
check_connection(target_ip, target_port)
ステップ3: NATの追跡(conntrack)
NAT処理はカーネルの conntrack テーブルに依存します。もし接続がタイムアウトする場合、ここが溢れているか、エントリが壊れている可能性があります。
# conntrackのテーブル数を確認(上限に達していないか?)
sysctl net.netfilter.nf_conntrack_count
—
まとめ:ネットワークは「見える化」から始まる
kube-proxy が行っているのは、パケットに対する「宛先書き換え」という極めてシンプルかつ強力な操作です。しかし、それが巨大な iptables ルール群や IPVS テーブルの中で行われるため、ブラックボックスに見えてしまいます。
- 小〜中規模なら
iptables: 設定の可視化がしやすく、トラブルシュートが容易。 - 大規模なら
IPVS: 性能重視だが、ツール(ipvsadm)への習熟が必要。
皆さんがもしWeb APIの設計やインフラ運用で悩んだら、まずは kubectl get endpoints で「宛先は生きているか」を確認し、次にノードへSSHして ipvsadm や iptables で「パケットの宛先が正しく書き換わっているか」を追ってください。
ネットワークは魔法ではありません。すべてはカーネルのテーブルに書き込まれた、冷徹なビットの列なのです。現場からは以上です!
コメント