えーっ。Nginx とか立てようよ。バッファリングとかレスポンス圧縮とか静的ファイル配信とか、欲しいだろう?
疲れるので普通に書きます。食べログ DevOps チームの @weakboson です。
この記事は Advent Calendar の3日目の投稿です。
production ではアプリケーションの手前に reverse proxy として HTTP サーバを立てるといろいろメリットがあります。そのような構成にして疎通できないときによくある原因と診断・調査方法を解説します。
疎通しないときの原因と診断手法
Case | よくある原因 | 診断 |
---|---|---|
a | Reverse proxy が port を間違えている | アプリのローカルから localhost に curl
|
b | アプリの bind が lo になっている | アプリの NIC Address に curl |
c | Reverse proxy とアプリが同一ホストで設定があべこべ | アプリのインタフェースそれぞれに curl |
d | Reverse proxy の送出する Host ヘッダと下流の期待するヘッダが合っていない | アプリ Nginx のログ確認 tcpdump で Host ヘッダを確認 |
Case a ~ c のサンプル構成は以下の通りとします。ロードバランサの下流に Web サーバがあり、その下にアプリケーションサーバ、アプリケーションは (HTTP サーバを介さず) port 3000 で直接 proxy リクエストを待ち受けているとします。
Case a: Reverse proxy が port を間違えている
そんな基本的な……と思いますが割とありがち。
アプリのローカルから localhost に curl
アプリホストのローカルから curl
で proxy している port を叩いてみて、接続を拒否されたら port 間違いの可能性があります。
$ curl -I -X GET http://localhost:3000/
curl: (7) Failed to connect to localhost port 3000: 接続を拒否されました
設定は正しいはずなのになー、と思ったらホントに意図通りの挙動をしているか確認してみましょう。netstat
か lsof
を使うのが定番です。
netstat での port 確認
netstat は出力量が多いので適宜 grep などでフィルタするとよいでしょう。port 3000 を listen しているプロセスが存在すれば Local Address
の列に :3000
が登場します。(オプションと左側の 0.0.0.0
については後述します。)
$ netstat -an
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
# 中略
tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN
# 後略
上は port 3000 を listen しているプロセスが存在したときの出力例です。何も出てこなかったら残念賞。
lsof での port 確認
lsof の -i オプションに port を指定して狙い撃ちで確認できます。何も出てこなかったら残念賞。
今回の例では port 3000 なので一般ユーザで実行できますが、1024 未満の port を調べるときには特権が必要です。
$ lsof -PRnl -i:3000
COMMAND PID PPID USER FD TYPE DEVICE SIZE/OFF NODE NAME
ruby 94442 92609 61551742 13u IPv4 330705330 0t0 TCP *:3000 (LISTEN)
Case b: アプリの bind が lo になっている
Linux には普通 NIC (Network Interface Card) と lo (local loopback device) の2つ (以上) のネットワークインタフェースがあります。Linux プロセスは同時に複数のインタフェースに bind できますが、意図して限定することもできます。充分に準備ができてないアプリをインターネットに公開して exploit されないようにという親心だと思うのですが、近年は development やデフォルトでは lo にのみ bind するサービス・フレームワークが多いです。Rails もこの例に漏れず development で何もオプションを付けず rails s
で起動すると lo bind になります。この状態では Rails が動作しているホストの外からアクセスできません。
参考: developmentでWEBrick起動のRailsはbindオプションでanyアクセスを受け付けるようになる
アプリの NIC Address に curl
NIC の Address を curl で叩いてみます。
$ curl -I -X GET http://`hostname -i`:3000
curl: (7) Failed to connect to 10.249.0.39 port 3000: 接続を拒否されました
$ curl -I -X GET http://localhost:3000
HTTP/1.1 200 OK
Last-Modified: Thu, 01 Nov 2018 08:11:07 GMT
Content-Type: text/html
Content-Length: 0
Server: WEBrick/1.3.1 (Ruby/2.4.2/2017-09-14)
Date: Thu, 29 Nov 2018 03:34:59 GMT
Connection: Keep-Alive
このように localhost なら通るのに NIC Address では拒否されるとき、アプリはたぶん lo に bind しています。
bind 状況も netstat か lsof で確認できます。
netstat での bind 確認
netstat では Local Address
列コロンの左側が bind Address です。
以下の例では 127.0.0.1
に bind しています。この状態だとリモートから接続できません。lo も NIC も受け付けるときはどのように表示されるかというと…… 0.0.0.0
になります。
$ netstat -an
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
# 中略
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN
# 後略
lsof での bind 確認
lsof ではNAME
列コロンの左側が bind Address です。
$ lsof -PRnl -i:3000
COMMAND PID PPID USER FD TYPE DEVICE SIZE/OFF NODE NAME
ruby 75218 92609 61551742 13u IPv4 330674262 0t0 TCP 127.0.0.1:3000 (LISTEN)
lsof では lo も NIC も ok というときは *
が表示されます。
$ lsof -PRnl -i:3000
COMMAND PID PPID USER FD TYPE DEVICE SIZE/OFF NODE NAME
ruby 94442 92609 61551742 13u IPv4 330705330 0t0 TCP *:3000 (LISTEN)
オマケ: WEBRick で全インタフェースに bind させるオプション
WEBRick
を開発以外で使うことはないと思いますが -b 0.0.0.0
で全インタフェースに bind して localhost からもリモートからもアクセスできるようになります。
$ bundle exec rails s -b 0.0.0.0
Case c: Reverse proxy とアプリが同一ホストで設定があべこべ
アプリと同一ホストに reverse proxy を配置するときは UNIX domain socket を採用することが多いので production でこのケースはあまりないでしょう。私がハマったのは production に近い構成を開発環境の同一ホストで再現したときです。同一ホストでは reverse proxy とアプリケーションの双方が lo と NIC を指定できるためあべこべにできます。
……いや「できます」って動かないからやっちゃダメです。
netstat, lsof でアプリケーション確認すると、NIC だけに bind していて lo アクセスできないプロセスは以下のように見えます。
$ netstat -an
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:445 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:57853 0.0.0.0:* LISTEN
tcp 0 0 10.249.0.39:3000 0.0.0.0:* LISTEN
# 後略
$ lsof -PRnl -i:3000
COMMAND PID PPID USER FD TYPE DEVICE SIZE/OFF NODE NAME
ruby 101477 92609 61551742 13u IPv4 330716822 0t0 TCP 10.249.0.39:3000 (LISTEN)
このときアプリサーバのローカルから curl で localhost に接続確認すると port が合っていてもアクセスできません。hostname では接続できる。わかりづれぇ!
$ curl -I -X GET http://localhost:3000
curl: (7) Failed to connect to localhost port 3000: 接続を拒否されました
$ curl -I -X GET http://`hostname`:3000
HTTP/1.1 200 OK
Last-Modified: Thu, 01 Nov 2018 08:11:07 GMT
Content-Type: text/html
Content-Length: 0
Server: WEBrick/1.3.1 (Ruby/2.4.2/2017-09-14)
Date: Wed, 28 Nov 2018 11:10:31 GMT
Connection: Keep-Alive
まとめると以下の通りです。
状況 | bind |
---|---|
localhost に接続できるがホストの外からアクセスできない | lo (127.0.0.1) のみ |
hostname で接続できるが localhost で接続できない | NIC のアドレスのみ |
ホスト内外からどのように指定しても ok | any (0.0.0.0) |
Nginx では upstream
にアプリサーバを記述します。ここで server
をなんと書くかで運命が分かれます。
http {
upstreap app_server {
server 何か:3000;
}
}
server
と Rails 起動コマンド、疎通の組み合わせは以下の通りです。
Nginx upstream の server
|
Rails Server 起動コマンド | 疎通 |
---|---|---|
localhost:3000 | rails s -b 127.0.0.1 | OK |
ホスト名:3000 | rails s -b 127.0.0.1 | NG |
localhost:3000 | rails s -b `hostname` | NG |
ホスト名:3000 | rails s -b `hostname` | OK |
localhost:3000 | rails s -b 0.0.0.0 | OK |
ホスト名:3000 | rails s -b 0.0.0.0 | OK |
アプリを any でも lo でもなく NIC に bind するのはわかりづらいのでよほどの事情がない限りおすすめしません。
Case d: Reverse proxy の送出する Host ヘッダと下流の期待するヘッダが合っていない
さあ、後半戦ややこしくなってまいりました。Case d ではサンプルを変えて以下の構成で解説します。
アプリケーションサーバは HTTP サーバが port 80 を受け付けて UNIX domain socket でアプリに proxy しているとします。多段 reverse proxy 構成です。
Case d の肝は HTTP サーバのバーチャルサーバ設定です。ここまでは具体的な設定を省いて診断方法だけ解説してきましたが、これは設定例を先に挙げた方がわかりやすそうです。
多段 reverse proxy の設定例
これは運用プラクティスなのですが production で監視をするとき HTTP サーバプロセス死活監視のエンドポイントとアプリケーションサービスは異なるバーチャルサーバにするとよいかもしれません。
Nginx は下層ブロックで設定継承する仕様に癖があり、重要設定は http か server コンテキストに記述してしまい location では触らないのが一つの方針かなと考えています。これについては 12/5 の Advent Calendar で触れます。
http {
upstream app_server {
server 10.249.0.39;
server 10.249.0.40;
# ...
}
server {
server_name example.com;
# サービスのためのバーチャルサーバ
# 公開用アクセス制御
include acl_for_public.conf;
location / {
proxy_pass http://app_server;
}
}
server {
server_name "" default;
# ヘルスチェック・モニタリングのためのエンドポイント
# 社内限定アクセス制御
include acl_for_internal.conf;
location = /healthcheck {
# ...
}
}
}
http {
upstream rails {
server unix:/app/current/tmp/sockets/rails.sock;
}
server {
server_name app_server;
location / {
proxy_pass http://rails;
}
}
server {
server_name "" default;
location = /healthcheck {
# ...
}
}
}
curl でシミュレートすると各リクエストとバーチャルサーバの組み合わせは以下の通りです。
リクエスト | curl コマンド | バーチャルサーバ |
---|---|---|
Web のサービス | curl -H 'Host:example.com' http://10.255.0.40 |
example.com |
Web Nginx のヘルスチェック | curl http://10.255.0.40/haealthcheck |
default |
アプリへの reverse proxy | curl -H 'Host:app_server' http://10.255.3.42 |
app_server |
アプリ Nginx のヘルスチェック | curl http://10.255.3.42/healthcheck |
default |
ここまで来るとハマりそうなところがおわかりでしょうか?
Q: Host: app_server
って一体誰だよ!?
A: Web サーバ Nginx の upstream
名だよ
upstream app_server { # これこれこれ
# ...
}
この例の場合アプリに proxy するバーチャルサーバの定義を
server {
server_name example.com;
}
のようにすると NG です。このケースでは間違えたリクエストはデフォルトバーチャルサーバが受け付けるので接続拒否ではなくおそらく Not Found や Forbidden が返ります。
アプリ Nginx のログ確認
アプリの HTTP サーバまで到達してエラーレスポンスが返ってきているなら、HTTP サーバのエラーログを見るのが手っ取り早いです。Nginx では以下のように出力されます。
2018/11/28 23:05:56 [error] 56687#56687: *8 open() "/usr/local/openresty/nginx/html/hoge" failed (2: No such file or directory), client: 10.249.0.39, server: , request: "GET /hoge HTTP/1.1", host: "app_server"
server
が振られたバーチャルサーバ、host
が Host
ヘッダで普通は一致します。この例は Host: app_server
というリクエストが来て、マッチするバーチャルサーバがないのでデフォルトに回されたことを意味します。
tcpdump で Host ヘッダを確認
ところが Not Found エラーはログ出力しないのが定番だったりもします……疎通確認フェーズなんだし一時的に設定変更して reload すればよいのですが、サクッと今すぐ見たいのだ!というときは tcpdump
をアスキーモードで実行して Host
ヘッダを確認するのが手です。他にもいろいろわかるし、私もこういうときはログ見るより tcpdump することが多いです。
sudo tcpdump -A port 80
すでに死活監視が入っている、自分以外にもアクセスしているという状況だとものすごい出力があるので grep などで絞り込むとよいでしょう。
sudo tcpdump -A port 80 | grep Host
この状態で Web サーバかロードバランサにリクエストを投げると Host
ヘッダが出力されるはずです。
Host:app_server
Host:app_server
...
以上です。
自分で振り返ると、思い込みせずに淡々と調べたらあまり時間を取られず解決できただろうになあと思います。
明日、4日目は @zettaittenani による「Ansible controller/target を Docker コンテナで構築する」です。
オマケ: コマンド実行例とオプションの解説
オマケで業務でよく使うオプションを解説します。
curl
オプション | 解説・意図 |
---|---|
-I | レスポンスヘッダのみ出力。ヘッダが見たいときのほか、ボディをだらっと流すのがいやなとき |
-X | HTTP メソッドを指定。-I オプションを使うとデフォルトで HEAD になります。アプリが HEAD を受け付けない、挙動が変わるということもあるので |
-k | TLS 証明書エラーを無視します。開発者のミカタ、オレオレ証明のお供 |
-H | HTTP ヘッダを追加します。バーチャルサーバが2種以上の HTTP サーバに直接リクエストを投げて検証するとき Host ヘッダを付与するのが有用 |
netstat
オプション | 解説・意図 |
---|---|
-a | すべての State を出力します。デフォルトは connected (ESTABLISHED) のみで、接続待ち、つまりリクエストを処理してないプロセスは出力されません |
--numeric-ports |
don't resolve port names port をプロトコルに変換しません。アプリが適当な port を使っていると使ってるつもりのないプロトコル名が出てきてびっくりする。というか port で grep できなくて困る |
-n |
don't resolve names host, port, user どれも変換しないで数値のまま出力。127.0.0.1 とか解決してくれなくていい、横に長い方がいやだという人におすすめ |
lsof
オプション | 解説・意図 |
---|---|
-i | port を指定します。ナニワトモアレこれがなくては |
-P | port をプロトコルに変換しません。netstat の--numeric-ports と同じ |
-n | host を解決しません。こういう記事でホスト名をボカしたいとき ※ |
-l | user を解決しません。シャイな人向け ※ |
-R | 親プロセスIDをppid として表示します。master/worker プロセスで処理するミドルウェアを使っていて、かつ master を停止する適切なコマンドを用意していないとき、kill 一発で鏖殺できる親プロセスIDが欲しいことがあります |
※ もちろん適宜外した方が使いやすいでしょう
tcpdump
オプション | 解説・意図 |
---|---|
-A | アスキーで出力。Web アプリの検証では HTTP ヘッダを見たいことが多い |
dst port |
dst port xxxx でmデスティネーション (宛先) port xxxx に限定。絞らないとあらゆる tcp 通信が出力されて大変なんです、port を間違えている可能性もあるので付け外しして確認 |
src host |
src host xxxx でソース (送り元) ホストを xxxx に限定。調査している自分自身以外も頻繁にアクセスしてる環境で監視する通信を絞り込めそう……と思うかもしれませんが、TCP/IP のソースフィールドは reverse proxy であってリモートホストではないので reverse proxy 疎通調査ではそんなに有用ではありません。直接リクエストが来てるときは有用よ |
-i |
-i lo でインタフェース lo を監視。指定しないとデフォルトになります。ローカルで TCP/IP reverse proxy しているとき通信は lo が用いられますが、lo はデフォルトにならないのでどんな環境でも指定が必須です |
hostname
オプション | 解説・意図 |
---|---|
-i | IP アドレスを出力します。この記事みたいにコマンド実行例でホスト名を出したくないとき有効です |