Pythonあまり使っていない人が最小構成でuWSGIのエンペラーモードを発動してみた話

この記事は ハンズラボ AdventCalendar2022 5日目の記事です。

ハンズラボ AdventCalendar2022 2日目の記事として「Pythonあまり使っていない人がDjangoの動作環境を調べてみた話」を書きました。調査の際にuWSGIのマニュアルに「Emperor mode」という記述をみてしまいました。
なんだかわかりませんが、とにかくエンペラーモードという響きがカッコいい！ということで使ってみました。タイトルも多少違和感がありますが、カッコよさ重視のタイトルにしてみました。

# Emperor modeとは

Emperor modeが何かを理解しておく必要があるので公式ドキュメントを確認しておきます。
https://uwsgi-docs.readthedocs.io/en/latest/Emperor.html

ものすごく大雑把に書くと、設定ファイルを検出して、対応するuWSGIインスタンスを起動してくれるものだと思いました。
uWSGI上で動かしたいアプリケーションを作るたびに、OS設定してuWSGIサービスを追加するのは手間だから、設定ファイル作れば対応するuWSGIが起動してくれるのは手間が減るということのように感じました。

## ドキュメントを読んで理解したこと
（すべて書くとマニュアル翻訳になってしまうので、必要そうなところのみ書いてます）

単一のサーバ、もしくはサーバ群に多くのアプリケーションをデプロイする必要があるときに、Emperor モードが適切。
特別なuWSGIインスタンスで、特別なイベントを監視し、必要に応じて（Emperorに管理されているときに、vassalという）インスタンスを作り、停止、再ロードしてくれる。

デフォルトで、EmperorはuWSGI設定ファイルの指定されたディレクトリをスキャンする。スキャンは、imeperial monitorプラグインを使って拡張が可能。`dir://`と`glob://`はコアに含まれているからロードの必要がなく、自動的に検出される。`dir://`がデフォルト。

新しい設定ファイルを検出すると、設定ファイルに基づきuWSGIインスタンスが起動する。
設定ファイルが変わると、インスタンスが再ロードされる。
`touch --no-dereference`が友達とある。シンボリックリンク自体の変更日付を変えるために使うコマンドだからだと考えられる。

# (まずは）uWSGI単体の動作確認

Emperorモードを確認するためには、複数のuWSGI上で動作するアプリケーションが必要だと考えました。
とはいえ、Djangoのアプリを2つ作るのはやりすぎなように感じました。
そこで、今回は最小構成を意識して、uWSGIアプリケーションを起動してみることにしました。


## アプリケーションの準備
WSGIに則ったインターフェースをもつアプリケーションを準備しました。（ドキュメントにかかれているのものを完全コピーさせていただきました）
```python
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]
```
参照先：
https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html#the-first-wsgi-application

## uWSGIのインストール

手元のPython環境をごちゃごちゃさせたくなかったため、venv環境を作り、以下のコマンドでuWSGIをインストールしました。当初、brewをつかってuWSGIをインストールしましたが、python pluginが入っていないようで手間がかかりそうなため、pip経由にしました。

```bash
python -m pip install uwsgi
```

## アプリケーションの起動

こちらも公式ドキュメントを真似て起動しました。
```bash
uwsgi --http-socket :9090 --wsgi-file test.py
```

ブラウザで`http://localhost:9090`を確認するとアプリケーションが応答します。
![スクリーンショット 2022-11-29 18.19.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/5417bdf4-67b4-135c-b7a0-74f9dc8f3a45.png)

このアプリケーションをコピーして2つにします。２つのアプリケーションをEmperorモードで起動してみることにしました。

# Emperor modeの起動方法でハマる

実は、公式ドキュメントを読んでもEmperor modeの構成に関する理解が進まず、非常に困難でした。
当初は、以下のように構成を作り起動させようとしました。

```:ディレクトリ構成
apps
├── app1
│   └── test.py
├── app2
│   └── test.py
├── app2.ini
config
└── config.skel
└── app1.ini -(symlink)-> config.skel
└── app2.ini -(symlink)-> config.skel
```

```ini:config.skel
[uwsgi]
chdir = %dapps/%n
threads = 2
socket = /tmp/sockets/%n.sock
wsgi-file = test.py
```

設定ファイルを1つ作り、それをシンボリックリンクとしてアプリケーションの数だけ作ることで設定ファイルを個別に書かなくても良いようにします。skel拡張子は、uWSGIの設定ファイルではないのでスキップされ、実質、app1.ini, app2.ini の2つのアプリケーションの設定となります。

```bash
uwsgi --emperor ./config
```
これで、さくっと動くのかなと思いました。

```
[emperor] vassal app2.ini is ready to accept requests
[emperor] vassal app1.ini is ready to accept requests
```

なんか動いている感はあるが、これにどうやってアクセスするんだろう？
iniファイルにsocketを書いたけどプロトコル、つなぎ方もわからないな。ということで悩みつつ、ググりました。

## 悩み抜いた末に

公式ドキュメントでもピンときておらず、ググっても明確な答えは見つけられませんでした。
しかし、以下のGithubのリポジトリをみて理解できました。（うん、むしろやりたかったことがすべて書いてある。）

https://github.com/smn/uwsgi-emperor-example

設定をみて、管理用のuWSGIインスタンスが必要で、(vassalだと思われる）それとは別にアプリケーション別のuWSGIインスタンスが必要ということだと理解しました。また、socketについては、uwsgiプロトコルでNginxといったサーバと接続させるものと理解しました。

Webサーバは必須のようなので、nginxはインストールしておきました。
```bash
brew install nginx
```

# Emperor modeの設定

これまでの理解をもとに、今回の構成を以下のようにしました。

```:ディレクトリ構成
apps
├── app1
│   └── test.py
├── app1.ini -> config.skel
├── app2
│   └── test.py
├── app2.ini -> config.skel
└── config.skel
emperor.ini
nginx.conf
```

それぞれのファイルは以下のようにしました。

```ini:emperor.ini
[uwsgi]
http-socket = :8000
master = true
emperor = %dapps
emperor-on-demand-directory = %dapps
emperor-stats-server = :8001
```

```conf:nginx.conf
daemon off;
error_log /dev/stdout info;

events {
    worker_connections  1024;
}

http {

    access_log /dev/stdout;

    server {
        listen      9000;
        server_name _;
        charset     utf-8;

        # max upload size
        client_max_body_size 75M;   # adjust to taste
        location /app1 {
            # リバプロとかでHTTPプロトコルを使う場合
            # proxy_pass  http://unix:./apps/app1.socket:/;
            uwsgi_pass unix:./apps/app1.socket;
        }

        location /app2 {
            # リバプロとかでHTTPプロトコルを使う場合
            # proxy_pass  http://unix:./apps/app2.socket:/;
            uwsgi_pass unix:./apps/app2.socket;
        }
    }
}
```

(参考にさせていただいた設定そのままというのも味気ないので）今回は、リバプロをする必要はないと判断したので、uwsgiプロトコル、socketベースでuWSGIとやりとりするように設定してみました。


```ini:config.skel
[uwsgi]
chdir = %d../apps/%n
threads = 2
socket = %d%n.socket
wsgi-file = test.py
master = true
protocol = uwsgi
# HTTP プロトコルにしたい場合
# protocol = http
```
config.skelについては、最小構成で動作させる例がなく、指定できるオプションの説明が見つけられず、かなり苦戦しました。自分なりに考えた結論としては、ファイルの内容にuwsgiコマンドのオプションを書く感じかなと考えています。protocolに何を指定するのかについてですが、uwsgiのオプションをprotocolでgrepすると、以下のようなものがあるので、おそらくfastcgi, scgiなども指定できそうだと考えています。

```
    --http-socket                           bind to the specified UNIX/TCP socket using HTTP protocol
    --http11-socket                         bind to the specified UNIX/TCP socket using HTTP 1.1 (Keep-Alive) protocol
    --fastcgi-socket                        bind to the specified UNIX/TCP socket using FastCGI protocol
    --fastcgi-nph-socket                    bind to the specified UNIX/TCP socket using FastCGI protocol (nph mode)
    --scgi-socket                           bind to the specified UNIX/TCP socket using SCGI protocol
    --raw-socket                            bind to the specified UNIX/TCP socket using RAW protocol
    --puwsgi-socket                         bind to the specified UNIX/TCP socket using persistent uwsgi protocol (puwsgi)
```

test.pyについては、app1, app2のどちらかがわかるように `App1`, `App2`という文字列を追記しています。

```python:apps/app1/test.py
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World App1"]
```

```python:apps/app2/test.py
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World App2"]
```

# Emperor mode 発動！(単なる実行です）

以下のコマンドを実行して、Emperor modeで複数のuWSGIアプリの起動をさせます。

```bash
uwsgi --ini emperor.ini --enable-threads  
```

ついで、Nginxを起動します。
```bash
nginx -c `pwd`/nginx.conf
```

ブラウザで`http://localhost:9000/app1`, `http://localhost:9000/app2`に接続するとアプリケーションが動作していることが確認できます。app1であれば、以下のようになっています。
![スクリーンショット 2022-11-29 19.50.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984285/c92a332c-875b-641d-5c2e-a15e8de61912.png)

## touch --no-dereference は友達なのか？

冒頭に書きましたが、`touch touch --no-dereference`は友達とありました。
実際に友だちになれるのか確認してみました。
```bash
touch touch --no-dereference app1.ini
```
を叩いてみると想定通りにapp1だけがリロードされました。
```
[emperor] reload the uwsgi instance app1.ini
```

# 最後に

Emperor mode を使うことで設定・運用の手間が減らしつつ、複数のWSGIアプリケーションを扱えるようにすることができることがわかりました。たとえば、使わずに1つのサーバで2つのDjangoアプリケーションを動かそうとすると、どのくらい手間が減るかイメージしやすそうです。
残念ながら情報量が少なめな気がするので、使う場合には事前検証が重要だと感じました。

今回は触れませんでしたが、vassalに関する設定も共通化することができたり、Tyrantモードなる、セキュアなマルチユーザホスティング機能もあるようですので、Emperorモードには一見の価値はあると思います。