送信元IPを使用する

Kubernetesクラスター内で実行されているアプリケーションは、Serviceという抽象化を経由して、他のアプリケーションや外の世界との発見や通信を行います。このドキュメントでは、異なる種類のServiceに送られたパケットの送信元IPに何が起こるのか、そして必要に応じてこの振る舞いを切り替える方法について説明します。

始める前に

用語

このドキュメントでは、以下の用語を使用します。

NAT
ネットワークアドレス変換(network address translation)
送信元NAT
パケットの送信元のIPを置換します。このページでは、通常ノードのIPアドレスを置換することを意味します。
送信先NAT
パケットの送信先のIPを置換します。このページでは、通常PodのIPアドレスを置換することを意味します。
VIP
Kubernetes内のすべてのServiceなどに割り当てられる仮想IPアドレス(virtual IP address)です。
kube-proxy
すべてのノード上でServiceのVIPを管理するネットワークデーモンです。

前提条件

Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:

以下の例では、HTTPヘッダー経由で受け取ったリクエストの送信元IPをエコーバックする、小さなnginxウェブサーバーを使用します。次のコマンドでウェブサーバーを作成できます。

kubectl create deployment source-ip-app --image=k8s.gcr.io/echoserver:1.4

出力は次のようになります。

deployment.apps/source-ip-app created

目標

  • 単純なアプリケーションを様々な種類のService経由で公開する
  • それぞれの種類のServiceがどのように送信元IPのNATを扱うかを理解する
  • 送信元IPを保持することに関わるトレードオフを理解する

Type=ClusterIPを使用したServiceでの送信元IP

kube-proxyがiptablesモード(デフォルト)で実行されている場合、クラスター内部からClusterIPに送られたパケットに送信元のNATが行われることは決してありません。kube-proxyが実行されているノード上でhttp://localhost:10249/proxyModeにリクエストを送って、kube-proxyのモードを問い合わせてみましょう。

kubectl get nodes

出力は次のようになります。

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

これらのノードの1つでproxyモードを取得します(kube-proxyはポート10249をlistenしています)。

# このコマンドは、問い合わせを行いたいノード上のシェルで実行してください。
curl http://localhost:10249/proxyMode

出力は次のようになります。

iptables

source IPアプリのServiceを作成することで、送信元IPが保持されているかテストできます。

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

出力は次のようになります。

service/clusterip exposed
kubectl get svc clusterip

出力は次のようになります。

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

そして、同じクラスター上のPodからClusterIPにアクセスします。

kubectl run busybox -it --image=busybox --restart=Never --rm

出力は次のようになります。

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

これで、Podの内部でコマンドが実行できます。

# このコマンドは、"kubectl run" のターミナルの内部で実行してください
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

そして、wgetを使用してローカルのウェブサーバーに問い合わせます。

# "10.0.170.92" の部分をService名が"clusterip"のIPv4アドレスに置き換えてください
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

client_addressは常にクライアントのPodのIPアドレスになります。これは、クライアントのPodとサーバーのPodが同じノード内にあっても異なるノードにあっても変わりません。

Type=NodePortを使用したServiceでの送信元IP

Type=NodePortを使用したServiceに送られたパケットは、デフォルトで送信元のNATが行われます。NodePort Serviceを作ることでテストできます。

kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

出力は次のようになります。

service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

クラウドプロバイダーで実行する場合、上に示したnodes:nodeportに対してファイアウォールのルールを作成する必要があるかもしれません。それでは、上で割り当てたノードポート経由で、クラスターの外部からServiceにアクセスしてみましょう。

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

出力は次のようになります。

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

これらは正しいクライアントIPではなく、クラスターのinternal IPであることがわかります。ここでは、次のようなことが起こっています。

  • クライアントがパケットをnode2:nodePortに送信する
  • node2は、パケット内の送信元IPアドレスを自ノードのIPアドレスに置換する(SNAT)
  • node2は、パケット内の送信先IPアドレスをPodのIPアドレスに置換する
  • パケットはnode1にルーティングされ、endpointにルーティングされる
  • Podからの応答がnode2にルーティングされて戻ってくる
  • Podからの応答がクライアントに送り返される

図で表すと次のようになります。

graph LR; client(client)-->node2[Node 2]; node2-->client; node2-. SNAT .->node1[Node 1]; node1-. SNAT .->node2; node1-->endpoint(Endpoint); classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; class node1,node2,endpoint k8s; class client plain;

クライアントのIPが失われることを回避するために、Kubernetesにはクライアントの送信元IPを保持する機能があります。service.spec.externalTrafficPolicyの値をLocalに設定すると、kube-proxyはローカルに存在するエンドポイントへのプロキシーリクエストだけをプロキシーし、他のノードへはトラフィックを転送しなくなります。このアプローチでは、オリジナルの送信元IPアドレスが保持されます。ローカルにエンドポイントが存在しない場合には、そのノードに送信されたパケットは損失します。そのため、エンドポイントに到達するパケットに適用する可能性のあるパケット処理ルールでは、送信元IPが正しいことを信頼できます。

次のようにしてservice.spec.externalTrafficPolicyフィールドを設定します。

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

出力は次のようになります。

service/nodeport patched

そして、再度テストしてみます。

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

出力は次のようになります。

client_address=198.51.100.79

今度は、正しいクライアントIPが含まれる応答が1つだけ得られました。これは、エンドポイントのPodが実行されているノードから来たものです。

ここでは、次のようなことが起こっています。

  • クライアントがパケットをエンドポイントが存在しないnode2:nodePortに送信する
  • パケットが損失する
  • クライアントがパケットをエンドポイントが存在するnode1:nodePortに送信する
  • node1は、正しい送信元IPを持つパケットをエンドポイントにルーティングする

図で表すと次のようになります。

graph TD; client --> node1[Node 1]; client(client) --x node2[Node 2]; node1 --> endpoint(endpoint); endpoint --> node1; classDef plain fill:#ddd,stroke:#fff,stroke-width:4px,color:#000; classDef k8s fill:#326ce5,stroke:#fff,stroke-width:4px,color:#fff; class node1,node2,endpoint k8s; class client plain;

Type=LoadBalancerを使用したServiceでの送信元IP

Type=LoadBalancerを使用したServiceに送られたパケットは、デフォルトで送信元のNATが行われます。Ready状態にあるすべてのスケジュール可能なKubernetesのNodeは、ロードバランサーからのトラフィックを受付可能であるためです。そのため、エンドポイントが存在しないノードにパケットが到達した場合、システムはエンドポイントが存在するノードにパケットをプロシキーします。このとき、(前のセクションで説明したように)パケットの送信元IPがノードのIPに置換されます。

ロードバランサー経由でsource-ip-appを公開することで、これをテストできます。

kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

出力は次のようになります。

service/loadbalancer exposed

ServiceのIPアドレスを表示します。

kubectl get svc loadbalancer

出力は次のようになります。

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

次に、Serviceのexternal-ipにリクエストを送信します。

curl 203.0.113.140

出力は次のようになります。

CLIENT VALUES:
client_address=10.240.0.5
...

しかし、Google Kubernetes EngineやGCE上で実行している場合、同じservice.spec.externalTrafficPolicyフィールドをLocalに設定すると、ロードバランサーからのトラフィックを受け付け可能なノードのリストから、Serviceエンドポイントが存在しないノードが強制的に削除されます。この動作は、ヘルスチェックを意図的に失敗させることによって実現されています。

図で表すと次のようになります。

Source IP with externalTrafficPolicy

アノテーションを設定することで動作をテストできます。

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

Kubernetesにより割り当てられたservice.spec.healthCheckNodePortフィールドをすぐに確認します。

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

出力は次のようになります。

  healthCheckNodePort: 32122

service.spec.healthCheckNodePortフィールドは、/healthzでhealth checkを配信しているすべてのノード上のポートを指しています。次のコマンドでテストできます。

kubectl get pod -o wide -l run=source-ip-app

出力は次のようになります。

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

curlを使用して、さまざまなノード上の/healthzエンドポイントからデータを取得します。

# このコマンドは選んだノードのローカル上で実行してください
curl localhost:32122/healthz
1 Service Endpoints found

ノードが異なると、得られる結果も異なる可能性があります。

# このコマンドは、選んだノード上でローカルに実行してください
curl localhost:32122/healthz
No Service Endpoints Found

コントロールプレーン上で実行中のコントローラーは、クラウドのロードバランサーを割り当てる責任があります。同じコントローラーは、各ノード上のポートやパスを指すHTTPのヘルスチェックも割り当てます。エンドポイントが存在しない2つのノードがヘルスチェックに失敗するまで約10秒待った後、curlを使用してロードバランサーのIPv4アドレスに問い合わせます。

curl 203.0.113.140

出力は次のようになります。

CLIENT VALUES:
client_address=198.51.100.79
...

クロスプラットフォームのサポート

Type=LoadBalancerを使用したServiceで送信元IPを保持する機能を提供しているのは一部のクラウドプロバイダだけです。実行しているクラウドプロバイダによっては、以下のように異なる方法でリクエストを満たす場合があります。

  1. クライアントとのコネクションをプロキシーが終端し、ノードやエンドポイントとの接続には新しいコネクションが開かれる。このような場合、送信元IPは常にクラウドのロードバランサーのものになり、クライアントのIPにはなりません。

  2. クライアントからロードバランサーのVIPに送信されたリクエストが、中間のプロキシーではなく、クライアントの送信元IPとともにノードまで到達するようなパケット転送が使用される。

1つめのカテゴリーのロードバランサーの場合、真のクライアントIPと通信するために、 HTTPのForwardedヘッダーやX-FORWARDED-FORヘッダー、proxy protocolなどの、ロードバランサーとバックエンドの間で合意されたプロトコルを使用する必要があります。2つ目のカテゴリーのロードバランサーの場合、Serviceのservice.spec.healthCheckNodePortフィールドに保存されたポートを指すHTTPのヘルスチェックを作成することで、上記の機能を活用できます。

クリーンアップ

Serviceを削除します。

kubectl delete svc -l app=source-ip-app

Deployment、ReplicaSet、Podを削除します。

kubectl delete deployment source-ip-app

次の項目

最終更新 May 12, 2021 at 12:43 AM PST : [ja] Update HAProxy Protocol Link (0e8b3e4aa)