FastAPI、uvicorn + gunicorn で デーモン化したWebサーバーの起動、確認、停止方法

# 経緯

- 1つのAWSインスタンスで、複数のgunicornプロセスを起動することを検討している
- 間違えて、他のプロセスを停止しないように、よい方法を模索している

# 環境

- AWS
- Ubuntu 20.04.2 LTS
- Python 3.8.10 (venvで環境構築)

```
uvicorn==0.14.0
gunicorn==20.1.0
setproctitle==1.2.2
fastapi==0.65.2
```

# 用語の整理

## Uvicornとは

> Uvicorn is a lightning-fast ASGI server implementation, using uvloop and httptools.
> *2より抜粋

- ASGI server の実装

## ASGI とは

> ASGI (Asynchronous Server Gateway Interface) is a spiritual successor to WSGI,
> *5より抜粋

- WSGI の 精神的続編 (*6 より)

## WSGI とは
 
> The Web Server Gateway Interface (WSGI, pronounced whiskey[1][2] or WIZ-ghee[3]) is a simple calling convention for web servers
> to forward requests to web applications or frameworks written in the Python programming language. 
> *4より抜粋

- Pythonで書かれたWebアプリケーションに、フォワードするWebサーバーのインターフェース定義
- ウィスキーと発音することがあるみたいだ (英語の解説動画を見てるとウィスキーと聞こえる)

## Uvicornとは (2回目)

- Pythonで書かれたWebアプリケーションに、フォワードするWebサーバーのためのインターフェース定義の
- 精神的続編である ASGIを実装したサーバー
- 正確な表現ではないが、PythonでWebアプリを作って、Webサーバー化してくれる、と考えればよいだろうか


## gunicorn とは

> Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. 
> *7 より抜粋

> Gunicorn is a mature, fully featured server and process manager.
> 
> Uvicorn includes a Gunicorn worker class allowing you to run ASGI applications, 
> with all of Uvicorn's performance benefits, while also giving you 
> unicorn's fully-featured process management.
> *2 より抜粋

- gunicornは、 WSGIのHTTPサーバー
- バージョン番号からも伺えるが、Gunicornは成熟している
- UvicornWorkerをgunicornで使えば、Uvicornのパフォーマンスの恩恵を受けつつ、gunicornのプロセス管理などの機能が使える、といったところだろうか


# Uvicorn のインストールと Hello, world

## Uvicorn のインストール
```
pip install uvicorn
```

# Uvicorn の Hello, world

- uvicornを使ってWebサーバーを起動して、
- HTTPリクエストに対して、
- Hello, world と 環境変数 を返却する簡単な例

```python:main.py
import uvicorn
import os

async def app(scope, receive, send):
    assert scope['type'] == 'http'

    mode = 'DEBUG'
    if ('MODE' in os.environ.keys()):
        mode = os.environ['MODE']

    body = (f'Hello, world (mode:{mode})\n').encode()

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': body,
    })

if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=8080, log_level="info")

```

## unicornで、Webサーバーを起動

- 以下のようにすると、フォアグランドで実行される
- （nohup などを使って、バックグランド実行にすることもできる）

```bash
python main.py

INFO:     Started server process [49698]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)
INFO:     127.0.0.1:45258 - "GET / HTTP/1.1" 200 OK
```

### curlで動作確認

- 環境変数が設定されていないので、モードがDEBUGと表示されることを確認

```
curl http://127.0.0.1:8080/

Hello, world (mode:DEBUG)
```

# gunicornの活用

## gunicorn のインストール
```
pip install gunicorn
```

## gunicorn の起動

- 先ほど作った Pythonのファイル名 main と 関数名appをgunicornのコマンドの引数に指定する
- gunicornのコマンドオプションで各種設定が行える
- ここでは、-c コマンドオプションのみを指定
- -c を使うと、設定を外部ファイル（gunicorn_config.pyなど）に記述できる

```bash
# mainはpythonのファイル名、appは関数名

gunicorn -c gunicorn_config_prod.py main:app
```

### gunicornの設定

- ワーカー指定、デーモン化、プロセスIDをファイルに保存、プロセス名の指定ができる
- （このあたりが、gunicornの特徴と考えてよいだろうか）
- 設定の詳細は、*3が参考になる

```python:gunicorn_config_prod.py

# 実行するPythonがあるパス
pythonpath = './'

# ワーカー数
workers = 2

# ワーカーのクラス、*2 にあるようにUvicornWorkerを指定 (Uvicornがインストールされている必要がある)
worker_class = 'uvicorn.workers.UvicornWorker'

# IPアドレスとポート
bind = '127.0.0.1:8080'

# プロセスIDを保存するファイル名
pidfile = 'prod.pid'

# Pythonアプリに渡す環境変数
raw_env = ['MODE=PROD']

# デーモン化する場合はTrue
daemon = True

# エラーログ
errorlog = './logs/error_log.txt'

# プロセスの名前
proc_name = 'my_python_app'

# アクセスログ
accesslog = './logs/access_log.txt'
```
### 動作確認

- 設定した環境変数 PROD が表示されることを確認

```
curl http://127.0.0.1:8080/
Hello, world (mode:PROD)
```

## gunicornのプロセスの確認

### プロセス名を変更するためのパッケージをインストールする

```bash
pip install setproctitle
```

### psコマンドでプロセスを確認

- proc_name を設定しておくと、grepしやすい、みやすい
- プロセス名を変更していない場合は、「gunicornという文字列」と「指定したport番号」などでgrepすればよさそう
- （コマンドオプションでport番号を指定しておく必要があるが）
- 以下のpsコマンドの実行で、masterプロセスが1つ、workerプロセスが2つあることが確認できる
- gunicornの設定ファイルで指定したmy_python_appという名前が表示されることを確認

```bash
ps aux | grep my_python_app | grep -v grep

ubuntu     46752  0.1  2.2  33040 22608 ?        S    17:41   0:00 gunicorn: master [my_python_app]
ubuntu     46754  0.2  2.2  33512 22804 ?        S    17:41   0:00 gunicorn: worker [my_python_app]
ubuntu     46755  0.1  2.2  33512 22808 ?        S    17:41   0:00 gunicorn: worker [my_python_app]
```

## gunicorn でプロセスIDをファイルに保存

- 「pidfile」項目でファイル名のファイルにPIDが出力される
- masterプロセスのPIDが保存されるようだ

```bash:prod.pid
cat prod.pid

46752
```

- errorlog のINFOレベルのログでもプロセスIDを確認できそうだ

```
[2021-07-01 17:41:23 +0900] [46752] [INFO] Listening at: http://127.0.0.1:8080 (46752)
[2021-07-01 17:41:23 +0900] [46752] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2021-07-01 17:41:23 +0900] [46754] [INFO] Booting worker with pid: 46754
[2021-07-01 17:41:23 +0900] [46754] [INFO] Started server process [46754]
[2021-07-01 17:41:23 +0900] [46754] [INFO] Waiting for application startup.
[2021-07-01 17:41:23 +0900] [46754] [INFO] Application startup complete.
[2021-07-01 17:41:23 +0900] [46755] [INFO] Booting worker with pid: 46755
[2021-07-01 17:41:23 +0900] [46755] [INFO] Started server process [46755]
[2021-07-01 17:41:23 +0900] [46755] [INFO] Waiting for application startup.
[2021-07-01 17:41:23 +0900] [46755] [INFO] Application startup complete.
```

## gunicorn のプロセスの停止

- master プロセスを停止すれば、workerプロセスも停止するようだ

### デーモン化したプロセスを停止するコマンド例

```bash
kill `cat prod.pid`
```

# FastAPI

- FastAPIを使った場合も同様に実行できた

## FastAPIのインストール

```bash
pip install FastAPI
```

```python:main2.py
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

if __name__ == "__main__":
    uvicorn.run("main2:app", host="127.0.0.1", port=8080, log_level="info")

```

```
gunicorn -c gunicorn_config_prod.py main2:app

curl http://127.0.0.1:8080/
{"Hello":"World"}
```

# 参考資料

- *1 https://stackoverflow.com/questions/14604653/how-to-stop-gunicorn-properly
- *2 https://www.uvicorn.org/
- *3 https://docs.gunicorn.org/en/stable/settings.html
- *4 https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface
- *5 https://asgi.readthedocs.io/en/latest/
- *6 https://ja.wikipedia.org/wiki/%E7%B2%BE%E7%A5%9E%E7%9A%84%E7%B6%9A%E7%B7%A8
- *7 https://gunicorn.org/
- *8 https://fastapi.tiangolo.com/