nginxでロードバランシング＆リバースプロキシの設定

いまいち理解しきれていなかったnginxでのロードバランシング＆リバースプロキシ設定を、テスト用環境を作りながら整理しました。
参考：
https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/
http://nginx.org/en/docs/http/load_balancing.html

作るもの＆条件
-
・Dockerコンテナ上に構築
・アプリケーションコンテナ内のCakePHPビルドインWEBサーバーを動かす
・クライアントからのリクエストはnginxで構築したリバースプロキシコンテナでアプリコンテナへプロキシする
・今のところロードバランシングするアプリケーションコンテナは1つしか無いため、あまり意味ないけど1つのみ定義（でもやりたかった）

ロードバランシングの設定
-
###upstreamディレクティブでサーバーグループを定義する
設定ファイル（/etc/nginx/nginx.confなど）に下記のように定義する。
公式によると定義する場所はhttpコンテキストとのこと。

```nginx
http {
    upstream test {
        server app:８７６５;
    }
}
```
バーチャルサーバー用の設定ファイル置き場はetc/nginx/conf.d/*になるが、ここに配置する設定ファイルは基本的にhttpコンテキスト内と考えて良い様子。
（httpコンテキストの記載をあえてしなくても正常動作しているのを確認、またserverディレクティブはhttp内でないと動作しない）
元締めのnginx.confにはデフォルトでhttpコンテキストが存在している為upstreamディレクティブはそこに記載すると良さそうだが、バーチャルサーバーの場合はあえてhttpディレクティブを用意すると下記のようなエラーになった。

```
2021/01/25 10:45:49 [emerg] 1#1: "http" directive is not allowed here in /etc/nginx/conf.d/test.conf:12
nginx: [emerg] "http" directive is not allowed here in /etc/nginx/conf.d/test.conf:12
```

サーバーグループ内のWEBサーバー（今回の場合アプリコンテナ）が複数ある場合、また2つ以上のサーバーグループを定義したい場合、それらのサーバー（コンテナ）全てが起動していないとエラーになる模様。

これとか

```nginx
http {
    upstream test {
        server app1:８７６５;
        server app2:８７６５;
    }
}
```
こういう場合

```nginx
http {
    upstream test1 {
        server app1:８７６５;
    }
    upstream test2 {
        server app2:８７６５;
    }
}
```
リバースプロキシコンテナが起動する前にエラーになってしまう為、記載したサーバーが全て起動できる前提での構築や動作確認が必要な様子。
（今回は複数定義が不要なので一方を削除しこれ以上の確認はしてません）

```
2021/01/26 02:49:05 [emerg] 1#1: host not found in upstream "app1:８７６５" in /etc/nginx/conf.d/test.conf:2
nginx: [emerg] host not found in upstream "app1:８７６５" in /etc/nginx/conf.d/test.conf:2
```


ロードバランスのアルゴリズムもここで指定する。
指定しない場合、デフォルトではラウンドロビンになる。

ラウンドロビン：グループのサーバーに順番にリクエストが割り当てられる仕様

尚、下記のように記載するとbackend1.example.comにリクエストが5回割り当てられた後、backend2.example.comの順番になるらしい。公式より。

```nginx
http {
    upstream backend {
        server backend1.example.com weight=5;
        server backend2.example.com;
        server 192.0.0.1 backup;
    }
}
```

ラウンドロビンの他、アクティブな処理数が最も少ないサーバーに割り当てたり、クライアントのIPアドレスによって割り当てるサーバーを決定するアルゴリズムもある。
有料版のnginx plusだと、リクエストとレスポンスの内容から割り当てるサーバーを学習しながら決めるアルゴリズムもあるとか。

リバースプロキシの設定
-

upstreamディレクティブで指定したサーバーに対し、serverディレクティブでまとめてプロトコルを渡せる。

```nginx
upstream test {
    server app:８７６５;
}

server {
    location / {
        proxy_pass http://test;
    }
}
```

もしupstreamディレクティブに定義されていないサーバーに何かプロトコルを渡そうとした場合、upstreamディレクティブ内だけでなく、nginxコンテナのhostsに記載されているホスト名も含めてnginxが参照しようとしてくれるらしい。
その場合にhostsにも記載がないと下記のエラー。

```
2021/01/26 02:50:32 [emerg] 1#1: invalid host in upstream "app:８７６５" in /etc/nginx/conf.d/test.conf:15
nginx: [emerg] invalid host in upstream "app:８７６５" in /etc/nginx/conf.d/test.conf:15
```

composeファイルでnetworks又はlinksオプションを使用するとhostsに記載できそうなのでそちらを利用するか、IPアドレス指定に書き直すと良さそう。（試してない）

###serverディレクティブに記載するもの

リバースプロキシの設定でなくても記載するものを追記。こんな感じ。

```nginx
    listen 80 default_server;
    server_name test.jp;

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
```

###プロキシに渡すもの

locationディレクティブに、プロキシに渡すものを色々追記。

```nginx
　　 #@appはこのロケーションに名前をつけるもの
    location @app {
        #リクエストヘッダのポート番号を除いたHTTP_HOSTの値(local.test.jp)
        proxy_set_header Host $host;
        #クライアントのプロトコル (開始スキーマ、HTTPまたはHTTPS) 特定のためのヘッダー
        proxy_set_header X-Forwarded-Proto $scheme;
        #接続を受け入れたサーバー（リバプロコンテナ）のポート（80）
        proxy_set_header X-Forwarded-Port $server_port;
        #プロキシを渡す先（upstreamで定義したサーバーグループ）
        proxy_pass http://test;
        proxy_http_version 1.1;
    }
```


色々寄り道しながらも、ここまでで下記のようなconfファイルを作成。

```nginx
upstream test {
    server app:8765;
}

server {
    listen 80 default_server;
    server_name test.jp;

    location / {
        try_files $uri @app;
    }

    location @app {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://test;
        proxy_http_version 1.1;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}
```



まさかのPHP実行不可
-
ここで一旦動作確認したところ、phpファイルが404エラーになる。
試しにアプリコンテナにhtmlファイルを配置するとこちらは表示可。
アプリコンテナのビルドインWEBサーバーがphpを実行できていないと思われる。
フレームワークのビルドインなのにそうなるとは。

###対処方法

ビルドインサーバーの動作なのでfastcgiは関係ないけど、php-fpmのconfファイルを見ていたらここでひらめきが。

/usr/local/etc/php-fpm.d/www.conf

```php
; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect.
; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
; must be separated by a comma. If this value is left blank, connections will be
; accepted from any ip address.
; Default Value: any
;listen.allowed_clients = 127.0.0.1
```
ビルドインってlocalhostで実行されてなかったっけ？

```sh
Welcome to CakePHP v3.9.5 Console
---------------------------------------------------------------
App : src
Path: /app/src/
DocumentRoot: /app/webroot
Ini Path:
---------------------------------------------------------------
built-in server is running in http://localhost:8765/
You can exit with `CTRL-C`
[Wed Jan 27 02:50:43 2021] PHP 7.4.13 Development Server (http://localhost:8765) started
```

パスがこれじゃリバースプロキシも上手く処理できないよね。
コンテナ起動時に渡すコマンドに下記を追記。

```sh
php bin/cake.php server -H 0.0.0.0
```

動作確認。

```sh
Welcome to CakePHP v3.9.5 Console
---------------------------------------------------------------
App : src
Path: /app/src/
DocumentRoot: /app/webroot
Ini Path:
---------------------------------------------------------------
built-in server is running in http://0.0.0.0:8765/
You can exit with `CTRL-C`
[Wed Jan 27 02:56:04 2021] PHP 7.4.13 Development Server (http://0.0.0.0:8765) started
```
これでPHPが実行できるようになった！
ここまでで下記の設定が完了

ロードバランサーでの負荷分散（分散してないけど）
↓
リバースプロキシがリクエストを処理
↓
アプリコンテナでPHP実行
↓
ブラウザでアプリ稼働確認（レスポンスが返ってくる）

リダイレクト時のURLにポート番号が含まれない
-

完成！と思いきや、リダイレクト時に想定外の挙動をしているのを発見。
今回リクエストするurlにポート番号を含めるような想定をしていたが、リダイレクトの際に返ってくるurlにポート番号の記載がなくなる事で、こちらも404エラーになってしまう。

###対処方法

nginxの公式モジュールや変数一覧を元に対応
http://nginx.org/en/docs/http/ngx_http_proxy_module.html
http://nginx.org/en/docs/varindex.html

これを追記

```
proxy_redirect http://$host/  http://$http_host/;
```

**http://$host/とは**
server_nameで指定した名前

**http://$http_host/とは**
公式に下記のような記載があった
>An unchanged “Host” request header field can be passed like this:
>proxy_set_header Host       $http_host;

なので最初からproxy_set_header Hostに渡す変数をこのように記載すれば追記必要ないかも（試してない）
レスポンスにポート番号を含める事自体テスト用の設定なので、今回はproxy_redirect追記で作業を終わらせることにした。

まとめ
-
nginxは出来ること多くて便利だけど、慣れないときつい。特にコンテナで動かす場合や、fastcgiとの連携部分。
あとはドキュメントが翻訳より英語のまま読んだ方がなぜか分かりやすい仕上がりになっている。時間かけてちゃんと読みたいなぁ。