docker-composeにてFastAPIをHTTPSで動かしてみる

データテクノロジーセンター 兼 3D円グラフ撲滅委員吉野祥 です。

Dockerによる設定は一度設定が完了して、しばらく触らないと、なぜそのように設定したのかを個人的に忘れがちになります。

本記事では、備忘録のための車輪の再開発をしつつ、Gunicornによるプロセス監視など使ったことがなかったパッケージも使って、FastAPIを動かしてみようと思います。

ディレクトリ構成

Pipfile

パッケージのインストールにはpipenvを使用しました。

FastAPIを動かすのに最低限必要なのはfastapi, uvicornです。 uvicornstandardでインストールしないと、gunicornを動かすとき、uvloopがないぞ、と怒られます。

uvicornはワーカー数の指定はできますが、落ちたことを感知して再起動するなどのプロセス監視はできないため、Gunicornをインストールしています。

プロセスを監視するパッケージとして、FastAPI – Using a process managerでは、Gunicorn, Supervisor, Circusによる監視方法を提示しています。

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

Gunicorn is probably the simplest way to run and manage Uvicorn in a production setting. Uvicorn includes a gunicorn worker class that means you can get set up with very little configuration.

orjsonは、より高速にJSONを取り扱うために使用しています。(参考: FastAPI – Custom Response – HTML, Stream, File, others – Use ORJSONResponse)

For example, if you are squeezing performance, you can install and use orjson and set the response to be ORJSONResponse.

下記パッケージは本記事では使用していませんが、利用することが多いので、ついでにインストールしています。

Docker

本番用と開発用にマルチステージビルドを分けています。

本番と開発で分けると、ポート番号や環境変数などの設定の違いがあるときや、テスト時にしか使わないパッケージを除く場合に便利です。 また、パッケージのダウンロードやインストール時に残った不要なファイルの削除もできます。

  • 開発用
    • ソースコードを永続ボリュームで共有
      • 後述のdocker-compose.override.ymlにて設定
    • テストパッケージインストール
  • 本番用
    • ソースコードをイメージにCOPY

本番用はソースコードをイメージに埋め込むことが多いですが、開発時は埋め込みだとコードの変更があるたびにイメージを作り直さなくて不便なので、開発時は永続ボリュームを利用しています。

docker-compose.yml

永続ボリュームの logs は、Gunicorn で設定するアクセスログとエラーログを配置する場所です。

httpsで起動させるために必要な鍵と証明書は、secrets機能を使って読み込ませています。 鍵と証明書は後述するシェルで作成しています。

開発時と本番時のdocker-compose

docker-compose.ymlの他に本番用と開発用を分けるため、ふたつのファイルを作成しています。

  • 開発用
    • docker-compose.override.yml
    • 引数なしで読み込み可能
  • 本番用
    • docker-compose.prod.yml

ポート番号など開発時と本番時に設定が異なる場合に利用します。

docker-compose.override.ymlは引数なしで読み込ませることができます。

個人的にコマンドを打つ機会が多い開発用にdocker-compose.override.ymlを設定しましたが、環境によっては本番用にdocker-compose.override.ymlを設定してもいいとともいます。

開発用 docker-compose.override.yml

docker-compose.override.ymlはデフォルトで読み込まむため、特にdocker-compose.override.ymlをコマンドで指定しなくても読み込ませることができます。

本番用 docker-compose.prod.yml

開発用のdocker-compose.override.ymlを読み込ませず、任意のファイルを読み込ますには-fオプションを使います。

Gunicorn設定 (backend/gunicorn.conf.py)

ワーカー数とスレッド数

ワーカー数とスレッド数は、Better performance by optimizing Gunicorn configを参考にすると、CPUの数 * 2 + 1 が最適とのことなので、2 * os.cpu_count() + 1を設定しています。

Each of the workers is a UNIX process that loads the Python application. There is no shared memory between the workers. The suggested number of workers is (2*CPU)+1.

The suggested maximum concurrent requests when using workers and threads is still(2*CPU)+1.

secrets

docker-compose.ymlのsecretsにて設定した鍵と証明書はここで設定しています。

secretsで設定すると/run/secrets/に配置され、名前はホスト側の名前ではなく、docker-compose.ymlで記載した名称で配置されます。

暗号スイート (Cipher suite)

Gunicorn – settings – SSLによると、python3.6以降において、デフォルトだと、クライアントサーバー間で最も高いバージョンのTLSを使うそうです。

Negotiate highest possible version between client/server. Can yield SSL. (Python 3.6+)

TLSv1.2が使えない場合、TLSv1.1を使うといったことがありそうなので、暗号スイートを明示的にしています。

OWASPからTLS Cipher String Cheat SheetのCipher-String Aを参考に設定しました。 Racoon Attack対策から、鍵交換がDHEの暗号スイート(暗号スイートの頭がDHE)は除いています。

自己証明書(オレオレ証明書)の作成

Chrome58以降、SAN (Subject Alternative Name) 拡張領域を設定しないと、エラーになるので、subjectAltName=に実行しているサーバーのIPアドレスを設定しています。(参考:Chrome58になると自己署名の証明書がエラーになるので、OpenSSLに詳しくなった話)

また、iOS 13 および macOS 10.15 における信頼済み証明書の要件によると、iOS 13 および macOS 10.15以降で、2019年7月1日以降に発行される証明書の発行期限825日以下でないといけないそうなので、期限を825日に設定しています。

メインプログラム (backend/main.py)

FastAPIを起動するためのメインプログラムです。

CORSMiddlewareは、CORSへの対応です。参考 : FastAPI – CORS (Cross-Origin Resource Sharing)

上記で設定したAPIでは@app.getとしているのでGETメソッドです。 メソッドに応じて、後ろの単語が変わります。

  • @app.get
  • @app.post
  • @app.patch
  • @app.delete

@app.getの引数では、'/' としているので、https://IPアドレス:ポート番号 にGETリクエストを投げると、レスポンスで、{'message': 'Hello World'}が返ってきます。 もし'/sample' としていれば、アクセス先は、https://IPアドレス:ポート番号/sample になります。

response_class=ORJSONResponseは、前述したJSONを高速に取り扱うためのパッケージorjsonを使いますという宣言です。

response_model=schemas.MainResponse,は、レスポンスの形を定義しています。 定義しなくても動きますが、レスポンス定義のAPIドキュメントへの反映や予期していないレスポンスへのバリデーションエラーを設定できます。

summary='main get'は、記載したAPIへの説明です。 APIごとに設定できます。

tags=['main']は、APIごとに設定できるタグです。 後述するAPIドキュメントにおいて、同じタグでAPIをまとめることができます。

Response (backend/schema/main.py)

APIで返すレスポンスの形を定義できます。

定義することで、後述するAPIドキュメントに定義が反映されたり、定義以外で返却しようとしたときバリデーションエラーになるように設定されます。

APIドキュメント

FastAPIでは、APIサーバーを立ち上げると、ドキュメントも自動で2種類作成されます。

  • https://IPアドレス:ポート番号/docs
    • Swagger UI形式
  • https://IPアドレス:ポート番号/redoc
    • ReDoc形式

前述したレスポンスの定義をきちんと定義すると、APIドキュメントにも自動で反映されて、とても便利です。

おわりに

再開発してみると、案外忘れてたり、よくドキュメント読んでいなかったり、ポカミスも多かったりで、非常に勉強になりました。。。 また、新たにFastAPIを調べてみて、1年以上前とくらべて、FastAPIの記事が世の中に量産されてて非常に嬉しい限りです。

今回はテストやDBといった事項までいかなかったので、次の機会に書こうと思います

  • このエントリーをはてなブックマークに追加