Calicoの「闇」を暴く:IP-in-IPとSNATの排他制御がネットワークトラブルの元凶になる理由
現場で「なぜかPodからの外部通信がパケットロスする」「パケットが途中で消える」という不可解なトラブルに直面したことはないだろうか。KubernetesのネットワークをCalicoで組んでいるとき、その原因の多くはルーティングの最適化と、クラウド基盤側が求めるNATの「お作法」の不整合にある。
今回は、Calicoの真髄であるIP-in-IP(もしくはVXLAN)と、クラスター外通信時のSNAT(Masquerading)の密接かつ危険な関係について、現場の視点から深掘りする。
—
Calicoのトンネリング:パケットは「二重」に包まれている
Calicoの基本戦略は、Pod間通信をBGPでルーティングすることだ。しかし、AWSやGCPのようなクラウド環境では、ホスト(Node)が認識していないIPアドレス(PodのIP)をパケットに乗せてVPC内を流すと、クラウド側のルーターが「未知のIPだ」としてパケットをドロップしてしまう。
そこで登場するのが IP-in-IP や VXLAN といったトンネリング技術だ。
- IP-in-IP: パケット全体を、NodeのIPアドレスを送信元・宛先とするIPヘッダーで包む。オーバーヘッドが少ないが、セキュリティグループでの制御が少し厄介になる。
- VXLAN: L2オーバーレイを構築する。UDPで包むため、ロードバランサーや各種ネットワーク機器との親和性が高い。
ここで重要なのは、「Podのパケットは、トンネル内では内側のIPヘッダーとして隠蔽されている」という事実だ。
—
SNAT(Masquerading)という名の「覆面」
クラスター外、つまりインターネットやVPC内の他のリソースへ通信する場合、PodのIPアドレス(クラスタープライベートIP)をそのまま外に出すわけにはいかない。宛先サーバーは戻りパケットをどこに返せばいいのか分からないからだ。
ここでCalicoの natOutgoing 設定が登場する。
# CalicoのIPPool設定例
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
name: default-ipv4-ippool
spec:
cidr: 192.168.0.0/16
# trueにすると、クラスター外への通信時にNodeのIPへSNATされる
natOutgoing: true
natOutgoing: true にすると、Calicoはiptables(あるいは最近のトレンドであるeBPF)を操作し、PodからのパケットがNodeから出ていく際に、送信元IPをNode自身のIPに書き換える。これが「SNAT(Masquerading)」だ。
陥りやすい罠:トンネリングとSNATの排他
ここで現場が最も混乱するのは、「トンネリングを有効にしているのに、SNATが正しく適用されないケース」だ。
Calicoの IPIPMode が Always に設定されている場合、全ての通信がカプセル化される。もしAWSのVPC内部など、物理ネットワークがPodのIPを直接解釈できない環境でSNATをオフにすると、パケットはVPCのルーターに到達した瞬間にブラックホールへ消える。
逆に、natOutgoing: true を設定していても、Podのルーティングテーブルや cali インターフェースの挙動によって、SNATがバイパスされることがある。特に、Cloud NativeなCNI運用において、「ホストのルーティングテーブル」と「Calicoのiptablesルール」のどちらが先にパケットを捕まえるかという競合は、デバッグの鬼門だ。
—
現場で役立つデバッグ手順
PodからAPIサーバーへの通信がうまくいかないとき、私はまず以下のステップでパケットの生死を確認する。
1. tcpdump でパケットの「顔」を覗く
Nodeにログインし、パケットがどのヘッダーを持っているか確認する。
# Node上で、caliインターフェースを監視
# pod-ipは対象PodのIP
sudo tcpdump -ni any host <pod-ip> -vv
もしパケットが tunl0 インターフェース(IP-in-IP用)を通過しているのに、宛先にSNATされた形跡がない場合、そのパケットは物理ネットワークで捨てられている。
2. iptablesのカウンターを確認する
SNATが適用されているかを確認する最も確実な手段だ。
# NATテーブルを監視
sudo iptables -t nat -L cali-POSTROUTING -v -n
ここで MASQUERADE ルールのパケットカウンターが増えていないなら、Podの通信はNAT処理を「スキップ」していることになる。原因は、Podが属するIPPoolの natOutgoing が false であるか、通信先がCalicoによって「ローカル(クラスター内)」と誤判定されている可能性が高い。
—
開発者が知っておくべき「通信のライフサイクル」
Web APIを開発する際、以下のようにPythonで外部APIを叩くコードを書いたとする。
import requests
# 外部のAPIエンドポイント
url = "https://api.external-service.com/data"
try:
# タイムアウトを設けるのは鉄則だが、
# CalicoのSNATが死んでいると、この接続は即座にSYN-SENTで止まる
response = requests.get(url, timeout=5)
print(response.status_code)
except Exception as e:
# ここで出るエラーが「Connection Timeout」なら、
# 戻りパケットがNATの不整合で帰ってきていない可能性を疑う
print(f"Network Error: {e}")
もしエラーが Connection Timeout であれば、「パケットは出ているが、戻ってきていない」状況だ。これはSNATによる送信元IPの書き換えは成功しているが、戻りのパケットがNodeのルーティングテーブルで正しくPodへ戻されていない(Reverse Path Filteringの干渉など)ケースが多い。
—
SREからのアドバイス
Calicoの設定は、クラウドのネットワークトポロジーと強く結びついている。
- AWSやGCPを使うなら: 基本的にVPCのルーティングテーブルとPodのIPレンジを統合(VPCネイティブルーティング)し、IP-in-IPを使わない設定を目指すべきだ。
- どうしてもIP-in-IP/VXLANが必要なら: SNATのルールが、NodeのIPを正しく選択しているか、
iptablesのルール順序を確認する癖をつけてほしい。
Kubernetesのネットワークは「魔法」ではない。パケットは常に、物理的なNICから仮想的なインターフェースへと、泥臭いルールに従って流れている。その流れを可視化できれば、どんな難解なネットワークトラブルも、ただの「パケットの迷子探し」に過ぎない。
現場からは以上だ。次は、eBPFを用いた kube-proxy 置き換えの際の落とし穴について話そうと思う。
コメント