---
layout: blog
title: K8sクラスターでロードバランサー後のリクエストのソースIPを保持する方法
categories: ネットワーク
tags: [ネットワーク, blog]
date: 2024-05-27 11:52:22 +0800
draft: false
toc: false
comments: true
---
引言
アプリケーションのデプロイは常に単純なインストールと実行とは限りません。時にはネットワークの問題を考慮する必要があります。本文では、K8sクラスターでサービスがリクエストのソースIPを取得できるようにする方法を紹介します。
アプリケーションがサービスを提供する際は、一般的に入力情報に依存します。入力情報が5タプル(ソースIP、ソースポート、宛先IP、宛先ポート、プロトコル)に依存しない場合、そのサービスはネットワークとの結合性が低いため、ネットワークの詳細を気にする必要はありません。
したがって、ほとんどの人には本文を読む必要はありません。ネットワークに興味がある場合や視野を広げたい場合は、以下の本文を読み進めて、より多くのサービスシナリオを理解してください。
本文はK8s v1.29.4を基にしています。文中では一部podとendpointを混用していますが、本文のシナリオでは同等とみなせます。
誤りがあればご指摘ください。速やかに修正します。
なぜソースIP情報が失われるのか?
まず、ソースIPとは何かを明確にしましょう。AがBにリクエストを送信し、BがリクエストをCに転送する場合、Cが見るIPプロトコルのソースIPはBのIPですが、本文ではAのIPをソースIPとみなします。
主に以下の2つの動作でソース情報が失われます:
- ネットワークアドレス変換(NAT):公衆IPv4の節約やロードバランシングなどを目的とします。サービス側が見るソースIPはNATデバイスのIPとなり、真のソースIPではありません。
- プロキシ(Proxy):リバースプロキシ(RP, Reverse Proxy)とロードバランサー(LB, Load Balancer)がこれに該当し、以下ではプロキシサーバーと総称します。この種のプロキシサービスはリクエストをバックエンドサービスに転送しますが、ソースIPを自身のIPに置き換えます。
- NATは簡単に言えばポート空間でIP空間を交換するものです。IPv4アドレスが限られているため、1つのIPアドレスで65535個のポートをマッピングでき、ほとんどの場合これらのポートを使い切らないため、複数のサブネットIPが1つの公衆IPを共有し、ポートでサービスを区別します。その使用形式は:
public IP:public port -> private IP_1:private portです。詳細はネットワークアドレス変換を参照してください。 - プロキシサービスは隠蔽または公開を目的とします。プロキシサービスはリクエストをバックエンドサービスに転送しつつ、ソースIPを自身のIPに置き換えてバックエンドサービスの実際のIPを隠し、セキュリティを保護します。その使用形式は:
client IP -> proxy IP -> server IPです。詳細はプロキシを参照してください。
NATとプロキシサーバーは非常に一般的で、ほとんどのサービスはリクエストのソースIPを取得できません。
これはソースIPを変更する一般的な2つの方法です。他にあれば補足をお願いします。
ソースIPをどのように保持するか?
以下はHTTPリクエストの例です:
| フィールド | 長さ(バイト) | ビットオフセット | 説明 |
|---|---|---|---|
| IPヘッダー | |||
ソースIP | 4 | 0-31 | 送信元のIPアドレス |
| 宛先IP | 4 | 32-63 | 受信元のIPアドレス |
| TCPヘッダー | |||
| ソースポート | 2 | 0-15 | 送信ポート番号 |
| 宛先ポート | 2 | 16-31 | 受信ポート番号 |
| シーケンス番号 | 4 | 32-63 | 送信元が送信したデータのバイトストリームを識別 |
| 確認番号 | 4 | 64-95 | ACKフラグが設定されている場合、次の期待されるシーケンス番号 |
| データオフセット | 4 | 96-103 | データ開始位置がTCPヘッダーからのバイト数 |
| 予約 | 4 | 104-111 | 予約フィールド、使用せず0に設定 |
| フラグ位 | 2 | 112-127 | SYN、ACK、FINなどの各種制御フラグ |
| ウィンドウサイズ | 2 | 128-143 | 受信側が受信可能なデータ量 |
| チェックサム | 2 | 144-159 | 传输中のエラー検出に使用 |
| 緊急ポインター | 2 | 160-175 | 送信元が受信元に迅速に処理してほしい緊急データの位置 |
| オプション | 可変 | 176-… | タイムスタンプ、最大セグメント長などを含む可能性 |
| HTTPヘッダー | |||
| リクエスト行 | 可変 | …-… | リクエストメソッド、URI、HTTPバージョンを含む |
ヘッダーフィールド | 可変 | …-… | Host、User-Agentなどの各種ヘッダーフィールド |
| 空行 | 2 | …-… | ヘッダーとボディ部分を分離 |
| ボディ | 可変 | …-… | オプションのリクエストまたはレスポンス本文 |
上記のHTTPリクエスト構造を閲覧すると、TCPオプション、リクエスト行、ヘッダーフィールド、ボディが可変であることがわかります。そのうちTCPオプションのスペースは限定的で一般的にソースIP伝達に使用されず、リクエスト行は固定情報で拡張不可、HTTPボディは暗号化後修正不可のため、HTTPヘッダーフィールドのみがソースIP拡張伝達に適しています。
HTTPヘッダーにX-REAL-IPフィールドを追加してソースIPを伝達できます。この操作は通常プロキシサーバーで行われ、プロキシサーバーがリクエストをバックエンドサービスに送信すると、バックエンドサービスはこのフィールドからソースIP情報を取得できます。
注意:プロキシサーバーがNATデバイスより前に配置されていることを保証する必要があります。これにより、真のリクエストのソースwhoamiを取得できます。阿里雲の製品ではロードバランサーが独立した商品カテゴリとして存在し、ネットワーク上の位置が通常のアプリケーションサーバーと異なります。
K8S操作ガイド
whoamiプロジェクトを例にデプロイします。
Deploymentの作成
まずサービスを作成します:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami-deployment
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: docker.io/traefik/whoami:latest
ports:
- containerPort: 8080
このステップでDeploymentを作成し、3つのPodを含み、各podに1つのコンテナが含まれ、whoamiサービスが実行されます。
Serviceの作成
NodePortまたはLoadBalancerタイプのサービスを作成して外部アクセスをサポートするか、ClusterIPタイプのサービスを作成してクラスター内アクセス専用とし、Ingressサービスを追加して外部アクセスを公開します。
NodePortはNodeIP:NodePortまたはIngressサービス経由でアクセス可能でテストに便利です。本節ではNodePortサービスを使用します。
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
サービス作成後、curl whoami.example.com:30002でアクセスすると、返されるIPはNodeIPで、リクエストのソースwhoamiではありません。
注意:これは正しいクライアントIPではありません。これらはクラスターの内部IPです。起こっていることは:
- クライアントがnode2:nodePortにデータパケットを送信
- node2がデータパケットのソースIPアドレスを自身のIPアドレスに置き換え(SNAT)
- node2がデータパケットの宛先IPをPod IPに置き換え
- データパケットがnode1にルーティングされ、エンドポイントへ
- Podの返信がnode2にルーティングされ返信
- Podの返信がクライアントに送信
図で表す:

externalTrafficPolicy: Localの設定
この状況を避けるため、KubernetesにはクライアントソースIPを保持する機能があります。service.spec.externalTrafficPolicyをLocalに設定すると、kube-proxyはローカルエンドポイントにのみリクエストをプロキシし、他のノードにトラフィックを転送しません。
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
externalTrafficPolicy: Local
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
curl whoami.example.com:30002でテストすると、whoami.example.comがクラスターの複数ノードのIPにマッピングされている場合、一定割合でアクセス不可となります。ドメイン記録がendpoint(pod)所在ノードのIPのみを含むことを確認する必要があります。
この設定には代償があり、クラスター内のロードバランシング能力を失います。クライアントはendpointがデプロイされたノードにのみアクセスして応答を得られます。

クライアントがNode 2にアクセスした場合、応答がありません。
Ingressの作成
ほとんどのサービスはユーザー向けにhttp/httpsを使用します。https://ip:port形式はユーザーに馴染みが薄いため、一般的にIngressを使用して上記のNodePortサービスをドメインの80/443ポートにロードバランシングします。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami-ingress
namespace: default
spec:
ingressClassName: external-lb-default
rules:
- host: whoami.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami-service
port:
number: 80
適用後、curl whoami.example.comでテストすると、ClientIPは常にendpoint所在ノード上のIngress ControllerのPod IPとなります。
root@client:~# curl whoami.example.com
...
RemoteAddr: 10.42.1.10:56482
...
root@worker:~# kubectl get -n ingress-nginx pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-controller-c8f499cfc-xdrg7 1/1 Running 0 3d2h 10.42.1.10 k3s-agent-1 <none> <none>
IngressリバースプロキシでNodePortサービスを使用すると、endpointの前に2層のserviceが追加されます。下図は両者の違いを示します。
graph LR
A[Client] -->|whoami.example.com:80| B(Ingress)
B -->|10.43.38.129:32123| C[Service]
C -->|10.42.1.1:8080| D[Endpoint]graph LR
A[Client] -->|whoami.example.com:30001| B(Service)
B -->|10.42.1.1:8080| C[Endpoint]パス1では、外部からIngressにアクセスすると、最初に到達するendpointはIngress Controllerで、次にwhoamiのendpointです。
Ingress Controllerは本質的にLoadBalancerサービスです。
kubectl -n ingress-nginx get svc
NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE
default echoip-ingress nginx ip.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 18h
default whoami-ingress nginx whoami.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 16h
したがって、前述のexternalTrafficPolicyをIngress Controllerに設定することでソースIPを保持できます。
同時に、ingress-nginx-controllerのconfigmapでuse-forwarded-headersをtrueに設定し、Ingress ControllerがX-Forwarded-ForまたはX-REAL-IPフィールドを認識できるようにします。
apiVersion: v1
data:
allow-snippet-annotations: "false"
compute-full-forwarded-for: "true"
use-forwarded-headers: "true"
enable-real-ip: "true"
forwarded-for-header: "X-Real-IP" # X-Real-IP or X-Forwarded-For
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.10.1
name: ingress-nginx-controller
namespace: ingress-nginx
NodePortサービスとingress-nginx-controllerサービスの違いは、主にNodePortのバックエンドが通常各ノードにデプロイされないのに対し、ingress-nginx-controllerのバックエンドは通常各外部公開ノードにデプロイされる点です。
NodePortサービスでexternalTrafficPolicyを設定するとクロスノードリクエストが無応答になるのに対し、IngressはリクエストでまずHEADERを設定してからプロキシ転送するため、ソースIP保持とロードバランシングの両能力を実現します。
まとめ
- アドレス変換(NAT)、プロキシ(Proxy)、リバースプロキシ(Reverse Proxy)、**ロードバランサー(Load Balance)**でソースIPが失われます。
- ソースIP喪失を防ぐため、プロキシサーバー転送時に真のIPをHTTPヘッダーフィールド
X-REAL-IPに設定してプロキシサービス経由で伝達します。多層プロキシの場合、X-Forwarded-Forフィールドを使用し、スタック形式でソースIPとプロキシパスのIPリストを記録します。 - クラスターNodePortサービスで
externalTrafficPolicy: Localを設定するとソースIPを保持できますが、ロードバランシング能力を失います。 - ingress-nginx-controllerをすべてのloadbalancerロールノードにdaemonset形式でデプロイする前提で、
externalTrafficPolicy: Localを設定するとソースIPを保持しつつロードバランシング能力を保持します。