お久しぶりです。ついに在宅勤務になったstreampackのfadoです。個人的には会社の方が色々と作業等捗るのですが在宅なら人との接触7~8割減という目標が達成できそうでいいのかもしれません。出社もそうですがここ最近、当たり前のことがそうではなくなり、まだまだ厳しい状況が続いていくのだろうと思うと不安は拭えないですね。

今回は当たり前だと思っていたNetwork Load Balancerの挙動がAWS Fargateを挟むとうまくいかなかった、それにまつわるお話を共有したいと思います。

背景

Client側がfirewallのルールなどにより特定のIPアドレスにしかアクセスできないシチュエーションに対応するため、静的IPアドレスをサポートするNetwork Load Balancerを採用しました。

それに加え、アプリケーション側でログや統計の解析のためClient IPアドレスをそのまま保持したかったのですが、アプリケーションのログにNetwork Load BalancerのプライベートIPアドレスが記録されていた問題に直面しました。

調べても有力な情報があまり見つからなかったので色々と検証して解決にたどり着きました。同じく壁にぶち当たった方々のために参考になれば何よりです!

リソースと構成

検証環境はAWS上で構築しました。

  • Clientの送信元IPアドレス(以下 Client IPアドレス)
  • Elastic Load Balancer(以下 ELB)の種類の一つであるNetwork Load Balancer(以下 NLB)
  • Amazon ECS
  • AWS Fargate
  • Dockerコンテナアプリケーション: nginx (以下 AWS Fargate(nginx))
  • Webアプリケーション: nginx

NLBの詳細設定とAmazon ECSのセットアップ等は割愛させて頂きます。

原因

色々と調べた結果、原因はNLBの仕様によるもので、NLBのターゲットグループの種類が IPであることが分かりました。

NLBで設定するターゲットグループのターゲットの種類によって保持するClient IPアドレスが変わってきます。
ターゲットの種類が、

1.インスタンス ID の場合はClient IPアドレス
2.IP の場合はNLBのプライベートIPアドレス

が保持され、アプリケーションに提供されます。

Amazon ECSにて起動タイプとしてAWS Fargate、かつNLBを利用する際、ターゲットグループが自動的に作成され、ターゲットの種類がIPとなります。

対処法

NLBProxy Protocol機能を使い、Proxy ProcotolのヘッダーからClient IPアドレスを抽出し、Webアプリケーションに渡せるようにしました。

手順

まずはアプリケーション側でProxy Protocolに対応していることを確認します。サポートしていないとエラーが出ますのでご注意ください。

1.ターゲットグループでProxy Protocol v2を有効にします。
対象NLBのターゲットグループから[属性の編集] -> [Proxy Protocol v2]の有効化にチェック

*反映まで数分かかる場合があります。

2.AWS Fargate側のコンテナアプリケーションをProxy Protocolに対応させます。
今回のアプリケーションはnginxです。

nginx.confのserverディレクティブ内を下記を追加

nginx.conf

    server {
        listen 80 proxy_protocol;
        server_name _;
        set_real_ip_from XXX.XXX.XXX.XXX;     ←適宜変更
        real_ip_header proxy_protocol;
        real_ip_recursive on;
        location / {
           proxy_pass   http://YYY.YYY.YYY.YYY;     ←適宜変更
           proxy_set_header  Host $host;
           proxy_set_header  X-Real-IP $proxy_protocol_addr;
           proxy_set_header  X-Forwarded-For $proxy_protocol_addr;
        }
  • proxy_protocol
    • 必須
    • nginxProxy Protocolヘッダーを受け付けられるように listenディレクティブに追加
  • set_real_ip_from
    • 必須
    • IPアドレス、CIDR、ホスト名を設定可能
    • 指定されたIPアドレス以外からはCLient IPアドレスの上書きは許可しません
    • NLBのプライベートIPアドレスか、NLBのあるVPCを指定。セキュリティー上0.0.0.0/0は避けましょう
  • real_ip_header
    • AWS FargateのログにもClient IPを記載したい場合なら必須
    • Client IPアドレスを上書きする際に使うリクエスヘッダー
  • real_ip_recursive
    • 任意だがセキュリティー上、あった方が望ましいです
    • set_real_ip_fromと一緒に設定
    • リクエストヘッダーにIPアドレスが複数ある場合どれを利用するかを設定
  • $proxy_protocol_addr
    • proxy_protocolを有効にするとClient IPアドレスがこの関数に格納されます

3.proxyされるアプリケーションでの設定
今回のアプリケーションはnginxです。

自分の環境に合わせてserverディレクティブに下記行を追加します。

nginx.conf

set_real_ip_from  ZZZ.ZZZ.ZZZ.ZZZ;      ← 適宜変更
real_ip_header    X-Forwarded-For;
real_ip_recursive on;
  • set_real_ip_fromはAWS Fargate(nginx)のプライベートIPアドレスを指定
  • real_ip_headerはAWS Fargate(nginx)で設定したproxy_set_headerを指定
  • real_ip_recursiveはonの場合、Client IPアドレスはreal_ip_headerで送られてきた最後のIPになります。offの場合、Client IPアドレスはset_real_ip_fromに指定されていない最後のIPになります。

結果

WebアプリケーションのnginxのログにClient IPアドレスが記録されていることを確認できました。
*グローバルIPアドレスは加工してあります。

AWS Fargate(nginx)のログ

■Webアプリケーションのログ

access.log

210.227.xxx.xxx - - [31/Mar/2020:13:55:28 +0900] "GET /player.html HTTP/1.0" 200 395 "-" "curl/7.54.0" "210.227.xxx.xxx" "210.227.xxx.xxx"
210.227.xxx.xxx - - [31/Mar/2020:13:57:17 +0900] "GET /player.html HTTP/1.0" 200 395 "-" "curl/7.54.0" "210.227.xxx.xxx" "210.227.xxx.xxx"
3.84.xxx.xxx - - [31/Mar/2020:13:57:34 +0900] "GET /player.html HTTP/1.0" 206 395 "-" "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)" "3.84.xxx.xxx" "3.84.xxx.xxx"
113.43.xxx.xxx - - [31/Mar/2020:13:58:06 +0900] "GET /player.html HTTP/1.0" 200 395 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Mobile/15E148 Safari/604.1" "113.43.xxx.xx" "113.43.xxx.xx"

追加検証

NLBインスタンス IDを使用してターゲットを指定すると、Clientの送信元 IP アドレスが保持され、Targetに渡されます。せっかくなのでこの挙動も検証したいと思います。

構成
Client(http) -> NLB(8081/tcp) -> Target(80/tcp)
*こちらの都合で検証時80ポートは既に使用済みなので8081ポートにしました。

  • Client側
    • グローバルIPアドレス: 210.227.xxx.xxx
    • プライベートIPアドレス: 192.168.2.253
  • NLB側
    • Listener: 8081
    • URL: xxxx-test-nlb-f3xxxxxx.elb.ap-northeast-1.amazonaws.com
    • グルーバルIPアドレス: 13.113.xxx.xxx
    • プライベートIPアドレス: 10.16.1.11
  • Target側
    • グローバルIPアドレス:13.231.xxx.xxx
    • プライベートIPアドレス: 10.16.1.13

テストとしてClient側でcurlコマンドを実行し、Client側とTarget側でtcpdumpを実施します。

$ curl -I http://xxxx-test-nlb-f3xxxxxx.elb.ap-northeast-1.amazonaws.com:8081/

■Target側のtcpdump
送信元IPアドレスはClient側のグローバルIPアドレスであることを確認できました。

$ sudo tcpdump -XX -p -n -i eth0 src 210.227.xxx.xxx |grep -v ssh
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode

12:14:15.614152 IP 210.227.xxx.xxx.32507 > 10.16.1.13.http: Flags [S], seq 3069243899, win 65535, options [mss 1360,nop,wscale 6,nop,nop,TS val 133874316 ecr 0,sackOK,eol], length 0
    0x0000:  06db acd0 82e8 06d4 4729 9c6c 0800 4500  ........G).l..E.
    0x0010:  0040 0000 4000 2606 8c45 d2e3 ea72 0a10  .@..@.&..E...r..
    0x0020:  010d 7efb 0050 b6f0 f1fb 0000 0000 b002  ..~..P..........
    0x0030:  ffff 7c2e 0000 0204 0550 0103 0306 0101  ..|......P......
    0x0040:  080a 07fa c28c 0000 0000 0402 0000       ..............

■Client側のtcpdump
Target側のIPアドレスではなく、NLBのグローバルIPアドレスでリクエストを返されたことを確認できました。

$ sudo tcpdump -XX -p -n -i utun1 src 13.113.xxx.xxx
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on utun1, link-type NULL (BSD loopback), capture size 262144 bytes
12:31:24.430760 IP 13.113.xxx.xxx.8081 > 192.168.2.253.61858: Flags [S.], seq 3834156092, ack 3564724080, win 26847, options [mss 1460,sackOK,TS val 1006342388 ecr 134891930,nop,wscale 7], length 0
    0x0000:  0200 0000 4508 003c 0000 4000 e906 02cf  ....E..<..@.....
    0x0010:  0d71 bdce c0a8 02fd 1f91 f1a2 e488 943c  .q.............<
    0x0020:  d479 5f70 a012 68df 73b4 0000 0204 05b4  .y_p..h.s.......
    0x0030:  0402 080a 3bfb 90f4 080a 499a 0103 0307  ....;.....I.....

結論

ELBの一種であるNLBはターゲットグループの種類によって保持する送信元IPアドレスが変わります。

種類がインスタンス IDの場合は送信元IPアドレスがClient IPアドレスになりますが種類がIPの場合はNLBのプライベートIPアドレスになります。自分が想定したIPアドレスがログなどに記録されていない時は上記をご確認下さい。

それでは皆さま、くれぐれも手洗いうがい等お忘れなく、ご自愛ください。

参考文献

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/network/load-balancer-target-groups.html#target-type
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/network/load-balancer-target-groups.html#proxy-protocol
https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/
http://nginx.org/en/docs/http/ngx_http_realip_module.html

元記事はこちら

AWS FargateだとNLB経由のClient IPアドレスは記録されない?」