FastAPIをマルチプロセス＆SSLで動かすためのステップバイステップ

Flask並みに簡単なのにモダンな開発スタイルも可能なフレームワーク[FastAPI](https://fastapi.tiangolo.com)を、本番を意識した構成で動かす方法を調べたので共有したいと思います。

# 構成に関する要件と討ち手

「本番を意識した構成」と大きく出てみたものの、やりたいことは以下2点です。

- マルチプロセス構成の管理をしたい 👉 gunicornでマルチプロセス管理
- SSLをオフロード＆証明書を一元管理したい 👉 Nginxでリバースプロキシ

前者によりパフォーマンスや耐障害性を確保し、後者により運用しやすさやセキュリティを確保するようなイメージです。

# Uvicornで起動（基本形）

ASGIサーバーとしてUvicornを使用してFastAPIをシングルプロセスで起動するまでの手順です。はじめにライブラリのインストール。

```bash
$ pip install fastapi uvicorn
```

インストールしたらファイル`run.py`を作成して以下の通りコードを記述してください。

```python:run.py
from fastapi import FastAPI

app = FastAPI()

@app.get('/')
async def hello():
    return {"text": "hello world!"}
```

そしたらサーバーを起動してみましょう。

```bash
$ uvicorn run:app
INFO:     Started server process [13372]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```

http://127.0.0.1:8000 でUvicornが起動したと出てきたのでブラウザでアクセスしてみると以下のようなJSONが返されるはずです。

```json
{"text":"hello world!"}
```

稼働確認はこれで完了です。


# マルチプロセスにする

Uvicorn自体でマルチプロセスの仕組みは持っているのですが、プロセスを監視して落ちたら再起動するといったような仕組みが無いみたいです。

>Uvicorn provides a lightweight way to run multiple worker processes, for example --workers 4, but does not provide any process monitoring.

https://www.uvicorn.org/deployment/#using-a-process-manager

そこで、最もシンプルな方法として示されているGunicornをプロセスマネージャーとして利用する方法を採用してみたいと思います。

## Gunicornのインストール

以下の通り。簡単。

```bash
$ pip install Gunicorn
```

## マルチプロセスで起動

早速Gunicornを使ったマルチプロセス管理のもとで（UvicornをASGIとした）FastAPIアプリを起動します。以下の例ではワーカープロセス4つ、ポート1234番で起動しています。

```bash
$ gunicorn -w 4 -k uvicorn.workers.UvicornWorker run:app --bind localhost:1234 
```

上記コマンドを実行すると、以下のようにワーカープロセスがずらずらと起動してきます。

```bash         
[2020-08-18 19:52:58 +0900] [13404] [INFO] Starting gunicorn 20.0.4
[2020-08-18 19:52:58 +0900] [13404] [INFO] Listening at: http://127.0.0.1:1234 (13404)
[2020-08-18 19:52:58 +0900] [13404] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2020-08-18 19:52:58 +0900] [13407] [INFO] Booting worker with pid: 13407
[2020-08-18 19:52:58 +0900] [13408] [INFO] Booting worker with pid: 13408
[2020-08-18 19:52:58 +0900] [13407] [INFO] Started server process [13407]
[2020-08-18 19:52:58 +0900] [13407] [INFO] Waiting for application startup.
　　：（略）
[2020-08-18 19:52:58 +0900] [13410] [INFO] Waiting for application startup.
[2020-08-18 19:52:58 +0900] [13410] [INFO] Application startup complete.
```

ログが出てくるのが止まった後、http://127.0.0.1:1234 にアクセスしてHello worldが表示されたらGunicorn経由でのアクセスは成功です。


## プロセス管理機能の確認

今回Gunicornを利用するねらいである、プロセス数監視＆復活機能が働いていることを確認してみましょう。例ではワーカーとしてPID13407〜13410が起動したので、別のターミナルを起動してPID13407のプロセスを落としてみます。

```bash
$ kill 13407 -9
```

すると以下のように13407が落ち、ただちに新たなワーカープロセスがPID13456として起動してくる様子が確認できると思います。

```bash
[2020-08-18 20:07:36 +0900] [13407] [INFO] Shutting down
[2020-08-18 20:07:36 +0900] [13407] [INFO] Error while closing socket [Errno 9] Bad file descriptor
[2020-08-18 20:07:36 +0900] [13407] [INFO] Waiting for application shutdown.
[2020-08-18 20:07:36 +0900] [13407] [INFO] Application shutdown complete.
[2020-08-18 20:07:36 +0900] [13407] [INFO] Finished server process [13407]
[2020-08-18 20:07:36 +0900] [13407] [INFO] Worker exiting (pid: 13407)
[2020-08-18 20:07:36 +0900] [13456] [INFO] Booting worker with pid: 13456
[2020-08-18 20:07:37 +0900] [13456] [INFO] Started server process [13456]
[2020-08-18 20:07:37 +0900] [13456] [INFO] Waiting for application startup.
[2020-08-18 20:07:37 +0900] [13456] [INFO] Application startup complete.
```

# Nginxのリバースプロキシ配下にする

Nginxをリバースプロキシとする方法について解説していきます。ここではMacを前提としていますので、Linuxなどを利用する場合はパスやコマンドを置き換えてください。
なおこの記事ではリバースプロキシもWebアプリも同じマシンで動かしますが、実際には異なるマシンになると思いますので、適宜アドレスなどを読み替えてください。

## Nginxのインストール

以下の通りです🍺

```bash
$ brew install nginx
```

インストールしたら起動してみましょう。

```bash
$ sudo nginx
```

http://127.0.0.1:8080/ にアクセスしてNginxのウェルカムページが表示されたらインストールは成功です。


## リバースプロキシの設定

`/usr/local/etc/nginx/servers/fastapitest`を作成して以下の通り定義します。ポート2345番に来たアクセスについて、ルート以下すべてをhttp://127.0.0.1:1234 のプロキシとして振る舞うような内容です。
`proxy_set_header`はセットしてアプリに渡してあげないと、自身のドメインやプロトコルがわからない故に様々な問題が生じます。詳細は[Nginxのドキュメント](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header)などを参照してください。なおこれはNginx固有の話ではなくリバースプロキシを利用する際の一般的な問題です。

```fastapitest
server {
    listen 2345;
    server_name fastapitest;

    proxy_set_header    Host    $host;
    proxy_set_header    X-Forwarded-Host    $host;
    proxy_set_header    X-Forwarded-Proto   $scheme;
    proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    proxy_set_header    X-Real-IP           $remote_addr;

    location / {
        # 別マシンの場合は127.0.0.1ではなくそのマシンのアドレスにする
        proxy_pass  http://127.0.0.1:1234/;
    }
}
```

定義が完了したら、以下のコマンドでNginxの設定をリロードします。

```bash
$ sudo nginx -s reload
```

http://127.0.0.1:2345 にアクセスしてHello worldが表示されたらNginx経由のアクセスは成功です。

# SSLの設定

先ほどのリバースプロキシの定義に以下の通り追加します。証明書の取得手順は割愛します。監視するポート番号を443に変更し、`server_name`を実際のドメイン名に変更します。

```fastapitest
server {
    # 2345の監視をやめて443を監視
    # listen 2345;
    listen 443 ssl;
    # ドメインを指定
    # server_name fastapitest;
    server_name fastapitest.your.domain;

    # SSL関連の設定を追加
    ssl_certificate_key     /path/to/privkey.pem;       # Key
    ssl_certificate         /path/to/fullchain.pem;     # Cert
    ssl_protocols           TLSv1.1 TLSv1.2 TLSv1.3;    # TLS1.1以上
    ssl_ciphers             HIGH:!aNULL:!MD5;           # 暗号化方式。HIGH=128bit長以上とし、MD5は不許可
    ssl_prefer_server_ciphers   on;                     # サーバの設定を優先
    # SSL関連の設定 ここまで

    proxy_set_header    Host    $host;
    proxy_set_header    X-Forwarded-Host    $host;
    proxy_set_header    X-Forwarded-Proto   $scheme;
    proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    proxy_set_header    X-Real-IP           $remote_addr;

    location / {
        # 別マシンの場合は127.0.0.1ではなくそのマシンのアドレスにする
        proxy_pass  http://127.0.0.1:1234/;
    }
}
```

なお`ssl_ciphers`の値は[Nginxのデフォルト値](http://nginx.org/en/docs/http/configuring_https_servers.html)なので指定しなくても同じですが、設定余地として明示したものです。

定義を修正したら、`sudo nginx -s reload`でリロードしてhttps://fastapitest.your.domain/ にアクセスしてHello worldが表示されたらSSLオフロードも成功です。


## おまけ：Unixドメインソケットで接続

わざわざリバースプロキシをSSLオフロード用に構えたのに同一マシン内のGunicornに接続することもないと思いますが、いつか役に立つかもしれないのでメモしておきます。

まずはTCP通信ではなくUnixドメインソケットにバインドしてGunicornを起動しなおします。

```bash
$ gunicorn -w 4 -k uvicorn.workers.UvicornWorker run:app --bind unix:/tmp/myuvicorn.sock
```

続いてNginxの設定変更です。WSGIであれば`location`ディレクティブに`uwsgi_pass`としてUnixドメインソケットのパスを設定すればOKでしたが、ASGIの場合はソケットを含む`upstream`ディレクティブでサーバーグループを定義し、これを`proxy_pass`ディレクティブで指定する必要があるようです。

```
     ：（略）
    location / {
        # proxy_pass  http://127.0.0.1:1234/;
        proxy_pass   http://uvicorntest;
    }
}

upstream uvicorntest {
    server  unix:/tmp/myuvicorn.sock;
}
```

設定リロード後にアクセスしてHello worldが表示されたら成功です。


# さいごに

私はFlask派でしたが、FastAPIを触ってみてのファーストインプレッションが良かったので徐々に軸足を移して行こうと思っています。
というわけで私もまだFastAPI素人・Uvicorn素人なので、「ここちがってるよー」みたいなのがあれば是非是非ご指摘いただけるとうれしいです🙏