本記事では、備忘録のための車輪の再開発をしつつ、Gunicornによるプロセス監視など使ったことがなかったパッケージも使って、FastAPIを動かしてみようと思います。
├ backend |
│ ├ router |
│ │ ├ __init__.py |
│ │ └ main.py |
│ ├ schemas |
│ │ ├ __init__.py |
│ │ └ main.py |
│ ├ gunicorn.conf.py |
│ └ main.py |
├ docker |
│ ├ DockerFile |
│ ├ Pipfile |
│ └ Pipfile.lock |
├ scripts |
│ └ make_key.sh |
├ logs |
├ docker-compose.override.yml |
├ docker-compose.prod.yml |
└ docker-compose.yml |
[[source]] |
url = "https://pypi.org/simple" |
verify_ssl = true |
name = "pypi" |
[packages] |
fastapi = "==0.63.0" |
uvicorn = {extras = ["standard"], version = "==0.13.4"} |
Gunicorn = "==20.0.4" |
orjson = "==3.5.1" |
python-multipart = "==0.0.5" |
aiofiles = "==0.6.0" |
[dev-packages] |
pytest = "==6.2.2" |
pytest-asyncio = "==0.14.0" |
requests = "==2.25.1" |
[requires] |
python_version = "3.8" |
パッケージのインストールにはpipenvを使用しました。
FastAPI
を動かすのに最低限必要なのはfastapi, uvicorn
です。 uvicorn
はstandard
でインストールしないと、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.
下記パッケージは本記事では使用していませんが、利用することが多いので、ついでにインストールしています。
python-multipart
- ファイルのアップロードに必要
aiofiles
- ファイルのダウンロードに必要
pytest
requests
- APIテストに必要
- 参考: FatAPI – Testing
pytest-asyncio
- 非同期のテストに必要
FROM python:3.8-slim as base |
WORKDIR /usr/src/fastapi_sample |
COPY docker/Pipfile . |
RUN pip install pipenv |
# 開発用 |
FROM base as dev |
RUN pipenv install --dev |
# 本番用 |
FROM base as prod |
RUN pipenv install |
COPY backend backend |
本番用と開発用にマルチステージビルドを分けています。
本番と開発で分けると、ポート番号や環境変数などの設定の違いがあるときや、テスト時にしか使わないパッケージを除く場合に便利です。 また、パッケージのダウンロードやインストール時に残った不要なファイルの削除もできます。
- 開発用
- ソースコードを永続ボリュームで共有
- 後述のdocker-compose.override.yml にて設定
- ソースコードを永続ボリュームで共有
- ソースコードをイメージにCOPY
本番用はソースコードをイメージに埋め込むことが多いですが、開発時は埋め込みだとコードの変更があるたびにイメージを作り直さなくて不便なので、開発時は永続ボリュームを利用しています。
version: '3.9' |
services: |
fastapi-sample: |
build: |
context: ./ |
dockerfile: docker/Dockerfile |
image: fastapi-sample |
working_dir: /usr/src/fastapi_sample |
command: > |
pipenv run gunicorn backend.main:app |
--config './backend/gunicorn.conf.py' |
volumes: |
- ./logs:/usr/src/fastapi_sample/logs |
environment: |
- TZ=Asia/Tokyo |
restart: always |
secrets: |
- source: ssh_key |
- source: ssh_crt |
secrets: |
ssh_key: |
file: ./scripts/server.key |
ssh_crt: |
file: ./scripts/server.crt |
永続ボリュームの logs は、Gunicorn で設定するアクセスログとエラーログを配置する場所です。
httpsで起動させるために必要な鍵と証明書は、secrets機能を使って読み込ませています。 鍵と証明書は後述するシェルで作成しています。
docker-compose.yml
の他に本番用と開発用を分けるため、ふたつのファイルを作成しています。
- 開発用
docker-compose.override.yml
docker-compose.prod.yml
ポート番号など開発時と本番時に設定が異なる場合に利用します。
docker-compose.override.yml
は引数なしで読み込ませることができます。
個人的にコマンドを打つ機会が多い開発用にdocker-compose.override.yml
を設定しましたが、環境によっては本番用にdocker-compose.override.yml
を設定してもいいとともいます。
version: '3.9' |
services: |
fastapi-sample: |
build: |
target: dev |
volumes: |
- ./backend:/usr/src/fastapi_sample/backend |
ports: |
- 8080:8080 |
environment: |
- PORT=8080 |
docker-compose.override.yml
はデフォルトで読み込まむため、特にdocker-compose.override.yml
をコマンドで指定しなくても読み込ませることができます。
# イメージ作成 |
docker-compose build |
# コンテナ立ち上げ |
docker-compose up -d |
# コンテナ落とす |
docker-compose down -v |
version: '3.9' |
services: |
fastapi-sample: |
build: |
target: prod |
ports: |
- "8080" |
environment: |
- PORT=8080 |
開発用のdocker-compose.override.yml
を読み込ませず、任意のファイルを読み込ますには-f
オプションを使います。
# イメージ作成 |
docker-compose -f docker-compose.yml -f docker-compose.prod.yml build |
# コンテナ立ち上げ |
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d |
# コンテナ立ち上げ |
docker-compose -f docker-compose.yml -f docker-compose.prod.yml down -v |
import os |
# Server Socket |
bind = '0.0.0.0:' + os.getenv('PORT') |
# Worker Processes |
workers = 2 * os.cpu_count() + 1 |
threads = 2 * os.cpu_count() + 1 |
worker_class = 'uvicorn.workers.UvicornWorker' |
# Debugging |
reload = True |
# SSL |
keyfile = '/run/secrets/ssh_key' |
certfile = '/run/secrets/ssh_crt' |
ciphers = 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256' |
# Logging |
accesslog = './logs/access.log' |
errorlog = './logs/error.log' |
loglevel = 'info' |
ワーカー数とスレッド数は、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.
docker-compose.yml
のsecretsにて設定した鍵と証明書はここで設定しています。
secretsで設定すると/run/secrets/
に配置され、名前はホスト側の名前ではなく、docker-compose.yml
で記載した名称で配置されます。
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)は除いています。
cd `dirname $0` |
COUNTRY=JP |
STATE=Tokyo |
LOCATION=Minato-Ku |
ORGANIZATION=Cresco |
ORGANIZATION_UNIT=DataTechnorogyCenter |
COMMON_NAME=ShoYOSHINO |
KEY_PATH=server.key |
CSR_PATH=server.csr |
SAN_PATH=san.txt |
CRT_PATH=server.crt |
IP_ADDR=`curl -s ifconfig.me` |
CRT_DAYS=825 |
openssl ecparam -name prime256v1 -genkey -out ${KEY_PATH} |
openssl req -new -sha256 -key ${KEY_PATH} -out ${CSR_PATH} \ |
-subj "/C=${COUNTRY}/ST=${STATE}/L=${LOCATION}/O=${ORGANIZATION}/OU=${ORGANIZATION_UNIT}/CN=${COMMON_NAME}" |
echo subjectAltName=IP.1:${IP_ADDR} > ${SAN_PATH} |
openssl x509 -req -days ${CRT_DAYS} -in ${CSR_PATH} -signkey ${KEY_PATH} -out ${CRT_PATH} -extfile ${SAN_PATH} |
chmod 600 ${KEY_PATH} |
chmod 600 ${CRT_PATH} |
Chrome58以降、SAN (Subject Alternative Name) 拡張領域を設定しないと、エラーになるので、subjectAltName=
に実行しているサーバーのIPアドレスを設定しています。(参考:Chrome58になると自己署名の証明書がエラーになるので、OpenSSLに詳しくなった話)
また、iOS 13 および macOS 10.15 における信頼済み証明書の要件によると、iOS 13 および macOS 10.15以降で、2019年7月1日以降に発行される証明書の発行期限825日以下でないといけないそうなので、期限を825日に設定しています。
import sys |
from fastapi import FastAPI |
from fastapi.middleware.cors import CORSMiddleware |
from fastapi.responses import ORJSONResponse |
sys.path.append('../') |
import backend.schemas.main as schemas |
app = FastAPI() |
app.add_middleware( |
CORSMiddleware, |
allow_origins=['*'], |
allow_credentials=True, |
allow_methods=['*'], |
allow_headers=['*'], |
) |
@app.get( |
'/', |
response_class=ORJSONResponse, |
response_model=schemas.MainResponse, |
summary='main get', |
tags=['main'] |
) |
async def main(): |
return {'message': 'Hello World'} |
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をまとめることができます。
APIで返すレスポンスの形を定義できます。
定義することで、後述するAPIドキュメントに定義が反映されたり、定義以外で返却しようとしたときバリデーションエラーになるように設定されます。
from pydantic import BaseModel |
class MainResponse(BaseModel): |
message: str |
FastAPIでは、APIサーバーを立ち上げると、ドキュメントも自動で2種類作成されます。
https://IPアドレス:ポート番号/docs
- Swagger UI形式
https://IPアドレス:ポート番号/redoc
- ReDoc形式
前述したレスポンスの定義をきちんと定義すると、APIドキュメントにも自動で反映されて、とても便利です。
再開発してみると、案外忘れてたり、よくドキュメント読んでいなかったり、ポカミスも多かったりで、非常に勉強になりました。。。 また、新たにFastAPIを調べてみて、1年以上前とくらべて、FastAPIの記事が世の中に量産されてて非常に嬉しい限りです。
今回はテストやDBといった事項までいかなかったので、次の機会に書こうと思います