お久しぶりです。ついに在宅勤務になった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となります。
#対処法
NLB
のProxy 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ディレクティブ内を下記を追加
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ディレクティブに下記行を追加します。
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アプリケーションのログ
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