Pythonあまり使っていない人がDjangoのコンテナ実行環境(Fargate)を考えてみる

この記事は ハンズラボ AdventCalendar2022 10日目の記事です。
記事の内容は、ハンズラボ AdventCalendar2022 2日目、5日目の知識を前提に書いています。

# 背景

弊社では、Djangoでアプリケーション開発もします。私が所属している部署でも近くDjangoアプリケーションをECS(Fargate)構成でアプリケーションをリリースしようとしています。
Djangoアプリケーションをコンテナで動かす場合、一般的にどういった構成にしているんだろう？とGoogle検索しました。しかし、今年のアドベントカレンダーで私が書かせていただ内容をもとに考えると、しっくりするものがないように個人的に感じました(調べ方が悪いということもあると思います)。また、Djangoアプリケーションをコンテナで動かす記事が少ない印象を受けました。

例えば、Dockerのサイト（日本語翻訳サイトですが、公式も同じ指定方法です）にDjangoのサンプルが書かれています。しかし、docker-composeの中で使っているサーバが開発向けサーバになっているようです。

>python manage.py runserver 0.0.0.0:8000


これについては、https://docs.djangoproject.com/ja/4.1/ref/django-admin/#runserver でも強調して書かれています。
uWSGIの作りを考えてもプロセスやスレッド、リクエスト回数によるリスタートであったり、プロセス権限などなど本番環境で使用するサーバとしては適切ではないと考えています。特に、プロセス・スレッド数指定ができないのはダメージが大きいと思います。サーバスペックが活かしきれないからです。


>DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through security audits or performance tests. (And that's how it's gonna stay. We're in the business of making web frameworks, not web servers, so improving this server to be able to handle a production environment is outside the scope of Django.)

他にも例はありましたが、この構成でスケールアウトしていけるのかな？という疑問をもつ例もありました。
というわけで、自分なりに構成案を考えてみました。

# 1つのコンテナ構成

Googleで検索するとNginxとuWSGIとをコンテナで分けるケースが非常に多いのですが、そこまで大規模ではない場合、分けなくても良いと考えています。1つのコンテナにNginx, uWSGIを同居させます。この構成は、ECSを使う場合に難しいことを考えなくてもスケールアウトがしやすく、コンテナが１つであるためコストも抑えられると考えています。
仮にどうしてもコストを抑えつつ、Djangoアプリケーションを複数運用したい場合には、Emperor modeを使うことで複数アプリケーション運用も可能だと考えています。

![スクリーンショット 2022-12-04 9.50.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/682d0188-4a47-2d6e-7fe0-85b26c9261f1.png)

# 複数のコンテナ構成

Googleで検索すると、この構成が多いと感じています。この構成は、静的コンテンツと動的コンテンツとのワークロードが違っていて、たとえば静的コンテンツのリクエストはそこそこだが、動的コンテンツのリクエストが多いときに良い気がします。これは、動的コンテンツの方だけスケールアウトしたり、スケールアップという調整ができるからです。
静的コンテンツについてはCDN使えばいいのでは？という気もしますが、NginxとuWSGIとの間でパラメータ設定タイムアウト設定をしているのでNginxが不要なのか？というとそうでもないように思います。

![スクリーンショット 2022-12-04 9.53.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/97b54bd3-7c6f-f9b7-35b9-c7eea68519e6.png)

この構成のときにスケールアウトをすると、このような形になる可能性もあります。（スケールアウトしたときの一例です。負荷次第で形は違ってくるはずです）
![スクリーンショット 2022-12-04 10.01.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/9917b53c-5307-fd05-277b-867e0b3e200b.png)

複数コンテナ構成をECS上で動かす場合、コンテナ間の通信をどうしたら良いのだろう？という疑問が出てくると思います。
今年のアドベントカレンダーで書かせていただいた内容をもとに考えると、NginxでuWSGIをproxy_pass、もしくはuwsgi_passで指定すると思います。どちらで設定するにしても、動的に増減しうるuWSGIが動作するサーバをリアルタイムにNginxのuWSGIのロードバランス設定に反映することは困難です。

このとき、Nginxと、uWSGIの間に、ロードバランサーやロードバランサー的な役割としてNginxを置く案が出てくると思います。しかし、たとえばシステムの利用者が少ない場合、コストも掛かるし、管理も手間がかかるためオーバースペックにも感じます。
![スクリーンショット 2022-12-05 10.33.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/60790687-73ee-9e82-d7ac-94b74452802b.png)

他の案としてですが、例えばuwsgi_passとして共有ストレージにソケットファイルを置くかというと、また、共有ストレージのコストが追加になってしまいます。この案では動的なUnixDomainSocketをuWSGI側で認識をする方法がないようにも見えます。スケールアウトができない可能性があると思います。また、複数のサーバで1つのunix domain socketを共有して問題がないのかまで、私は確証がとれていないです。
![スクリーンショット 2022-12-05 10.42.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/1e0633d3-6f71-ee96-4fc1-c38c513ebe05.png)

したがって、これは確認ができていないですが、おそらくECSのサービス検出をしてDNS的に複数のuWSGIコンテナを解決できるようにするのがコスト・管理的に良いのではないかと考えています。具体的には、http_passのところにサービス検出するサーバ名をしているのではないか？と思っています。
![スクリーンショット 2022-12-05 10.39.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/30c015cd-20b0-3ecc-c022-bf6a9c2d73e0.png)

# １つのコンテナ構成で動かしてみる

複数サーバ構成では、それなりに考えることが多いのと、今回想定しているユーザ数は1つのコンテナでも問題なさそうなので1つのコンテナ構成で進めます。

1つのコンテナで動かす場合には以下のようなDockerfileになります。（`/var/app`は場所があまり良くないので、実際には直す予定)
今回は`/var/app`にDjangoアプリケーションを配置します。ファイル中のカスタム設定ファイルは、適宜読み替えてください。Djangoアプリケーションは、Djangoチュートリアルのmysiteアプリケーションにしています。

```dockerfile:Dockerfile
FROM debian:bookworm
RUN apt-get update && \
    apt-get install -y sqlite3 python3 nginx systemd uwsgi uwsgi-plugin-python3 python3-pip && \
    pip install django && \
    rm /etc/nginx/sites-enabled/default && \
    mkdir -p /var/app

COPY Nginxのカスタム設定ファイル /etc/nginx/sites-available 
COPY uWSGIのカスタム設定ファイル /etc/uwsgi/apps-available 
COPY Djangoアプリケーション/ /var/app/

RUN ln -s /etc/nginx/sites-available/Nginxのカスタム設定ファイル /etc/nginx/sites-enabled/ && \
    ln -s /etc/uwsgi/apps-available/uWSGIのカスタム設定ファイル /etc/uwsgi/apps-enabled/
CMD systemctl start nginx
CMD systemctl start uwsgi

EXPOSE 80
ENTRYPOINT [ "/lib/systemd/systemd" ]
```

```nginx:Nginxカスタム設定ファイルの例
server {
    listen 80;
#	listen [::]:80;

#	root /var/www/example.com;
	index index.html;

	location / {
		try_files $uri $uri/ =404;
	}
# locationはアプリに応じてパスを修正
    location /polls {
        # uwsgi_paramsを設定しないと、Djangoにパラメータが届いてこないものがでてきてしまう
        include /etc/nginx/uwsgi_params;
        uwsgi_pass unix:/var/app/mysite.sock;
    }
}
```
debian内のuwsgi設定ファイルをみるとsocketを置く場所が決まっています。このため`/var/app`は良くないので注意してください。

```ini:uWSGIのカスタム設定ファイル
[uwsgi]
chdir=/var/app/mysite
module=mysite.wsgi:application
vacuum=True
socket=/var/app/mysite.sock
max-requests=5000
env=DJANGO_SETTINGS_MODULE=mysite.settings
plugins=python3
```
apt-get経由でuWSGIをインストールしているため、pythonプラグインを指定しています。pipで入れる場合、plugins指定なくても動作するはずです。

debianの設定をみていて気がついたのですが、uWSGIの設定は上記の内容だけではなく、もう少し必要なパラメータが補われて動作します。当初は、`uwsgi --ini`で動かせばよいのではないかと思っていたのですが、こうしてしまうとパラメータが補われないため、自分で指定する範囲が広くなってしまいます。このため、systemctl経由でuWSGIを動かしたほうが良いと考えています。

ここまでの設定でコンテナイメージが作成できます。
1つのコンテナ構成であれば、ECS(Fargate)上で動作させる際でもあまり悩まずに設定でき、かつスケールアウトしたときのコンテナ通信まで考慮しなくても良いように思います。

# 動作確認

(ここではDockerイメージのビルド・起動の一例を示しています。イメージのタグ名であったり、ポートなど設定は適宜読み替えてください。)

Dockerイメージをビルドします。
```shell
docker build -t django_container .
```

ビルド後、コンテナを起動します。
```shell
docker run -itd --privileged --rm -p 8080:80 django_container
```

最後にブラウザで、`http://localhost:8080/polls`に接続します。
チュートリアルアプリケーションですが、データベース疎通（Sqlite3ですが）もでき、動作しています。
![スクリーンショット 2022-12-05 10.08.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/82e9c7ba-b7a8-b626-79ba-d695b1d2499a.png)

# 後日談

コンテナもこれで良さそうかなとおもっていたら、アドベントカレンダーの本ブログ公開の３日前に、
「Fargateだとprivileged指定ができないのでsystemdがエラー吐いてしまって起動できないです」と言われてしまいました。。
なるほど。こういったこともnginx, uWSGIを分けている理由の１つなのかなと思いました。

「Dockerさん、１コンテナで複数プロセスはやはり非推奨ですかね？」ということで調べてみたところ、
https://docs.docker.com/config/containers/multi-service_container/
こういった場合について、いくつかの例を書いてくれていました。

apacheのworkerのようなケースもあり、複数のプロセスを動かしてはいけないかというとそうではなく、親プロセスが子プロセスについても管理できていればよいと思いました。ただ、１コンテナに対して１つのサービスが推奨なようです。

１つのサービスをどう捉えるかですが、今回はNginx+uWSGIを１サービスとして捉えているので（無理やり感？）、privileged指定しないのであれば、sysytemdは外して同じように起動してあげれば良いかなと思い、以下のように変更しました。

entrypoint.shでnginx, uwsgiを管理。このためデーモン化はしないように変更
```shell:entrypoint.sh
#!/bin/bash

/usr/bin/uwsgi --ini /etc/uwsgi/apps-enabled/django.ini &
/usr/sbin/nginx -g "daemon off; master_process on;"
  
wait -n
exit $?
```

django.iniでは、systemdで起動するときに補完してもらっていたパラメータを追記。また、ログも指定した場所に出るように変更（これは、Fargateだと標準出力にしたほうが良いかも。）
```ini:django.ini
[uwsgi]
chdir=/var/app/mysite
module=mysite.wsgi:application
uid = www-data
chmod-socket = 660
chown-socket = www-data
no-orphans = true
workers = 2
logger=file:/var/log/uwsgi/app/django.log
log-date = true
vacuum=True
socket=/var/app/mysite.sock
max-requests=5000
env DJANGO_SETTINGS_MODULE = mysite.settings
plugins=python3,logfile
```

Dockerfileでは、systemdとお別れをして、entrypoint.shにきてもらいました。
```Dockerfile:Dockerfile
FROM debian:bookworm
RUN apt-get update && \
    apt-get install -y sqlite3 python3 nginx uwsgi uwsgi-plugin-python3 python3-pip && \
    pip install django && \
    rm /etc/nginx/sites-enabled/default && \
    mkdir -p /var/app

COPY Nginxのカスタム設定ファイル /etc/nginx/sites-available 
COPY uWSGIのカスタム設定ファイル /etc/uwsgi/apps-available 
COPY Djangoアプリケーション/ /var/app/

RUN ln -s /etc/nginx/sites-available/Nginxのカスタム設定ファイル /etc/nginx/sites-enabled/ && \
    ln -s /etc/uwsgi/apps-available/uWSGIのカスタム設定ファイル /etc/uwsgi/apps-enabled/

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh

EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]
```

Dockerイメージをビルドします。
```shell
docker build -t django_container .
```

ビルド後、コンテナを起動します。(--privilegedは不要)
```shell
docker run -itd --rm -p 8080:80 django_container
```

また、変更後、Fargateでも動作することが確認できました。（B/G Deployもこれからなので、もう少し修正すべき箇所はあるかもしれません）
（確認できたのがアドベントカレンダーの公開３日前だから、あぶないところだった。。）

## 2022/12/10 追記

https://aws.amazon.com/jp/blogs/compute/task-networking-in-aws-fargate/
を見ると、1つのタスクに複数コンテナを使ってHTTPで連携という感じで書かれています。(nginx-uWSGIをhttp_passでつなげるイメージ)
良くも悪くもECRのコンテナイメージ数のトレードオフもありそうな気もするのでケース・バイ・ケースで判断しても良い気がしました。ひとまず今回の記事は、1つのコンテナでもできるよ、ということで書いてみました。

# 最後に

自分なりに考えた構成をまとめてみました。アドベントカレンダーの5日目に書かせていただいた、Emperor modeを調べたときの理解がなければ、こういった構成案を考えるところまでは来れなかった気がしています。単純にカッコいい！という気持ちでしたが調べておいてよかったです。

サイトの規模次第では、1つのコンテナ案でよいのではないかという結論になりましたが、もしニーズが多いようであれば、複数コンテナ＋ECS(fargate)も試してみたいと思っています。