送信元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=registry.k8s.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からの応答がクライアントに送り返される
図で表すと次のようになります。
クライアントの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を持つパケットをエンドポイントにルーティングする
図で表すと次のようになります。
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エンドポイントが存在しないノードが強制的に削除されます。この動作は、ヘルスチェックを意図的に失敗させることによって実現されています。
図で表すと次のようになります。
アノテーションを設定することで動作をテストできます。
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を保持する機能を提供しているのは一部のクラウドプロバイダだけです。実行しているクラウドプロバイダによっては、以下のように異なる方法でリクエストを満たす場合があります。
-
クライアントとのコネクションをプロキシーが終端し、ノードやエンドポイントとの接続には新しいコネクションが開かれる。このような場合、送信元IPは常にクラウドのロードバランサーのものになり、クライアントのIPにはなりません。
-
クライアントからロードバランサーの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
次の項目
- Service経由でアプリケーションに接続する方法についてさらに学ぶ。
- External Load Balancerを作成する方法について学ぶ。