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

お久しぶりです。ついに在宅勤務になった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`

![AWS-NLB_Fargate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/3507ad88-3560-eb02-421c-5600ad1c42f1.png)

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

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

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

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

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

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

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

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

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

![nlb-proxyprotocol.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/83f607bc-036b-045a-9021-487ad9003510.png)
＊反映まで数分かかる場合があります。

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
 - 必須
 - `nginx`が`Proxy 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)`のログ

![aws-fargate-log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/cee01d7a-a284-a689-2121-de6f1b75659c.png)

■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