ハァイ、ジョージィ！reverse proxy立ててくれないのかい？

えーっ。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 のログ確認<br/>tcpdump で Host ヘッダを確認 |

Case a ~ c のサンプル構成は以下の通りとします。ロードバランサの下流に Web サーバがあり、その下にアプリケーションサーバ、アプリケーションは (HTTP サーバを介さず) port 3000 で直接 proxy リクエストを待ち受けているとします。

![case-abc.png](https://qiita-image-store.s3.amazonaws.com/0/14326/53ef37dd-03f0-4af3-474a-d4f3703b03c8.png)

# Case a: Reverse proxy が port を間違えている

そんな基本的な……と思いますが割とありがち。

## アプリのローカルから localhost に curl

アプリホストのローカルから `curl` で proxy している port を叩いてみて、接続を拒否されたら port 間違いの可能性があります。

```shell
$ 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` については後述します。)

```shell
$ 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 を調べるときには特権が必要です。

```shell
$ 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アクセスを受け付けるようになる](https://qiita.com/weakboson/items/9ef390679347460d6d77)

## アプリの NIC Address に curl

NIC の Address を curl で叩いてみます。

```shell
$ 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` になります。

```shell
$ 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 です。

```shell
$ 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 というときは `*` が表示されます。

```shell
$ 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 からもリモートからもアクセスできるようになります。 

```shell
$ 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 アクセスできないプロセスは以下のように見えます。

```shell
$ 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
# 後略
```

```shell
$ 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** では接続できる。わかりづれぇ！

```shell
$ 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` をなんと書くかで運命が分かれます。

```conf
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 ではサンプルを変えて以下の構成で解説します。

![case-d.png](https://qiita-image-store.s3.amazonaws.com/0/14326/ca6dcb3b-23d2-82b1-79e3-e733d5669823.png)


アプリケーションサーバは HTTP サーバが port 80 を受け付けて UNIX domain socket でアプリに proxy しているとします。多段 reverse proxy 構成です。

Case d の肝は HTTP サーバのバーチャルサーバ設定です。ここまでは具体的な設定を省いて診断方法だけ解説してきましたが、これは設定例を先に挙げた方がわかりやすそうです。

## 多段 reverse proxy の設定例

これは運用プラクティスなのですが production で監視をするとき HTTP サーバプロセス死活監視のエンドポイントとアプリケーションサービスは異なるバーチャルサーバにするとよいかもしれません。

Nginx は下層ブロックで設定継承する仕様に癖があり、重要設定は http か server コンテキストに記述してしまい location では触らないのが一つの方針かなと考えています。これについては 12/5 の Advent Calendar で触れます。

```nginx:Webのnginx.conf
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 {
            # ...
        }
    }
}
```

```nginx:アプリのnginx.conf
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` 名だよ

```nginx:Webのnginx.conf
upstream app_server { # これこれこれ
    # ...
}
```

この例の場合アプリに proxy するバーチャルサーバの定義を

```nginx:アプリのnginx.conf
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 することが多いです。

```shell
sudo tcpdump -A port 80
```

すでに死活監視が入っている、自分以外にもアクセスしているという状況だとものすごい出力があるので grep などで絞り込むとよいでしょう。

```shell
sudo tcpdump -A port 80 | grep Host
```

この状態で Web サーバかロードバランサにリクエストを投げると `Host` ヘッダが出力されるはずです。

```shell
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 アドレスを出力します。この記事みたいにコマンド実行例でホスト名を出したくないとき有効です |