kubernetesでクライアントのグローバルIPを取得する

kubernetesのデフォルト挙動では、podが取得するリモートIPがクラスターネットワークのIPになっています。
そのため、httpサーバーのログにも、10.0.0.1などのプライベートIPアクセスが記録されます。IPv6トラフィックの場合も同様です。

多くの場合、クラウドプロバイダが提供するプロキシサービスの機能を活用してIPを抽出することになるでしょう。
HTTPの場合には、X-FORWARDED-FORにプロキシがホップごとのIPを記録する規約があります。その他、TCP/IPのレベルでは Proxy Protocol を使う必要があるでしょう。

NodePortやLoadBalancerの場合

k8sのServiceを素朴に実装するとクライアントのグローバルIPがNATで変換されてしまいますが、クラスタ機能の制約と引き換えに クライアントIPを取得できるオプション もあります。
( Loss of client source IP for external traffic )

クラスタ実装に依存する面がありますが、GKEのリージョナルLoadBalancerでは動作します。
Serviceの設定例は以下のとおりです。specセクションにexternalTrafficPolicy: Localという設定を追加します。

apiVersion: v1
kind: Service
metadata:
  labels:
    name: web
  name: web
spec:
  externalTrafficPolicy: Local
  ports:
    - name: http
      port: 80
      targetPort: web-http
      protocol: TCP
  selector:
    name: web
  type: LoadBalancer
  loadBalancerIP: 192.168.0.1

なおexternalTrafficPolicy: Localはリクエストを受ける全ノードにPodが存在する前提であるため、素朴な想定ではDaemonSetと組み合わせる必要があるでしょう。

※ k8s v1.6あたりで、 リクエスト消失のネットワーク障害 が起きていました。古いクラスタの場合、利用バージョンに注意が必要です。

ネットワークアクセスの継続性に注意が必要

Serviceの再起動は、Pod類と同様、

$ kubectl apply -f service_config.yml

で実行できます。リソース競合でエラーになる場合はdelete -> applyを実行します。

この処理は、Webサービスの玄関にあたる設定を変えるため、注意が必要です。

まず、ロードバランサーの再作成が走るため、しばらくネットワークアクセスが中断します。

また、再起動前にIPを固定する設定を追加しておかないと、再起動にあたり外部IPを新たに取得する挙動になる可能性があります。IPが変わると、DNS設定も変更しないと現行のホスト名でアクセスできなくなります。
外部IPの固定については、 Google Container Engine の外部IPを指定する で解説しています。

PodのhostPortはクライアントIPを保持している

もはやクラスタとは言えない構成を含めるなら、podSpecのports.[].hostPortを指定するとPodが起動したホストのポートを直接listenします。一般的にはクローズドなネットワークにサービス公開する構成になるでしょう。
ファイアーウォール設定を通じてこのポートにアクセスできたとすると、このトラフィックはk8sのルーティングを介していないため、クライアントのグローバルIPを直接取得できます。

Proxy Protocol

TCP通信(非HTTP)のアクセス元IPの伝播には、Proxy Protocolが必要です。

コンテナイメージ

Proxy ProtocolのCODECツールとして、 go-mmproxy が手軽です。
バイナリと以下のスタートアップスクリプトを同梱したコンテナイメージを作成しておきます。

#!/bin/bash

ip rule add from 127.0.0.1/8 iif lo table 123
ip route add local 0.0.0.0/0 dev lo table 123

go-mmproxy -l 0.0.0.0:25577 -4 127.0.0.1:<target_port> -allowed-subnets </path/to/subnet.txt>

<target_port>は実アプリのポートで、たとえばsshdのウェルノウンポートは22番です。
<subnet.txt>はアクセス許可するIP範囲を記載したテキストで、制限なしの設定例は以下のような一行です。

0.0.0.0/0

サイドカーのマニフェスト

sshdのサイドカー構成のコンテナスペックの抜粋は以下のようになります。

    spec:
      containers:
      - name: mmproxy
        # image: required
        ports:
        - containerPort: 25577
          name: mmproxy
        securityContext:
          capabilities:
            add: ["NET_ADMIN"]
      - name: sshd
        # image: required
        ports:
        - containerPort: 22
          name: sshd

NET_ADMIN特権はスタートアップスクリプトのipコマンドに必要です。
TCPサービスからの戻りのパケットをキャプチャして書き換えます。

実サービスのコンテナポート(上の例の22)は公開しなくても動作するでしょう。
外部との通信は25577番に集約します。

ヘルスチェックに注意

Proxy Protocolを使用する場合、フロントにプロキシがある想定ですが、ヘルスチェックには注意が必要です。

go-mmproxyは互換性のないパケットを受信すると通信をドロップします。Google Cloudの構成では、ヘルスチェックは通過するものの、実通信の90%程度が失敗する挙動になりました。

ヘルスチェック対象をgo-mmproxyではなく実サービスに代えることで通信が安定しました。上の例ではヘルスチェック用に22番を定義しています。 TCPプロキシはログ出力も乏しく、非常に分かりにくい挙動となるため注意が必要です。

⁋ 2017/04/18↻ 2024/08/29