Docker × Nginx パス名でリバースプロキシ

![docker_nginx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258841/5b81b1b4-d700-c57f-6183-d15be7d1611e.png)

## 公式

##### リファレンス

* [Dockerfile reference](https://docs.docker.com/engine/reference/builder/)
* [Compose file version 3 reference](https://docs.docker.com/compose/compose-file/#/tmpfs)

##### DockerHub

* [nginx](https://hub.docker.com/_/nginx?tab=description)
* [python](https://hub.docker.com/_/python)

## ディレクトリ構造

下記のようなディレクトリ構造で構築していきます。
コードについてはGitHubにて公開しています。

[ryuichi1208/api-srv](https://github.com/ryuichi1208/api-srv)

```
├── Makefile
├── README.md
├── docker-compose.yml
├── proxy
│   ├── Dockerfile
│   ├── conf.d
│   │   └── default.conf
│   └── nginx.conf
├── webapp_flask
│   ├── Dockerfile
│   ├── main.py
│   └── requirements.txt
├── webapp_go
│   ├── Dockerfile
│   ├── Makefile
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   └── static
│       ├── handler.go
│       ├── main.go
│       ├── server.go
│       └── template.go
└── webapp_sh
    ├── Dockerfile
    ├── index.html
    └── main.sh
```

## APIサーバ定義

FlaskとGo言語で簡単なAPIサーバを定義しておきます。

#### Flask (Python)

Dockerで動作させるための最低限のファイルを用意します。
Flaskではエンドポイントとして「/api/flask/hello」を実装します。

(Flaskとは、pythonのWebフレームワークで軽量なWebフレームワークであり、小さく使い始めることができるのが特徴です)

``` python:main.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
from flask import abort, jsonify, request
import json

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello world'

@app.route('/api/flask/hello')
def api():
    return jsonify({'ip': request.environ.get('HTTP_X_REAL_IP', request.remote_addr)}), 200

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=80)
```

``` Dockerfile:Dockerfile
FROM python:3.7.4-alpine3.9
WORKDIR /home/app
COPY . .
RUN pip install -U pip && pip install -r requirements.txt

ENTRYPOINT ["python", "main.py"]
CMD [""]
```

``` requirements.txt
Flask==1.1.1
```

#### Ecoh (Golang)

golangとechoを使って簡単にAPIを実装します。
Echoではエンドポイントとして「/api/go/hello」を実装します。
EchoはgolangのWeb FWでHTMLも出力できます。
ただネット上で出ている情報数の多さ的にはAPIサーバとしてのユースケースが多いように感じました。
(詳しく調査したわけではないです。)

``` go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()
	initRouting(e)
	e.Logger.Fatal(e.Start(":80"))
}

func initRouting(e *echo.Echo) {
	e.GET("/", hello)
	e.GET("/api/go/hello", test)
}

func test(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{"go": "hello"})
}

func hello(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{"hello": "world"})
}
```

``` Dockerfile:Dockerfile
## Build ##

FROM golang:alpine3.10 AS build
WORKDIR /home/app
COPY . .
ENV GO111MODULE=on
RUN apk --no-cache add git make build-base \
	&& make \
	&& go get github.com/labstack/echo/v4

## App ##

FROM alpine:3.9.4
WORKDIR /home/app
RUN apk --no-cache add tzdata && \
	cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
COPY --from=build /home/app/main /home/app
EXPOSE 80
ENTRYPOINT ["./main"]
CMD []
```

## Nginx

serverコンテキスト内の記述は下記のように設定します。
今回はパス名ベースでのリバースプロキシなのでproxy_passへプロキシ先のサーバを記述します。
詳細設定については過去に下記のような記事を書いたのでご参照ください。
[(今更)Nginx覚書](https://qiita.com/ryuichi1208/items/50594810be613d838948)

proxy_set_headerではプロキシ先のWebサーバで送信元IPアドレスを取得する為に設定してます。

``` nginx:/etc/nginx/conf.d/default.conf
server {
    listen 80;

    server_name www.example.com;

    proxy_set_header    Host    $host:$server_port;
    proxy_set_header    X-Real-IP    $remote_addr;
    proxy_set_header    X-Forwarded-Host       $host;
    proxy_set_header    X-Forwarded-Server    $host;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;

    location /api/flask/ {
        proxy_pass http://web_flask_upst;
    }

    location /api/go/ {
        proxy_pass http://web_go;
    }

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page 497 /497.html;
    location = /497.html {
        root /var/www/html;
        internal;
    }
}
```


## docker-compose.yml

docker-composeを用いて上記のコンテナを一括で起動していきます。
今回は検証の為全てのコンテナは同一のネットワークへ属するようにします。
各コンテナには「container_name」を設定してコンテナ名で通信が可能になるように設定します。

|名前|用途|
|---|----|
|check|クライアント用のコンテナ。pingやcurlなど基本的なネットワークコマンドを実行|
|proxy|nginx用のコンテナ|
|web_flask|flask用のコンテナ|
|web_go|go用のコンテナ(Echo)|

``` yaml
---
version: '3.7'

services:
  check:
    container_name: check
    hostname: check_server
    image: centos:centos7
    tty: true

  proxy:
    container_name: proxy
    hostname: reverse_proxy
    build: ./proxy
    deploy:
      replicas: 1
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./proxy/nginx.conf:/etc/nginx/nginx.conf
      - ./proxy/conf.d/default.conf:/etc/nginx/conf.d/default.conf

  web_flask:
    container_name: web_flask
    hostname: web_flask
    build: ./webapp_flask
    deploy:
      replicas: 1

  web_go:
    container_name: web_go
    hostname: web_flask
    build: ./webapp_go
    deploy:
      replicas: 1

networks:
  default:
    ipam:
      config:
        - subnet: 192.168.2.0/24
```

* [Compose file version 3 reference](https://docs.docker.com/compose/compose-file/#network_mode)

## SSL化(オレオレ証明書)

リバースプロキシでのSSLオフロードを試す為だけに自己署名証明書を作成します。

``` bash
$ docker-compose exec proxy bash
$ cd /etc/nginx/ssl/

# 秘密鍵の生成
openssl genrsa 2048 > server.key

# 証明書署名要求の作成
$ openssl req -new -key server.key > server.csr

# 自己署名
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

# 確認
$ ls -l
server.crt  server.csr	server.key

# SSLサーバ証明書の内容
## 公開鍵の情報
## コモンネーム
## 公開鍵の所有者情報
## 所有者を証明した認証局の情報
## 証明書の有効期限
## 証明書のシリアル番号
## 証明書の失効リスト参照先
```

上記のパスで証明書を生成したらあとはnginx.confへsslの設定を追加するだけで設定が完了します。

``` nginx:nginx.conf
server {
    listen 443 ssl;
    server_name localhost;

    ssl_certificate_key /etc/nginx/ssl/server.key;
    ssl_certificate /etc/nginx/ssl/server.crt;
}
```

若干余談ですが切れがちな証明書の有効期限をチェックするスクリプトを書いてる人がいたので便利！と思ったので紹介します。

[SSL証明書の有効期限を確認するスクリプト](https://qiita.com/jsaito/items/17f115974341fe57879c)

opensslコマンドで期限を確認し現在時刻と比較する処理をループしてるようです。
この結果をなんらかの通知先(slackなど)へやっておくとアクセスした時に初めて気づくなどのようなことが減りそうで良さそうですね。

## (余談)リバースプロキシメモ(proxy_next_upstream)

proxy_next_upstreamはリクエストの送信先）からエラーが返されたりした際の動作に関する設定を行います。
proxyしたサーバーからのレスポンスがstatus_code:5xxだった場合やタイムアウトなどの時にリトライする仕組みを設定できます。

* [Module ngx_http_proxy_module](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream)

``` nginx:nginx.conf
upstream flask_app {
    server 192.168.1.1;
    server 192.168.1.2;
    server 192.168.1.3;
}

    location / {
        proxy_pass http://flask_app;
        proxy_next_upstream http_500;
    }
}
```

上記の設定ではupstremで設定したサーバへproxyした結果が500を返したきた場合は次のサーバへリクエストをリトライするような設定です。
ちなみにerror_pageを無効にしないとupstreamの設定と競合するので注意が必要です。

|設定|概要|
|---|----|
|http_500|a server returned a response with the code 500
|http_502|a server returned a response with the code 502;
|http_503|a server returned a response with the code 503;
|http_504|a server returned a response with the code 504;
|http_403|a server returned a response with the code 403;
|http_404|a server returned a response with the code 404;
|http_429|a server returned a response with the code 429;

## 参考サイト

* [docker for mac + nginx + オレオレ証明書でローカルSSL環境を作ったメモ](https://katekichi.hatenablog.com/entry/2017/06/14/docker_for_mac_%2B_nginx_%2B_%E3%82%AA%E3%83%AC%E3%82%AA%E3%83%AC%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%A7%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%ABSSL%E7%92%B0%E5%A2%83%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%9F)
* [訪問者のIPアドレスを取得する](https://living-sun.com/ja/python/704926-get-ip-address-of-visitors-python-flask-ip-address-werkzeug.html)
* [Go の echo ってWebサーバーでサクッと REST しちゃう](https://qiita.com/ezaki/items/62e806ae42828bb3567a)
* [Nginx でリバースプロクシを立てるときに気にすべき proxy_next_upstream 設定](https://muziyoshiz.hatenablog.com/entry/2017/10/25/235114)