ブラウザーから Web アプリを開けたのに、アクセスログには利用者ではなくノードの IP が出ている。 通信が届く途中で、送信元のアドレスが変わることがあります。
この章では、ロードバランサーからノードを経由して Pod に届く経路を、図のステップ再生で追います。 各地点で宛先と送信元を見比べ、どこで、何のために書き換わるのかを確かめます。
GOAL この章のゴール
- ロードバランサーからノードを経由して Pod へ届く通信を追える
- 送信元 IP が書き換わる理由と、保持する設定が分かる
- NodePort を作り、ノードの IP とポートでアプリに接続できる
全体像
通信は次の 3 ステップで Pod に届きます。
- 通信が LB に到達し、LB が任意のノードに振り分ける。
- そのノードの kube-proxy (または eBPF) が、書き換え表で Pod の所在 (EndpointSlice) を引く。
- 宛先が Pod IP に書き換わり、Pod ネットワークを通って目的の Pod に到達する。
Service の type で言えば、手順 1 が LoadBalancer、手順 1 から 2 への受け渡しが NodePort、手順 2 が ClusterIP の書き換え表にあたります。
公式ドキュメントが「LoadBalancer はまず NodePort と同じ変更を行い、外部 LB をその NodePort に向ける」と書いていたとおり、第 5 章で見た入れ子がそのまま経路になります。
一歩ずつ追う
下のステップ再生で、パケットの現在地と、その時点の宛先と送信元を追ってください。
既定 (externalTrafficPolicy: Cluster) では、LB が選んだ Node A に目的の Pod が居ないケースを再生しています。
1/7クライアントが LB の IP (198.51.100.10:80) に接続する
Cluster (既定) では LB がどのノードに送っても届く代わりに、SNAT でクライアント IP が消え、余計なホップが増えます。Local では Pod の居るノードだけが受け、クライアント IP が残ります。
ステップ 3 と 4 で、Node A は宛先を Pod IP に書き換える (DNAT) だけでなく、送信元を自分の IP に書き換えています (SNAT)。 公式ドキュメントも、ClusterIP 宛てではクライアント IP を書き換えずに転送するが、NodePort や LB 経由ではクライアント IP が変更される、と書いています。 この SNAT が要る理由は、しない場合を考えると分かります。
NAT は往復で対にして初めて成立します。
行きで宛先を書き換えたノードを、戻りも必ず通す必要があります。
だからノードを跨いで転送するときは送信元もそのノードにしておく。
これが NodePort の SNAT (masquerade) で、第 2 章で nft を使って自分で入れたものと同じ動作です。
代償として、Pod から見た送信元はノードの IP になり、クライアントの IP は失われます。
externalTrafficPolicy
クライアント IP を残したい、余計なホップを減らしたい、という要求に応えるのが externalTrafficPolicy: Local です。
- Cluster (既定):どのノードに届いても、Pod の居るノードへ転送する。LB のヘルスチェックはサービスプロキシの readiness (kube-proxy なら
:10256/healthz) を見るので、全ノードが healthy。均等に散らばるが、ホップが増え、クライアント IP が消える。 - Local:API リファレンスの説明では「外部 LB がノード間の振り分けを担うと仮定し、各ノードは自分の上の endpoint にだけ、クライアントの送信元 IP を masquerade せずに届ける。endpoint の無いノードに届いた通信は捨てられる」。
type: LoadBalancerのときはhealthCheckNodePortが自動で割り当てられ、LB はそこで Pod の有無を確かめて振り分け対象を絞る。ノードごとの Pod 数が違うと負荷が偏る。
上のシミュレータのトグルで Local に切り替えると、LB が Node B にだけ送り、SNAT のステップが消えるのが分かります。
演習クラスタで NodePort を辿る
演習クラスタの cloud-controller-manager は LoadBalancer 機能を持たないので (第 9 章で扱います)、外部 LB は作られません。
その代わり、NodePort までは辿れます。
第 5 章の web Service を NodePort に変えて、ノードの IP から届くことを確認します。
kubectl patch svc web -p '{"spec":{"type":"NodePort"}}'
kubectl get svc web
NODE_IP=$(kubectl get node -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
NODE_PORT=$(kubectl get svc web -o jsonpath='{.spec.ports[0].nodePort}')
echo "$NODE_IP:$NODE_PORT"
curl -s -o /dev/null -w '%{http_code}\n' "http://$NODE_IP:$NODE_PORT/"出力例
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web NodePort 10.96.12.34 <none> 80:31234/TCP 10m
10.0.1.11:31234
200ClusterIP (10.96.12.34:80) と NodePort (10.0.1.11:31234) のどちらに送っても同じ Pod に届きます。
Cilium の表にも NodePort の行が増えています。
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg service list | grep -B1 -A2 NodePort | head -12出力例
13 0.0.0.0:31234/TCP NodePort 1 => 10.244.0.23:80/TCP (active)
2 => 10.244.0.31:80/TCP (active)
14 10.0.1.11:31234/TCP NodePort 1 => 10.244.0.23:80/TCP (active)
2 => 10.244.0.31:80/TCP (active)Pod 側のアクセスログで、送信元がどう見えるかも確認しておきます。 演習クラスタは単一ノードなので backend は必ず同じノードに居て、ClusterIP 経由でも NodePort 経由でも、nginx のログには ターミナルの実行環境 の IP がそのまま出ます。
MY_IP=$(ip -4 addr show eth0 | awk '/inet /{print $2}' | cut -d/ -f1)
echo "workbench: $MY_IP"
curl -s -o /dev/null "http://10.96.12.34/" ; curl -s -o /dev/null "http://$NODE_IP:$NODE_PORT/"
kubectl logs -l app=web --tail=4 | awk '{print $1}'出力例
workbench: 10.244.0.9
10.244.0.9
10.244.0.9複数ノードのクラスタで Pod の無いノードの NodePort に送ると、ここにそのノードの IP が出ます。 それが上の図で見た SNAT の跡です。
healthCheckNodePort は type: LoadBalancer で Local にしたときだけ割り当てられるので、LB が pending のままでも、フィールドが埋まる様子は見られます。
kubectl patch svc web -p '{"spec":{"type":"LoadBalancer","externalTrafficPolicy":"Local"}}'
kubectl get svc web -o jsonpath='{.spec.healthCheckNodePort}{"\n"}'
curl -s -o /dev/null -w '%{http_code}\n' "http://$NODE_IP:$NODE_PORT/"
kubectl patch svc web -p '{"spec":{"type":"ClusterIP","externalTrafficPolicy":null}}'出力例
31567
200healthCheckNodePort の 31567 番が、LB のヘルスチェック先になるポートです。
最後の行で設定を元に戻しています。
ふりかえり
Qtype: LoadBalancer の通信が辿る順番として正しいのは?
LB はノードの NodePort に送り、ノード上の書き換え表 (ClusterIP の宛先を書き換えるものと同じ) が Pod IP に書き換えます。LB が Pod に直接送る構成は第 8 章のコンテナネイティブ LB です。
QNodePort で別ノードの Pod へ転送するときに SNAT する理由は?
NAT は往復で対になる必要があります。行きで宛先を書き換えたノードを戻りも通さないと、client は見知らぬ送信元からの応答を捨てます。SNAT はそのための措置で、クライアント IP が消えるのは副作用です。
QexternalTrafficPolicy: Local の説明として正しいのは?
Local では、受けたノードは同じノードの Pod にだけ転送し、送信元 IP を masquerade しません。type: LoadBalancer なら healthCheckNodePort で Pod の無いノードを LB の対象から外せます。代わりにノード間で負荷が偏ることがあります。
参考
- Kubernetes ドキュメント「Service」(LoadBalancer は NodePort の上に作られる、traffic policy) https://kubernetes.io/docs/concepts/services-networking/service/
- Kubernetes ドキュメント「Virtual IPs and Service Proxies」(NodePort / LB 経由ではクライアント IP が変わる、traffic policy と LB のヘルスチェック、trafficDistribution) https://kubernetes.io/docs/reference/networking/virtual-ips/
- Kubernetes API リファレンス「Service v1」(externalTrafficPolicy、healthCheckNodePort の割り当て条件、trafficDistribution) https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/
- Cilium ドキュメント「Kubernetes Without kube-proxy」(NodePort の SNAT モードと DSR モード) https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/