【実務・中級編】 Ciliumを用いたBPFベースのSNAT(Source Network Address Translation)制御メカニズム – クラウド&コンテナネットワーク実践ガイド

さらばiptablesの呪縛:CiliumとeBPFが実現する「爆速」SNATの正体

現場でKubernetesのネットワークトラブルに頭を抱えたことはないだろうか?

「パケットがどこで捨てられているのか分からない」「conntrack のテーブルが溢れてパケットがロストする」「iptables のルールが数万行に膨れ上がり、通信のたびにCPUが悲鳴を上げている」……。これらは、大規模なK8sクラスターを運用するSREなら一度は通る「通過儀礼」のような悪夢だ。

今日は、そんな従来の iptables ベースのネットワークスタックを根底から覆し、eBPFの力で通信を最適化する「Cilium」のSNAT(送信元IPアドレス変換)の裏側に深く切り込んでいこう。

なぜ「iptables」では限界なのか

そもそも、なぜ我々は iptables に苦しめられるのか。それは、iptables が「パケットの旅」において、極めて非効率な場所で判断を下しているからだ。

パケットがLinuxカーネルのネットワークスタックを通る際、iptables はチェーンと呼ばれるリストを上から順に走査する。ルールが1つ増えるごとに、全パケットに対する評価コストが線形に増大する。加えて、マルチコア環境では conntrack のロック競合が発生し、トラフィックが急増した瞬間にレイテンシが跳ね上がる。

Ciliumは、この「愚直なリスト検索」を捨てた。代わりに、eBPF(extended Berkeley Packet Filter) を用いて、カーネルの深い場所(tc レイヤーや XDP)に直接「魔法の命令」を焼き付ける。

CiliumのBPF-SNAT:パケットはどう変貌するのか

CiliumにおけるSNATは、パケットがPodから外部ネットワークへ向かう出口(Egress)のタイミングで実行される。

1. パケットのキャッチ: 外部宛のパケットがPodからノードのネットワークインターフェースを通過する際、eBPFプログラムがそれを「フック」する。
2. マッピングテーブルの参照: CiliumのBPFマップには、送信元IPと変換後のIP(ノードのIPやNAT GatewayのIP)の対応表が格納されている。
3. ヘッダーの書き換え: カーネルのプロトコルスタックを駆け上がる前に、パケットの送信元IPアドレスをその場で書き換える。
4. チェックサムの再計算: ここが地味だが重要だ。IPアドレスが変わればチェックサムも変わる。eBPFはこれをハードウェアに近いレイヤーで瞬時に再計算する。

このフローには iptables が介入する余地がない。つまり、コンテキストスイッチも、複雑なチェーンの走査も発生しないのだ。

実践:CiliumでEgress NATを制御する

CiliumでSNATを制御する場合、CiliumEgressNATPolicy というカスタムリソースを用いるのが定石だ。以下に、特定のPod群からの通信を特定のNAT IPに変換する設定例を示す。

# egress-nat-policy.yaml
apiVersion: "cilium.io/v2"
kind: CiliumEgressNATPolicy
metadata:
  name: "egress-nat-policy-web-api"
spec:
  # どのPodからの通信を対象にするか(ラベルセレクター)
  egress:
    - podSelector:
        matchLabels:
          app: "backend-api"
  # 外部ネットワークの宛先(特定の外部API等)
  destinationCIDRs:
    - "192.0.2.0/24" 
  # 書き換え後の送信元IPアドレス
  egressSourceIP: "203.0.113.10"

この設定を適用すると、Ciliumは即座にノード上のeBPFマップを更新し、対象パケットの送信元IPを 203.0.113.10 に書き換える。

現場で役立つデバッグ術:パケットを追跡する

理論は理解しても、現場では「本当に書き換わっているのか?」という疑念がつきまとう。そんな時は、cilium monitor コマンドが最強の武器になる。

# パケットのドロップ理由やNATの挙動をリアルタイムで追跡
cilium monitor --type drop --type trace

また、Pod内から実際に通信を確認する際は、以下のPythonスクリプトで、自分自身のIPがどう見えているかを外部サーバーに問い合わせるのが確実だ。

import requests

# 外部のIP確認用APIにリクエストを投げる
# 期待通りであれば、Ciliumで指定した egressSourceIP が返ってくるはずだ
response = requests.get('https://api.ipify.org?format=json')
if response.status_code == 200:
    print(f"現在の送信元IP: {response.json()['ip']}")
else:
    print("通信失敗")

最後に:SREとして何を意識すべきか

CiliumのSNATは魔法ではない。それは、カーネルの機能を極限まで引き出した「最適化」の結果だ。

皆さんがインフラを設計する際、以下の3点を忘れないでほしい。

1. conntrackの枯渇は防げない: eBPFでSNATを高速化しても、最終的な接続先のファイアウォールやNAT Gateway側のセッションテーブルには限界がある。
2. 可観測性(Observability): Hubble を活用せよ。Ciliumのパワーを最大限に活かすには、Hubble UIでトラフィックのフローを可視化し、どこでNATが発生しているかを視覚的に把握することが不可欠だ。
3. カーネルバージョンの重要性: eBPFは比較的新しいカーネル機能に依存する。古いカーネルで無理をして動かそうとせず、安定したLTSバージョンのカーネルを採用するのが、トラブルを未然に防ぐ最大の知恵だ。

ネットワークの世界は奥が深いが、eBPFという強力な武器があれば、かつて不可能だと思われた複雑な制御も、驚くほどクリーンに実現できる。ぜひ、次のクラスタ構築では iptables の呪縛を解き放ち、Ciliumのパフォーマンスを体感してみてほしい。

コメント

タイトルとURLをコピーしました