SystemdによるPython Webサーバーのデーモン化

# 想定されるシチュエーション

`Node.js` であればPM2などのサービスで簡単にデーモン化できますが、今回は `Django` や `Flask` などのPython Webフレームワークを使ったアプリ開発で、`hypercorn` などのPython Webサーバーをデーモン化するための手順です。

### 想定しているサービス

- Hypercorn / Uvicorn / Gunicorn
- Tornado

### 基本的な流れ

1. .systemファイル作成
1. スタートアップ用のシェルスクリプト作成
1. サービスの登録・起動

# デーモン化

基本的な流れと、`hypercorn` などのサーバーと若干違う部分がある `Tornado` の設定に分けて記述します。

### .systemファイル

`/etc/systemd/system` に `<YOUR_SERVICE>.service` というファイルを以下の要領で作成します。要所の説明はコメントとして記述しています。また、設定は必要に応じて変更してください。

```
[Unit]
# 何のサービスなのか分かるように記述
Description = <ABOUT_YOUR_SERVICE>

[Service]
# サーバーがあるディレクトリへのフルパス
WorkingDirectory=<PATH_TO_WORKING_DIR>

# サーバー起動用シェルへのフルパス
ExecStart="<PATH_TO_SHELL_SCRIPT>"

Restart=always
RestartSec=3s # これを省略するとリトライが早すぎてエラーになる
Type=simple

# サービスを起動するユーザーとグループ名
# root権限のあるグループは避ける
User=<USER_NAME>
Group=<GROUP_NAME>

[Install]
WantedBy=multi-user.target
```

保存したら忘れずにシステムデーモンをリロードしてください。

```
$ sudo systemctl daemon-reload
```

### シェルスクリプトの作成

Pythonの場合は仮想環境を構築して開発している場合が多いので、.systemファイルに直接起動コマンドを記述しても、仮想環境が反映されないため、エラーで起動しません。これを回避するため、起動用のシェルスクリプトを作成して対応します。記述方法は簡単で、基本は以下のような記述になります。（ `app.py` の中の `app` で定義したアプリケーションを起動）

```
#!/usr/bin/bash
python hypercorn app:app
```

`python hypercorn app:app` の部分は `--reload` など適宜変更して必要な起動コマンドにしてください。

ただ、このままでは複数のサービスをポートを分けて起動している場合など、色々と使いにくいので、メンテナンスを考えて、例えば以下のようなスクリプトにしていきます。

```
#!/usr/bin/bash

# アプリのホームをフルパスで指定
# `pwd`でカレントディレクトリ
HOME_DIR="<PATH_TO_HOME_DIR>"

# hypercornなどへのHOME_DIRからのパス
# $HOME_DIRで参照
SERVER="<PATH_TO_SERVER_APP>"

# アプリケーションの指定（）
APP="app:app"

# バインド情報
HOST_NAME="localhost"
PORT=8000

# その他変数は適宜設定
WORKERS=2

# 起動コマンド
# この例では hypercorn x 2 で localhost:8000 & リロードオプションで起動
echo "Starting <YOUR_APP>..."
$API_HOME$SERVER -w2 $HOME_DIR$APP -w $WORKERS -b $HOST_NAME:$PORT --reload
```

この例ではワーカー数を２に設定していますが、一つ落ちた場合でも、デーモンが再起動するまで３秒のスリープを取ってあるので、サービスが停止しないような設定になっています。ワーカーの数が多ければ多いほどサービスは停止しにくくなりますが、起動数に比例してメモリの消費が増えてきます。

### システムの起動と登録

シェルスクリプトが完成したら、ユーザーとグループに対して実行の許可を与えておきます。今回は0750にしてありますが、ここは適宜調整してください。

```
$ chmod 0750 ./server.sh
```

次に、システムデーモンから起動するかどうかを確かめます。

```
$ sudo systemctl start <YOUR_SERVICE>
$ systemctl status <YOUR_SERVICE>
```

これでデーモンの起動や停止は `systemctl [start|stop|restart] <YOUR_SERVICE>` で管理することができます。起動が確認できたら、システムを登録して自動的に起動するように設定して完了です！

```
$ sudo systemctl enable <YOUR_SERVICE>
```

システムの自動起動を解除したい場合は以下のコマンドで解除できます。

```
$ sudo systemctl disable <YOUR_SERVICE>
```

### Tornado

`Hypercorn/Uvicorn/Gunicorn` に一手間かかるので、`Tornado` のリロード設定もついでにここで書いておきます。基本的にはプログラムの中の `tornado.web.Application` に `autoreload=True` を設定して、`--autoreload` オプションをつけてサーバーを起動することになります。 実際には `debug=True` も同時に設定することが多いと思いますので、まずは以下の感じで `app.py` を記述します。

```
...中略...
app = tornado.web.Application(
    [
        (r"/", MainHandler),
        ...中略...
    ],
    debug=True,
    autoreload=True,
)
...中略...
```

リロードを有効にするためには、以下のようにしてこのアプリを起動する必要があります。

```
python -m tornado.autoreload app.py
```

デーモン化では、これに応じたシェルスクリプトを書くことになりますが、ポートの設定などは以下のようにプログラムの中にコーディングしておきます。（`hypercorn` などでも同様の設定も可能です）

```
if __name__ == "__main__":
    _port = <PORT_NUMBER>
    print("\nStarting up <YOUR_APP>...")
    if app.listen(_port):
        print(f"Started at PORT {_port}!\n")
        tornado.ioloop.IOLoop.instance().start()
    else:
        print("Failed startup...\n")
```

この例では、`localhost:<PORT_NUMBER>` で起動起動する設定になっています。このプログラムをデーモンとして起動するシェルスクリプトは、例えば以下のように書くことができます。

```
#!/usr/bin/bash

# Pythonへのフルパス
PYTHON_BIN=<PATH_TO_PYTHON>

# アプリケーションへのパス（シェルスクリプトからの相対パスでOK）
APP_NAME=<PATH_TO_APP>

# 起動コマンド
$PYTHON -m tornado.autoreload $APP_NAME
```

ファイルが揃ったら、他と同じように実行権限を付与して、システムデーモンで起動と登録を行って完了です！