AWSのECSでフロントにNext.jsとNginx(リバースプロキシ)を構築し、プライベートALB経由でバックエンドのAPIと通信するようなシステムを作成しました。
初めのうちはNginxとプライベートALBが上手く通信でき、ログイン処理が行えていたのですが、数日経ってログインができないという報告が上がりました。
ログを調べてみると、NginxとプライベートALBの通信間で502エラーで出ていました。
default.conf
の設定は次のようなイメージです。
# /etc/nginx/conf.d/default.conf
server {
# NGINXの受付ポート
listen 4000;
server_name nginx;
# 通常の通信はhttp://localhost:3000(Next.js)に飛ばす
location / {
# localhost は 127.0.0.1 (IPv4), ::1 (IPv6) どちらともに名前解決されるので、明示的に127.0.0.1を設定する
proxy_pass http://127.0.0.1:3000;
}
# passに/user/loginが含まれる場合バックエンドのAPIと通信を行う
location /user/login {
proxy_pass "https://api.dev.jp/user/login";
}
}
原因
Nginxはstartまたはreload時にドメインの名前解決を行い、そのIPをTTLに関係なく次のrestart、 reload時まで保持する挙動となっているそうです。(参考)
つまり、一度NginxをECSにデプロイすると最初に名前解決されたIPアドレスが保持され続けます。
次に、AWSのALBはトラフィック量に応じて自動的にスケールされ
、ALBが自動的にスケールアップするのに伴い、ロードバランサーのIPアドレスは動的に変わる
という仕様があります。(マニュアル、AWS-Black-Belt(8ページ))
このことから、プライベートALBが自動でスケールアップされると、NginxにキャッシュされたプライベートALBのIPアドレスが現在の値と異なってしまうため、502エラーが発生していた訳です。
やってみたこと(失敗)
Nginxのdefault.confにresolver
の設定を追加します。
# TTLが5秒を超えるとAmazonDNSサーバーに名前解決しにいく設定
resolver 169.254.169.253 valid=3s;
server {
・・・・・・・・・・・・
}
こうすることで、3秒間プライベートALBと通信できなかったら、AmazonDNSサーバーに再度名前解決を行い、最新のIPアドレスを取得してくれるようになる想定でした。
AmazonDNSサーバーのIPアドレスについては下記マニュアルを参照。
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-dns.html
結論(解決策)
上記の設定だけではALBのアドレスが変更されても、Nginx側のIPが変更されなかった。
どうやらresolver
を設定しただけでは名前解決してくれないらしく、名前解決するにはset
パラメータを利用してドメインを変数化しないといけないとのこと。
加えて、リクエストパラメータ(https://api.dev.jp/user/login?name='taro'の
?name='taro'部分)がプロキシ対象のリクエストのパスに含まれる場合、
set`パラメータを使用するとリクエストパラメータが消えてしまう仕様もあるので、修正内容は次のようになった。
# resolverで指定するDNSのIPは、VPCのネットワークに+2した値
resolver 169.254.169.253 valid=3s;
# /etc/nginx/conf.d/default.conf
server {
# NGINXの受付ポート
listen 4000;
server_name nginx;
# 通常の通信はhttp://localhost:3000(Next.js)に飛ばす
location / {
# localhost は 127.0.0.1 (IPv4), ::1 (IPv6) どちらともに名前解決されるので、明示的に127.0.0.1を設定する
proxy_pass http://127.0.0.1:3000;
}
# ドメインを変数backend_serverに入れる
set $backend_server api.dev.jp # 追加
# passに/user/loginが含まれる場合バックエンドのAPIと通信を行う
location /user/login {
# ${backend_server}で接続先URLを渡す。さらに${is_args}${args}でリクエストパラメータを渡す;
proxy_pass https://${backend_server}/user/login${is_args}${args}; # 修正
}
}
変数$is_args
はリクエスト行に引数がある場合は?となり、引数なしの時は空文字列になります。
変数$args
はurlに書かれたgetパラメータが保存される変数です。
この修正内容でECSにデプロイしたところ問題なく動作するようになりました。
※ resolver 169.254.169.253 は AWS内部からのみアクセス可能なようです。