実践編ーDockerを使ってnginxでリバースプロキシを立ててみる

# はじめに
なんとなくわかりにくいリバースプロキシの理解のため、ローカルでリバースプロキシを立ててみる記事です。

**この記事の範囲：Dockerでnginxのリバースプロキシを立て、ブラウザからWebサーバにリバースプロキシを介してリクエストを送りレスポンスを確認するところまで**

[プロキシとリバースプロキシの違いまとめ](https://qiita.com/zawawahoge/items/a931de1464ccaa228551)も書いているのでご覧ください。

# 実際に（ローカルで）リバースプロキシを立ててみよう
リバースプロキシは広い概念で、HTTPリバースプロキシやTCPリバースプロキシなどがあります。

ここでは、HTTPリバースプロキシの一つである**nginx**をローカルで立ててみましょう。

## nginxって何
**nginx**とは、Webサーバからリバースプロキシまでなんでもこなせる優れた**サーバサイドソフトウェア**です。

一般に、Webサーバは専用のアプリケーション(Apacheやnginx)を使い、特定のポートでListenして、クライアントとTCPコネクションを結びます。クライアントからHTTPリクエストが送られてきたら、なんらかの処理をした後、送信元にレスポンス(例えばindex.htmlなど)を返します。

nginxは、Webサーバのアプリケーションでもあるので、今回はこの用途でも使ってみましょう。

今回の構成は、ある一つのWebサイトの中に犬用コンテンツと猫用コンテンツの二つがあり、URLのパスでそれぞれにアクセスできるものを考えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260547/a492a4ac-fe6b-78bb-123e-10572ff42066.png)


HTTPリバースプロキシでは、クライアントから受け取ったリクエストのHTTPのパスで異なるサーバに割り振ることができます。
クライアントからはHTTPリバースプロキシサーバとしてのnginxに `/dog` もしくは `/cat` のパスで `localhost:80`(もしくは単に `localhost`)にアクセスします。
すると、リバースプロキシではパスを解析して、パスごとにあらかじめ設定したWebサーバにルーティングを行います。
リバースプロキシは、Webサーバからレスポンスが返ってくると、そのレスポンスをまるで自分が処理したかのようにクライアントに返します。

## 目標

- `localhost/cat` にリクエストを送ると、**猫好きのためのページ**を表示
- `localhost/dog` にリクエストを送ると、**犬好きのためのページ**を表示
- 猫用、犬用サーバは別に分ける
- クライアントは、それらのサーバの存在やポート番号を知らないでよい

## ディレクトリ構成
```
.
├── docker-compose.yml
├── cat-server
│   └── index.html
├── dog-server
│   └── index.html
└── reverse-proxy
    └── nginx.conf
```

ソースコードは[GitHub(zawawahoge/reverse-proxy)](https://github.com/zawawahoge/reverse-proxy)にアップロードしています。cloneして `docker-compose up` したらローカルで実際にこの構成を立てることができます。

## 実装詳細

### docker-compose

```yaml:docker-compose.yml
version: '3'

services:
  dog-server:
    image: nginx
    container_name: 'dog-container'
    volumes:
      - ./dog-server:/usr/share/nginx/html
    ports:
      - 7000:80
  
  cat-server:
    image: nginx
    container_name: 'cat-container'
    volumes:
      - ./cat-server:/usr/share/nginx/html
    ports:
      - 7001:80

  reverse-proxy:
    image: nginx
    volumes:
      - ./reverse-proxy/nginx.conf:/etc/nginx/nginx.conf
    ports:
      - 80:80
```

これを含むディレクトリで `docker-compose up` を実行することで、二つのWebサーバと一つのリバースプロキシが起動します。
各コンテナに対し、必要なファイルをvolumeとしてマウントしています。これらのファイルの中身についてもみてみます。

### インデックスページ（猫用、犬用）
犬用と猫用サーバのそれぞれについて、インデックスページを作っておきましょう。

```html:dog-server/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>犬好きのためのページ</title>
  </head>
  <body>
    <h1>犬好きのためのページ</h1>
  </body>
</html>
```

```html:cat-server/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>猫好きのためのページ</title>
  </head>
  <body>
    <h1>猫好きのためのページ</h1>
  </body>
</html>
```

非常にシンプルなものですが、これで十分でしょう。

### リバースプロキシ用の設定ファイル
続いて、nginxリバースプロキシ用の設定ファイルを書く必要があります。

```conf:reverse-proxy/nginx.conf
events {
    worker_connections  16;
}
http {
    server {
        listen 80;
        server_name localhost;
        location /dog {
            proxy_pass http://host.docker.internal:7000/;
            proxy_redirect off;
        }
        location /cat {
            proxy_pass http://host.docker.internal:7001/;
            proxy_redirect off;
        }
    }
}
```

### nginxの設定ファイルの書き方
#### events
nginxの設定ファイルは、**コンテクスト(context)**と呼ばれる `{}` で囲まれたブロックで構成されています。
最初のコンテクストである `events` については[公式（英語）](http://nginx.org/en/docs/ngx_core_module.html#events)で説明されています。

> Syntax:	events { ... }
Default:	—
Context:	main
Provides the configuration file context in which the directives that affect connection processing are specified.

コネクション全体に関する設定を意味するコンテクストです。 
`worker_connections` は、同時に接続できるワーカープロセスの数を表します(デフォルト1)。

#### http
`http` コンテクストには、HTTPサーバについての設定を書きます。
> Provides the configuration file context in which the HTTP server directives are specified.

#### server
`server` コンテクストは、仮想サーバのための設定をします。 
[公式ドキュメント](http://nginx.org/en/docs/http/ngx_http_core_module.html#server)に詳しいドキュメントが書かれています。

> Syntax:	server { ... }
Default:	—
Context:	http
Sets configuration for a virtual server. （略）

下のサイトが参考になります。
[nginx連載4回目: nginxの設定、その2 - バーチャルサーバの設定](https://heartbeats.jp/hbblog/2012/04/nginx04.html) 

#### HTTPリクエストを受け取ってからの流れ
HTTPリクエストが来ると、 `http` コンテクストにある `server` コンテクストで当てはまるものを探していきます。 `server` コンテクストには、 `listen` と `server_name` の二つが記述され、それらが条件となります。

1. まずは、 `listen` で指定されたIPアドレス（またはホスト）とポート番号がリクエストのものと一致するかどうか調べます。  
2. 当てはまったら、 `server_name` がHTTPヘッダの `Host` と同じであれば、その `server` コンテクストに書かれた処理をすることになります。

今回利用するのは次のような `server` コンテキストです。

```conf:
    server {
        listen 80;
        server_name localhost;
        # 処理...
    }
```

意味としては、 `listen` でポート80にきたリクエストかどうか判定し、 `server_name` で リクエストヘッダの `Host` が `localhost` と一致しているかどうかを判定します。どちらも当てはまるようなら続く処理が実行されるというものです。

今回の場合、リバースプロキシとして機能させるため、HTTPのURL内のパスを解析し、別のWebサーバにルーティングする処理をすることになります。

```conf:serverコンテクストの処理
        location /dog {
            proxy_pass http://host.docker.internal:7000/;
            proxy_redirect off;
        }
        location /cat {
            proxy_pass http://host.docker.internal:7001/;
            proxy_redirect off;
        }
```

続いて、パスが当てはまる `location` コンテクストが探され、 `/dog` というパスであれば、 `proxy_pass` に書かれたURIにルーティングされます。

パスがさらに長い場合、例えば `localhost/dog/list` であれば、 `http://host.docker.internal:7000/list` にルーティングされることになります。

#### proxy_passの注意点
ややこしいことですが、 `proxy_pass` はURIの場合とそうでない場合でルーティングのパスが変わるので注意してください。
[nginxのproxy_passの注意点](https://www.xmisao.com/2014/05/09/nginx-proxy-pass.html)

#### Dockerコンテナからみたホストのポート
`http://host.docker.internal` はDockerのコンテナから見たホストを指していて、コンテナ内からホストのポート `7000` にアクセスする場合にはこのように書くと名前解決してくれて**ホストそのもののポート**にアクセスできます。

このような書き方が必要になるのは、Dockerコンテナ内で `localhost` と記述した時は、Dockerを起動しているホストではなく**コンテナ自身**を指す仕様になっているからです。もちろん、Dockerを使わずにnginxを立ち上げた場合は `localhost` に置き換えて構いません。



要するに、 `localhost/dog` に来た場合は、Dockerを起動しているホストのポート `7000` にHTTPリクエストを送信する仕組みになっています（ `/cat` の場合も同様に `7001` ）。

# 結果
`docker-compose up` を実行し、curlコマンドに `localhost/dog` でリクエストを送り、レスポンスを見てみましょう。

```
curl --verbose localhost/dog
```

```shell:HTTPリクエストヘッダ
> GET /dog HTTP/1.1
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
```

```shell:HTTPレスポンスヘッダ
< HTTP/1.1 200 OK
< Server: nginx/1.17.0
< Date: Mon, 01 Jul 2019 13:38:14 GMT
< Content-Type: text/html
< Content-Length: 264
< Connection: keep-alive
< Last-Modified: Sun, 30 Jun 2019 13:43:59 GMT
< ETag: "5d18bc9f-108"
< Accept-Ranges: bytes
```

```html:HTTPレスポンスbody
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>犬好きのためのページ</title>
  </head>
  <body>
    <h1>犬好きのためのページ</h1>
  </body>
* Connection #0 to host localhost left intact
</html>%
```

各コンテナ（リバースプロキシ・Webサーバ）のログをみてみましょう。

```shell:nginxのログ
reverse-proxy_1  | 172.31.0.1 - - [01/Jul/2019:13:38:14 +0000] "GET /dog HTTP/1.1" 200 264 "-" "curl/7.54.0"
dog-container    | 172.31.0.1 - - [01/Jul/2019:13:38:14 +0000] "GET / HTTP/1.0" 200 264 "-" "curl/7.54.0" "-"
```

## 結局何が起こったの？
注目すべきなのは、クライアント側（curlを叩いた側）は、 `dog-container` というWebサーバの存在を全く知らないでも通信できているという点です。

つまり、クライアントからは、 `localhost/dog` というGETリクエストを `localhost:80` に送り、そこからレスポンスを受け取ったように見えますが、**実際は `dog-container` という（本当の）Webサーバからレスポンスが返ってきている**のです。

もちろん猫用もみれます。

```
curl localhost/cat
```

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>猫好きのためのページ</title>
  </head>
  <body>
    <h1>猫好きのためのページ</h1>
  </body>
</html>%
```

もちろんブラウザからもみれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260547/011d1bd8-ce2d-ec35-f718-46be36d6a4ef.png)

バッチリですね。

# まとめ
リバースプロキシの理解のために、実際にリバースプロキシを介したリクエストとレスポンスを確認しました。

リバースプロキシを使うことで、Webサーバを外から直接アクセスできないようにするだけでなく、リクエストに応じてサーバを自在に変えることができるため、柔軟なサーバ構成を考えられるようになるかと思います。

今回の実装のソースコードは[GitHub(zawakin/reverse-proxy)](https://github.com/zawakin/reverse-proxy)にアップロードしていますので、ぜひローカルで試してみてください。

# 参考サイト

- [本ページの元ネタ(訳した)](https://stackoverflow.com/questions/54061155/nginx-proxy-upstream-server-docker-compose-502-bad-gateway-connection-refused)
- [nginxのproxy_passの注意点
](https://www.xmisao.com/2014/05/09/nginx-proxy-pass.html)
- [docker-composeの書き方（英語）](https://docs.docker.com/compose/compose-file/)
- [コンテナ間通信（ネットワークを作成する）](https://knowledge.sakura.ad.jp/16082/)