uwsgiのDjangoアプリをnginxと同一コンテナで起動する場合の非機能要件Tips


https://qiita.com/nbykinb/items/7e809135ab5dbbd6c390

上記の記事は職場の方が先行して書いてくれたもので、関連してコンテナ周りの実装Tipsを少しまとめました。


## Dockerfile概要
- ECS Fagateを使う
- ベースは`debian:bookworm`
- アプリはVueフロントエンド + Djangoバックエンド
- Vueビルドはデプロイパイプライン上で済ませる
    - Dockerfileでは`COPY`対象
- 機能開発を優先したいので最低限の可用性・応答性能を確保できればコンテナ構成は単純で良い

### 1コンテナ1プロセスの原則は？
今回は1つのコンテナにnginxもアプリ（uwsgi + django, ビルド済みの`dist/**`）も詰め合わせです。
DevOpsの落とし所として、１コンテナにフルスタック構成することは今後もあるでは、と考えています。

なので今回はコンテナ1つで一定の非機能要件を担保するためのTips記録です。


## 非機能要件いろいろ
- モニタリング
    - コンテナの[HEALTHCHECK機能](https://docs.docker.jp/engine/reference/builder.html#healthcheck)を使う
    - あとはECS AutoScalingにキャパシティ管理（間接的なリソース監視）を任せて、最低限の可用性を確保する
- デバッグ用途もあるのでログ収集したい
    - 今回は[awslogsドライバ](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/using_awslogs.html)を採用
- webサーバとしてnginx、appサーバとしてuwsgiを使う
    - 同時接続を捌いたりweb/appそれぞれでプロセス管理してもらうため
    - 同一コンテナなのでsoft/hardのlimitは定義できず、柔軟性には欠ける
    - 今回はECS AutoScalingに任せる方針なのでweb/app一緒にスケールする
- デプロイパイプライン
    - （長くなるため...本記事では割愛）


## コンテナHEALTHCHECK
ELB構成のECS Fargateであればポート疎通のヘルスチェック状態は担保されますが、よりアプリ機能に即したヘルスチェックを簡単に実装できるのがコンテナHEALTHCHECK機能です。

ECS AutoScalingに任せておけば可用性は問題なし、
と言えるのは正常系だけなので、アクセスが集中したり直近のリリースにバグが含まれていたり、アプリが応答不能になってしまう場合、コンテナHEALTHCHECK結果に基づいてECS Fargateはタスクを自動交換してくれます。

- ECS Fargateの場合、コンテナHEALTHCHECKはDockerfileではなくECSタスク定義に記述する
- タスク定義例（抜粋）
```json
            "healthCheck": {
                "command": [
                    "CMD-SHELL",
                    "curl -fso /dev/null http://localhost/ || exit 1"
                ],
                "interval": 60,
                "timeout": 3,
                "retries": 3,
                "startPeriod": 0
            }
```

<details>
  <summary>ECSコンソール</summary>

![ecsTask_healthCheck01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215094/9538bb4b-73e9-f223-7051-c05d29a69a80.png)

![ecsTask_healthCheck02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215094/b81c22b3-13dd-4d73-45e1-4b6d845fa143.jpeg)
  
</details>




## コンテナログ収集
AWSドキュメントにも記載されていますが、stdout/stderrをキャプチャすることがコンテナのロギング仕様です。１コンテナ１プロセスではない今回の方針ではいくらかtipsが必要でした。

- コンテナではnginxとuwsgi、最終的に2つのプロセスを起動する必要がある
- コンテナはフォアグランドプロセスを必要とする
    - フォアであれば標準出力（ロギング）されるが、2つ以上のフォア起動は不可能では...
- web検索した限り、`systemd`や`supervisord`を使って複数プロセスをフォアグランド起動している試みは発見した
    - 信頼できる方法ではあるが...他チームに展開するには気が引ける
- そもそも複数プロセスを責務とする場合、`ENTRYPOINT`はどうなる？
    - nginxとuwsgiの起動パラメータを調整して何とかなった
- uwsgiはフォアグラウンド実行して、そのままstdout/stderrにログ出力する
- nginxはバックグラウンド実行しつつ、`server`ステートメントでログ出力先をstdout/stderrに変更


### Dockerfile: ENTRYPOINT
```dockerfile
ENTRYPOINT /usr/sbin/nginx -g "daemon on; master_process on;" && \
    /usr/local/bin/uwsgi --ini /var/app/django.ini
```
- `ENTRYPOINT`のプロセス（つまりフォアグラウンド）はPID1として扱われる
    - graceful shutdownしたい対象をフォアグランド選択する方が良い
    - 今回はuwsgiアプリでシグナルハンドラを実装する余地を残している

### nginxログ設定（抜粋）

```nginx
server {
    access_log /dev/stdout combined if=$log_enable;
    error_log /dev/stderr;
}

# conf.d/配置
# ELBヘルスチェックとコンテナHEALTHCHECK、両方をログから除外する
map $http_user_agent $log_enable {
    ~ELB-HealthChecker      0;
    ~curl/                  0;
    default                 1;
}
```

### uwsgiパラメータ（抜粋）
```
[uwsgi]
master=true
single-interpreter=true
chown-socket=www-data
```
- socketファイルの書き込み権限がwebサーバにないとリクエストは失敗する
- 公式の"Things to know"にならって、`master`と`single-interpreter`を追加


### appendix; uwsgiリロード
uwsgiに明示的なreload/restart命令はないため、OSプロセス一般の方法にならう。
```bash
#/usr/local/bin/uwsgi --ini /var/app/django.iniを実行中、かつPPID=1のプロセスを見つける
ps -ef
# 見つけたuwsgiプロセスIDにSIGHUPを送る
kill -HUP {PID}
# 以下のようなuwsgiログが記録される
gracefully (RE)spawned uWSGI master process (pid: 10)
```


## 参考ページ

https://gihyo.jp/dev/serial/01/perl-hackers-hub/000603

https://aws.amazon.com/jp/blogs/news/under-the-hood-fargate-data-plane/

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

https://github.com/tiangolo/uwsgi-nginx-docker

https://faun.pub/docker-logging-for-uwsgi-and-nginx-container-using-supervisor-and-fluentd-the-missing-piece-a6f72776bedf