Dockerで Nginx + uWSGI + Flask の疎結合なWebアプリケーションサーバーを構築する

**docker composeやソケットファイル無しで**ソケット通信を使用した nginxコンテナ ⇔ uWSGIコンテナ（Flaskアプリ）環境を構築する方法です。
※本稿の設定例そのままの値だとIP直指定しており、疎結合とは到底言えない状態のため、最終的にはサービス毎にAPIエンドポイントを作成するなど設定値を置き換えて利用ください。

それぞれのサービスを疎結合にして、マイクロサービスなアーキテクチャ設計をするために、サービス毎にコンテナを細かく分割するケースは多いと思います。
ただ、ロードバランサーやプロキシ、APIエンドポイントなどを用いず、コンテナを独立させながら直接疎通させるのは手間で疎結合にも出来ないので、特別な事情が無ければ止めた方が良さそうです。（あまり例も見かけないですし）
例えば、自動起動設定のコンテナを起動しているホストマシンが再起動された場合、コンテナのIPが変わってしまい、設定に反映しないとコンテナ間で疎通が出来なくなる、などの問題が発生するためです。

# 環境
Amazon ECS(別タスク定義)やDockerが起動しているマシン（RHEL7系Linux）にて動作を確認しました。
本稿では、例として公式AMIから起動したばかりで初期状態のAmazonLinux2上に構築します。

<details><summary>AmazonLinux2に構築するコマンド例</summary>

```shell-session
$ sudo su
# yum update -y
# yum -y install docker
# systemctl start docker
# systemctl enable docker
# systemctl status docker
# mkdir /opt/example-service

# mkdir /opt/example-service/app
# cd /opt/example-service/app/
# nano hello.py
# nano uwsgi.ini
# nano Dockerfile
# docker build -t hello -f Dockerfile .
# docker run -itd -p 3031:3031 hello

# mkdir /opt/example-service/web
# cd /opt/example-service/web/
# nano env_list
# nano uwsgi.conf.template
# nano Dockerfile
# docker build -t hello/nginx -f Dockerfile .
# docker run -itd --env-file=env_list -p 80:80 hello/nginx
# curl http://172.17.0.3/hello
```
</details>

# App
Flaskアプリのファイルを作成します。
※色々書いてますが関数はhelloだけでも大丈夫です。

```python:hello.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/healthcheck')
def healthcheck():
    return ""

@app.route("/hello")
def hello():
    return jsonify({
        "message": "Hello World!"
    })

@app.route('/api/v1/status/200', methods=['GET'])
def status_200():
    app.logger.info('execute 200 status')
    return jsonify({'message': '200 status message'}), 200

@app.route('/api/v1/status/400', methods=['GET'])
def status_400():
    app.logger.info('execute 400 status')
    return jsonify({'message': '400 status message'}), 400

@app.route('/api/v1/status/500', methods=['GET'])
def status_500():
    app.logger.info('execute 500 status')
    return jsonify({'message': '500 status message'}), 500
    
# Flaskのみで動作するビルトインサーバーを起動する※ローカルで動かす時用
if __name__ == '__main__':
    PORT = 5000
    app.run(
        threaded=True,
        debug=True,
        port=PORT,
        host='0.0.0.0',
    )
```

uWSGIの設定ファイルを作成します。

```ini:uwsgi.ini
[uwsgi]
env=TZ=UTC-9
socket=0.0.0.0:3031 # nginxとのソケット通信用
#http=0.0.0.0:9090 # 単体(Webサーバー無し)で動作するHTTPサーバーを起動する※uWSGIコンテナに直接HTTPリクエストしたい時用
wsgi-file=./hello.py
master=true
callable=app
```

アプリを動かすためのDockerfileを作成します。

１行目を`FROM python:latest`にすれば3~9行目のパッケージインストールコマンド群は不要になりますが、コンテナイメージサイズは倍（421MB ⇒ 952MB）になります。

```Dockerfile:Dockerfile
FROM python:alpine

RUN apk --update-cache add \
    gcc \
    g++ \
    build-base \
    linux-headers \
    python3-dev \
    pcre-dev

RUN pip install --upgrade pip \
    && pip install --no-cache-dir \
    Flask \
    uwsgi

COPY hello.py ./
COPY uwsgi.ini ./

EXPOSE 3031
CMD uwsgi uwsgi.ini
```

2023/10/06 追記
現在このコンテナイメージをbuildしようとするとuwsgiのインストール時にエラーが出ます。（原因及び解決策未調査）

https://github.com/unbit/uwsgi/issues/2232


## コンテナビルド＆起動コマンド例
```shell-session
$ sudo docker build -t hello -f Dockerfile .

$ sudo docker run -itd -p 3031:3031 hello
```


# Web
設定変更するためだけに毎回ビルドしなくて済むようにenvsubst用設定ファイルを作成します。（ECSなら環境変数に指定する。）
* SERVER_NAME：nginxコンテナにアクセスするIPもしくはFQDN
* RESOLVER：HOST_NAMEを解決できるレコードを持ったDNSサーバーIP
* HOST_NAME：uWSGIコンテナのIPもしくはFQDN
* PORT：uWSGIコンテナ側で待ち受けているwsgiソケットポート

```:env_list
SERVER_NAME=127.0.0.1
RESOLVER=169.254.169.253
HOST_NAME=172.17.0.2
PORT=3031
```

nginxの設定ファイルを作成します。

```json:uwsgi.conf.template
server {
    listen 80 default_server;
    server_name ${SERVER_NAME};

    location / {
        include uwsgi_params;
        resolver ${RESOLVER};
        set $url ${HOST_NAME};
        uwsgi_pass $url:${PORT};
    }

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

Dockerfileを作成します。

```Dockerfile:Dockerfile
FROM nginx:alpine

RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata

COPY ["uwsgi.conf.template","/tmp/"]

EXPOSE 80
ENTRYPOINT ["/bin/sh","-c"]
CMD ["envsubst '$$SERVER_NAME $$RESOLVER $$HOST_NAME $$PORT' < /tmp/uwsgi.conf.template > /etc/nginx/conf.d/uwsgi.conf && nginx -g 'daemon off;'"]
```

## コンテナビルド＆起動コマンド例
```shell-session
$ sudo docker build -t hello/nginx -f Dockerfile .

$ sudo docker run -itd --env-file=env_list -p 80:80 hello/nginx
```


# 確認
nginxが起動しているコンテナにリクエストします。

```shell-session
$ curl http://172.17.0.3/healthcheck
$ curl http://172.17.0.3/hello
{"message":"Hello World!"}
$ curl http://172.17.0.3/api/v1/status/200
{"message":"200 status message"}
$ curl http://172.17.0.3/api/v1/status/400
{"message":"400 status message"}
$ curl http://172.17.0.3/api/v1/status/500
{"message":"500 status message"}
```

（ECS以外）uWSGI, nginxそれぞれのログを確認したい場合

```shell-session
$ sudo tail -f /var/lib/docker/containers/[コンテナID]/[コンテナID]-json.log
```

# おまけ：docker-composeを用いる場合
docker networkのデフォルトサブネットアドレス帯と重複するのを避けるために、異なるアドレス帯を指定しています。
本稿例通りにenv_listの`HOST_NAME`を設定されている場合は、第二オクテットを 18 に変更してください。
```:env_list
HOST_NAME=172.17.0.2
              ↓
HOST_NAME=172.18.0.2
```

<details><summary>AmazonLinux2に構築するコマンド例</summary>

```shell-session
$ sudo su
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose

# nano /opt/example-service/docker-compose.yml
# cd /opt/example-service/
# /usr/local/bin/docker-compose up -d
# curl http://172.18.0.3/hello
```
</details>

```yml:docker-compose.yml
version: '3'
services:
  app:
    image: hello
    networks:
      hello-app-net:
        ipv4_address: 172.18.0.2
    ports:
     - "3031:3031"
    restart: always
  web:
    image: "hello/nginx"
    networks:
      hello-app-net:
        ipv4_address: 172.18.0.3
    ports:
     - "80:80"
    env_file: ./web/env_list
#    depends_on:
#     - app
    restart: always
networks:
  hello-app-net:
    driver: bridge
    ipam:
      driver: default
      config:
      - subnet: 172.18.0.0/24
#        gateway: 172.18.0.1
```

# 参考
## Flask
https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/

https://flask.palletsprojects.com/en/2.2.x/quickstart/

## uWSGI
https://www.python.ambitious-engineer.com/archives/1959

https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html

## Docker
https://mebee.info/2021/08/05/post-40153/

https://www.tohoho-web.com/docker/docker_logs.html

## docker-compose
https://man.plustar.jp/docker/compose/install.html

https://docs.docker.com/compose/compose-file/compose-file-v3/

https://man.plustar.jp/docker/compose/compose-file.html