コンテナ内のFastAPI - Docker¶
FastAPIアプリケーションをデプロイする場合、一般的なアプローチは**Linuxコンテナ・イメージ**をビルドすることです。
基本的には Dockerを用いて行われます。生成されたコンテナ・イメージは、いくつかの方法のいずれかでデプロイできます。
Linuxコンテナの使用には、セキュリティ、反復可能性(レプリカビリティ)、**シンプリシティ**など、いくつかの利点があります。
Tip
TODO: なぜか遷移できない
お急ぎで、すでにこれらの情報をご存じですか? 以下のDockerfile
の箇所👇へジャンプしてください。
Dockerfile プレビュー 👀
FROM python:3.9
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
# If running behind a proxy like Nginx or Traefik add --proxy-headers
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]
コンテナとは何か¶
コンテナ(主にLinuxコンテナ)は、同じシステム内の他のコンテナ(他のアプリケーションやコンポーネント)から隔離された状態を保ちながら、すべての依存関係や必要なファイルを含むアプリケーションをパッケージ化する非常に**軽量**な方法です。
Linuxコンテナは、ホスト(マシン、仮想マシン、クラウドサーバーなど)の同じLinuxカーネルを使用して実行されます。これは、(OS全体をエミュレートする完全な仮想マシンと比べて)非常に軽量であることを意味します。
このように、コンテナは**リソースをほとんど消費しません**が、プロセスを直接実行するのに匹敵する量です(仮想マシンはもっと消費します)。
コンテナはまた、独自の**分離された**実行プロセス(通常は1つのプロセスのみ)や、ファイルシステム、ネットワークを持ちます。 このことはデプロイ、セキュリティ、開発などを簡素化させます。
コンテナ・イメージとは何か¶
**コンテナ**は、**コンテナ・イメージ**から実行されます。
コンテナ・イメージは、コンテナ内に存在すべきすべてのファイルや環境変数、そしてデフォルトのコマンド/プログラムを**静的に**バージョン化したものです。 ここでの**静的**とは、コンテナ**イメージ**は実行されておらず、パッケージ化されたファイルとメタデータのみであることを意味します。
保存された静的コンテンツである「コンテナイメージ」とは対照的に、「コンテナ」は通常、実行中のインスタンス、つまり**実行**されているものを指します。
**コンテナ**が起動され実行されるとき(**コンテナイメージ**から起動されるとき)、ファイルや環境変数などが作成されたり変更されたりする可能性があります。
これらの変更はそのコンテナ内にのみ存在しますが、基盤となるコンテナ・イメージには残りません(ディスクに保存されません)。
コンテナイメージは プログラム ファイルやその内容、例えば python
と main.py
ファイルに匹敵します。
そして、**コンテナ**自体は(**コンテナイメージ**とは対照的に)イメージをもとにした実際の実行中のインスタンスであり、**プロセス**に匹敵します。
実際、コンテナが実行されているのは、**プロセスが実行されている**ときだけです(通常は単一のプロセスだけです)。 コンテナ内で実行中のプロセスがない場合、コンテナは停止します。
コンテナ・イメージ¶
Dockerは、**コンテナ・イメージ**と**コンテナ**を作成・管理するための主要なツールの1つです。
そして、DockerにはDockerイメージ(コンテナ)を共有するDocker Hubというものがあります。
Docker Hubは 多くのツールや環境、データベース、アプリケーションに対応している予め作成された**公式のコンテナ・イメージ**をパブリックに提供しています。
例えば、公式イメージの1つにPython Imageがあります。
その他にも、データベースなどさまざまなイメージがあります:
- PostgreSQL
- MySQL
- MongoDB
- Redis, etc.
予め作成されたコンテナ・イメージを使用することで、異なるツールを**組み合わせて**使用することが非常に簡単になります。例えば、新しいデータベースを試す場合に特に便利です。ほとんどの場合、**公式イメージ**を使い、環境変数で設定するだけで良いです。
そうすれば多くの場合、コンテナとDockerについて学び、その知識をさまざまなツールやコンポーネントによって再利用することができます。
つまり、データベース、Pythonアプリケーション、Reactフロントエンド・アプリケーションを備えたウェブ・サーバーなど、さまざまなものを**複数のコンテナ**で実行し、それらを内部ネットワーク経由で接続します。
すべてのコンテナ管理システム(DockerやKubernetesなど)には、こうしたネットワーキング機能が統合されています。
コンテナとプロセス¶
通常、**コンテナ・イメージ**はそのメタデータに**コンテナ**の起動時に実行されるデフォルトのプログラムまたはコマンドと、そのプログラムに渡されるパラメータを含みます。コマンドラインでの操作とよく似ています。
**コンテナ**が起動されると、そのコマンド/プログラムが実行されます(ただし、別のコマンド/プログラムをオーバーライドして実行させることもできます)。
コンテナは、メイン・プロセス(コマンドまたはプログラム)が実行されている限り実行されます。
コンテナは通常**1つのプロセス**を持ちますが、メイン・プロセスからサブ・プロセスを起動することも可能で、そうすれば同じコンテナ内に**複数のプロセス**を持つことになります。
しかし、**少なくとも1つの実行中のプロセス**がなければ、実行中のコンテナを持つことはできないです。メイン・プロセスが停止すれば、コンテナも停止します。
Build a Docker Image for FastAPI¶
ということで、何か作りましょう!🚀
FastAPI用の**Dockerイメージ**を、**公式Python**イメージに基づいて**ゼロから**ビルドする方法をお見せします。
これは**ほとんどの場合**にやりたいことです。例えば:
- **Kubernetes**または同様のツールを使用する場合
- **Raspberry Pi**で実行する場合
- コンテナ・イメージを実行してくれるクラウド・サービスなどを利用する場合
パッケージ要件(package requirements)¶
アプリケーションの**パッケージ要件**は通常、何らかのファイルに記述されているはずです。
パッケージ要件は主に**インストール**するために使用するツールに依存するでしょう。
最も一般的な方法は、requirements.txt
ファイルにパッケージ名とそのバージョンを 1 行ずつ書くことです。
もちろん、FastAPI バージョンについてで読んだのと同じアイデアを使用して、バージョンの範囲を設定します。
例えば、requirements.txt
は次のようになります:
fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0
そして通常、例えば pip
を使ってこれらのパッケージの依存関係をインストールします:
$ pip install -r requirements.txt
---> 100%
Successfully installed fastapi pydantic uvicorn
Info
パッケージの依存関係を定義しインストールするためのフォーマットやツールは他にもあります。
Poetryを使った例は、後述するセクションでご紹介します。👇
**FastAPI**コードを作成する¶
app
ディレクトリを作成し、その中に入ります- 空のファイル
__init__.py
を作成します main.py
ファイルを作成します:
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
Dockerfile¶
同じプロジェクト・ディレクトリにDockerfile
というファイルを作成します:
# (1)
FROM python:3.9
# (2)
WORKDIR /code
# (3)
COPY ./requirements.txt /code/requirements.txt
# (4)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# (5)
COPY ./app /code/app
# (6)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
-
公式のPythonベースイメージから始めます
-
現在の作業ディレクトリを
/code
に設定しますここに
requirements.txt
ファイルとapp
ディレクトリを置きます。 -
要件が書かれたファイルを
/code
ディレクトリにコピーします残りのコードではなく、最初に必要なファイルだけをコピーしてください。
このファイルは**頻繁には変更されない**ので、Dockerはこのステップではそれを検知し**キャッシュ**を使用し、次のステップでもキャッシュを有効にします。
-
要件ファイルにあるパッケージの依存関係をインストールします
--no-cache-dir
オプションはダウンロードしたパッケージをローカルに保存しないようにpip
に指示します。これは、同じパッケージをインストールするためにpip
を再度実行する場合にのみ有効ですが、コンテナで作業する場合はそうではないです。Note
--no-cache-dir
はpip
に関連しているだけで、Dockerやコンテナとは何の関係もないです。--upgrade
オプションは、パッケージが既にインストールされている場合、pip
にアップグレードするように指示します。何故ならファイルをコピーする前のステップは**Dockerキャッシュ**によって検出される可能性があるためであり、このステップも利用可能な場合は**Dockerキャッシュ**を使用します。
このステップでキャッシュを使用すると、開発中にイメージを何度もビルドする際に、**毎回**すべての依存関係を**ダウンロードしてインストールする**代わりに多くの**時間**を**節約**できます。
-
./app
ディレクトリを
/code` ディレクトリの中にコピーする。これには**最も頻繁に変更される**すべてのコードが含まれているため、Dockerの**キャッシュ**は**これ以降のステップ**に簡単に使用されることはありません。
そのため、コンテナイメージのビルド時間を最適化するために、
Dockerfile
の 最後 にこれを置くことが重要です。 -
uvicorn
サーバーを実行するための**コマンド**を設定しますCMD
は文字列のリストを取り、それぞれの文字列はスペースで区切られたコマンドラインに入力するものです。このコマンドは **現在の作業ディレクトリ**から実行され、上記の
WORKDIR /code
にて設定した/code
ディレクトリと同じです。そのためプログラムは
/code
で開始しその中にあなたのコードがある./app
ディレクトリがあるので、Uvicorn はapp.main
からapp
を参照し、インポート することができます。
Tip
コード内の"+"の吹き出しをクリックして、各行が何をするのかをレビューしてください。👆
これで、次のようなディレクトリ構造になるはずです:
.
├── app
│ ├── __init__.py
│ └── main.py
├── Dockerfile
└── requirements.txt
TLS Termination Proxyの裏側¶
Nginx や Traefik のような TLS Termination Proxy (ロードバランサ) の後ろでコンテナを動かしている場合は、--proxy-headers
オプションを追加します。
このオプションは、Uvicornにプロキシ経由でHTTPSで動作しているアプリケーションに対して、送信されるヘッダを信頼するよう指示します。
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
Dockerキャッシュ¶
このDockerfile
には重要なトリックがあり、まず**依存関係だけのファイル**をコピーします。その理由を説明します。
COPY ./requirements.txt /code/requirements.txt
Dockerや他のツールは、これらのコンテナイメージを**段階的に**ビルドし、**1つのレイヤーを他のレイヤーの上に**追加します。Dockerfile
の先頭から開始し、Dockerfile
の各命令によって作成されたファイルを追加していきます。
Dockerや同様のツールは、イメージをビルドする際に**内部キャッシュ**も使用します。前回コンテナイメージを構築したときからファイルが変更されていない場合、ファイルを再度コピーしてゼロから新しいレイヤーを作成する代わりに、**前回作成した同じレイヤーを再利用**します。
ただファイルのコピーを避けるだけではあまり改善されませんが、そのステップでキャッシュを利用したため、**次のステップ**でキャッシュを使うことができます。
例えば、依存関係をインストールする命令のためにキャッシュを使うことができます:
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
パッケージ要件のファイルは**頻繁に変更されることはありません**。そのため、そのファイルだけをコピーすることで、Dockerはそのステップでは**キャッシュ**を使用することができます。
そして、Dockerは**次のステップのためにキャッシュ**を使用し、それらの依存関係をダウンロードしてインストールすることができます。そして、ここで**多くの時間を節約**します。✨ ...そして退屈な待ち時間を避けることができます。😪😆
パッケージの依存関係をダウンロードしてインストールするには**数分**かかりますが、**キャッシュ**を使えば**せいぜい数秒**です。
加えて、開発中にコンテナ・イメージを何度もビルドして、コードの変更が機能しているかどうかをチェックすることになるため、多くの時間を節約することができます。
そしてDockerfile
の最終行の近くですべてのコードをコピーします。この理由は、**最も頻繁に**変更されるものなので、このステップの後にあるものはほとんどキャッシュを使用することができないのためです。
COPY ./app /code/app
Dockerイメージをビルドする¶
すべてのファイルが揃ったので、コンテナ・イメージをビルドしましょう。
- プロジェクトディレクトリに移動します(
Dockerfile
がある場所で、app
ディレクトリがあります) - FastAPI イメージをビルドします:
$ docker build -t myimage .
---> 100%
Tip
末尾の .
に注目してほしいです。これは ./
と同じ意味です。 これはDockerにコンテナイメージのビルドに使用するディレクトリを指示します。
この場合、同じカレント・ディレクトリ(.
)です。
Dockerコンテナの起動する¶
- イメージに基づいてコンテナを実行します:
$ docker run -d --name mycontainer -p 80:80 myimage
確認する¶
Dockerコンテナのhttp://192.168.99.100/items/5?q=somequery や http://127.0.0.1/items/5?q=somequery (またはそれに相当するDockerホストを使用したもの)といったURLで確認できるはずです。
アクセスすると以下のようなものが表示されます:
{"item_id": 5, "q": "somequery"}
インタラクティブなAPIドキュメント¶
これらのURLにもアクセスできます: http://192.168.99.100/docs や http://127.0.0.1/docs (またはそれに相当するDockerホストを使用したもの)
アクセスすると、自動対話型APIドキュメント(Swagger UIが提供)が表示されます:
代替のAPIドキュメント¶
また、http://192.168.99.100/redoc や http://127.0.0.1/redoc (またはそれに相当するDockerホストを使用したもの)にもアクセスできます。
代替の自動ドキュメント(ReDocによって提供される)が表示されます:
単一ファイルのFastAPIでDockerイメージをビルドする¶
FastAPI が単一のファイル、例えば ./app
ディレクトリのない main.py
の場合、ファイル構造は次のようになります:
.
├── Dockerfile
├── main.py
└── requirements.txt
そうすれば、Dockerfile
の中にファイルをコピーするために、対応するパスを変更するだけでよいです:
FROM python:3.9
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# (1)
COPY ./main.py /code/
# (2)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
-
main.py
ファイルを
/code` ディレクトリに直接コピーします。 -
Uvicornを実行し、
main
からapp
オブジェクトをインポートするように指示します(app.main
からインポートするのではなく)。
次にUvicornコマンドを調整して、app.main
の代わりに新しいモジュール main
を使用し、FastAPIオブジェクトである app
をインポートします。
デプロイメントのコンセプト¶
コンテナという観点から、デプロイのコンセプトに共通するいくつかについて、もう一度説明しましょう。
コンテナは主に、アプリケーションの**ビルドとデプロイ**のプロセスを簡素化するためのツールですが、これらの**デプロイのコンセプト**を扱うための特定のアプローチを強制するものではないです。
**良いニュース**は、それぞれの異なる戦略には、すべてのデプロイメントのコンセプトをカバーする方法があるということです。🎉
これらの**デプロイメントのコンセプト**をコンテナの観点から見直してみましょう:
- セキュリティ - HTTPS
- 起動時の実行
- 再起動
- レプリケーション(実行中のプロセス数)
- メモリ
- 開始前の事前ステップ
HTTPS¶
FastAPI アプリケーションの コンテナ・イメージ(および後で実行中の コンテナ)だけに焦点を当てると、通常、HTTPSは別のツールを用いて**外部で**処理されます。
例えばTraefikのように、**HTTPS**と**証明書**の**自動**取得を扱う別のコンテナである可能性もあります。
Tip
TraefikはDockerやKubernetesなどと統合されているので、コンテナ用のHTTPSの設定や構成はとても簡単です。
あるいは、(コンテナ内でアプリケーションを実行しながら)クラウド・プロバイダーがサービスの1つとしてHTTPSを処理することもできます。
起動時および再起動時の実行¶
通常、コンテナの**起動と実行**を担当する別のツールがあります。
それは直接**Docker**であったり、**Docker Compose**であったり、**Kubernetes**であったり、**クラウドサービス**であったりします。
ほとんどの場合(またはすべての場合)、起動時にコンテナを実行し、失敗時に再起動を有効にする簡単なオプションがあります。例えばDockerでは、コマンドラインオプションの--restart
が該当します。
コンテナを使わなければ、アプリケーションを起動時や再起動時に実行させるのは面倒で難しいかもしれません。しかし、**コンテナ**で作業する場合、ほとんどのケースでその機能はデフォルトで含まれています。✨
レプリケーション - プロセス数¶
Kubernetes や Docker Swarm モード、Nomad、あるいは複数のマシン上で分散コンテナを管理するための同様の複雑なシステムを使ってマシンのクラスターを構成している場合、 各コンテナで(Workerを持つGunicornのような)**プロセスマネージャ**を使用する代わりに、**クラスター・レベル**で**レプリケーション**を処理したいと思うでしょう。
Kubernetesのような分散コンテナ管理システムの1つは通常、入ってくるリクエストの**ロードバランシング**をサポートしながら、**コンテナのレプリケーション**を処理する統合された方法を持っています。このことはすべて**クラスタレベル**にてです。
そのような場合、UvicornワーカーでGunicornのようなものを実行するのではなく、上記の説明のように**Dockerイメージをゼロから**ビルドし、依存関係をインストールして、**単一のUvicornプロセス**を実行したいでしょう。
ロードバランサー¶
コンテナを使用する場合、通常はメイン・ポート**でリスニング**しているコンポーネントがあるはずです。それはおそらく、**HTTPS**を処理するための**TLS Termination Proxy**でもある別のコンテナであったり、同様のツールであったりするでしょう。
このコンポーネントはリクエストの 負荷 を受け、 (うまくいけば) その負荷を**バランスよく** ワーカーに分配するので、一般に ロードバランサ とも呼ばれます。
Tip
HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネントは、おそらく**ロードバランサー**にもなるでしょう。
そしてコンテナで作業する場合、コンテナの起動と管理に使用する同じシステムには、ロードバランサー(TLS Termination Proxy**の可能性もある)から**ネットワーク通信(HTTPリクエストなど)をアプリのあるコンテナ(複数可)に送信するための内部ツールが既にあるはずです。
1つのロードバランサー - 複数のワーカーコンテナー¶
**Kubernetes**や同様の分散コンテナ管理システムで作業する場合、その内部のネットワーキングのメカニズムを使用することで、メインの**ポート**でリッスンしている単一の**ロードバランサー**が、アプリを実行している可能性のある**複数のコンテナ**に通信(リクエスト)を送信できるようになります。
アプリを実行するこれらのコンテナには、通常**1つのプロセス**(たとえば、FastAPIアプリケーションを実行するUvicornプロセス)があります。これらはすべて**同一のコンテナ**であり同じものを実行しますが、それぞれが独自のプロセスやメモリなどを持ちます。そうすることで、CPUの**異なるコア**、あるいは**異なるマシン**での**並列化**を利用できます。
そして、**ロードバランサー**を備えた分散コンテナシステムは、**順番に**あなたのアプリを含む各コンテナに**リクエストを分配**します。つまり、各リクエストは、あなたのアプリを実行している複数の**レプリケートされたコンテナ**の1つによって処理されます。
そして通常、この**ロードバランサー**は、クラスタ内の*他の*アプリケーション(例えば、異なるドメインや異なるURLパスのプレフィックスの配下)へのリクエストを処理することができ、その通信をクラスタ内で実行されている*他の*アプリケーションのための適切なコンテナに送信します。
1コンテナにつき1プロセス¶
この種のシナリオでは、すでにクラスタ・レベルでレプリケーションを処理しているため、おそらくコンテナごとに**単一の(Uvicorn)プロセス**を持ちたいでしょう。
この場合、Uvicornワーカーを持つGunicornのようなプロセスマネージャーや、Uvicornワーカーを使うUvicornは**避けたい**でしょう。**コンテナごとにUvicornのプロセスは1つだけ**にしたいでしょう(おそらく複数のコンテナが必要でしょう)。
(GunicornやUvicornがUvicornワーカーを管理するように)コンテナ内に別のプロセスマネージャーを持つことは、クラスターシステムですでに対処しているであろう**不要な複雑さ**を追加するだけです。
Containers with Multiple Processes and Special Cases¶
もちろん、**特殊なケース**として、**Gunicornプロセスマネージャ**を持つ**コンテナ**内で複数の**Uvicornワーカープロセス**を起動させたい場合があります。
このような場合、**公式のDockerイメージ**を使用することができます。このイメージには、複数の**Uvicornワーカープロセス**を実行するプロセスマネージャとして**Gunicorn**が含まれており、現在のCPUコアに基づいてワーカーの数を自動的に調整するためのデフォルト設定がいくつか含まれています。詳しくは後述のGunicornによる公式Dockerイメージ - Uvicornで説明します。
以下は、それが理にかなっている場合の例です:
シンプルなアプリケーション¶
アプリケーションを**シンプル**な形で実行する場合、プロセス数の細かい調整が必要ない場合、自動化されたデフォルトを使用するだけで、コンテナ内にプロセスマネージャが必要かもしれません。例えば、公式Dockerイメージでシンプルな設定が可能です。
Docker Compose¶
Docker Composeで**シングルサーバ**(クラスタではない)にデプロイすることもできますので、共有ネットワークと**ロードバランシング**を維持しながら(Docker Composeで)コンテナのレプリケーションを管理する簡単な方法はないでしょう。
その場合、**単一のコンテナ**で、**プロセスマネージャ**が内部で**複数のワーカープロセス**を起動するようにします。
Prometheusとその他の理由¶
また、**1つのコンテナ**に**1つのプロセス**を持たせるのではなく、**1つのコンテナ**に**複数のプロセス**を持たせる方が簡単だという**他の理由**もあるでしょう。
例えば、(セットアップにもよりますが)Prometheusエクスポーターのようなツールを同じコンテナ内に持つことができます。
この場合、複数のコンテナ**があると、デフォルトでは、Prometheusが**メトリクスを**読みに来たとき、すべてのレプリケートされたコンテナの**蓄積されたメトリクス**を取得するのではなく、毎回**単一のコンテナ(その特定のリクエストを処理したコンテナ)のものを取得することになります。
その場合、**複数のプロセス**を持つ**1つのコンテナ**を用意し、同じコンテナ上のローカルツール(例えばPrometheusエクスポーター)がすべての内部プロセスのPrometheusメトリクスを収集し、その1つのコンテナ上でそれらのメトリクスを公開する方がシンプルかもしれません。
重要なのは、盲目的に従わなければならない普遍のルールはないということです。
これらのアイデアは、**あなた自身のユースケース**を評価し、あなたのシステムに最適なアプローチを決定するために使用することができます:
- セキュリティ - HTTPS
- 起動時の実行
- 再起動
- レプリケーション(実行中のプロセス数)
- メモリ
- 開始前の事前ステップ
メモリー¶
コンテナごとに**単一のプロセスを実行する**と、それらのコンテナ(レプリケートされている場合は1つ以上)によって消費される多かれ少なかれ明確に定義された、安定し制限された量のメモリを持つことになります。
そして、コンテナ管理システム(**Kubernetes**など)の設定で、同じメモリ制限と要件を設定することができます。
そうすれば、コンテナが必要とするメモリ量とクラスタ内のマシンで利用可能なメモリ量を考慮して、**利用可能なマシン**に**コンテナ**をレプリケートできるようになります。
アプリケーションが**シンプル**なものであれば、これはおそらく**問題にはならない**でしょうし、ハードなメモリ制限を指定する必要はないかもしれないです。
しかし、**多くのメモリを使用**している場合(たとえば**機械学習**モデルなど)、どれだけのメモリを消費しているかを確認し、**各マシンで実行するコンテナの数**を調整する必要があります(そしておそらくクラスタにマシンを追加します)。
**コンテナごとに複数のプロセス**を実行する場合(たとえば公式のDockerイメージで)、起動するプロセスの数が**利用可能なメモリ以上に消費しない**ようにする必要があります。
開始前の事前ステップとコンテナ¶
コンテナ(DockerやKubernetesなど)を使っている場合、主に2つのアプローチがあります。
複数のコンテナ¶
複数の**コンテナ**があり、おそらくそれぞれが**単一のプロセス**を実行している場合(Kubernetes**クラスタなど)、レプリケートされたワーカーコンテナを実行する**前に、単一のコンテナで**事前のステップ**の作業を行う**別のコンテナ**を持ちたいと思うでしょう。
Info
もしKubernetesを使用している場合, これはおそらくInit コンテナでしょう。
ユースケースが事前のステップを**並列で複数回**実行するのに問題がない場合(例:データベースの準備チェック)、メインプロセスを開始する前に、それらのステップを各コンテナに入れることが可能です。
単一コンテナ¶
単純なセットアップで、単一のコンテナ**で複数の**ワーカー・プロセス(または1つのプロセスのみ)を起動する場合、アプリでプロセスを開始する直前に、同じコンテナで事前のステップを実行できます。公式Dockerイメージは、内部的にこれをサポートしています。
Gunicornによる公式Dockerイメージ - Uvicorn¶
前の章で詳しく説明したように、Uvicornワーカーで動作するGunicornを含む公式のDockerイメージがあります: Server Workers - Gunicorn と Uvicornで詳しく説明しています。
このイメージは、主に上記で説明した状況で役に立つでしょう: 複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)
Warning
このベースイメージや類似のイメージは**必要ない**可能性が高いので、上記の: FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)のようにゼロからイメージをビルドする方が良いでしょう。
このイメージには、利用可能なCPUコアに基づいて**ワーカー・プロセスの数**を設定する**オートチューニング**メカニズムが含まれています。
これは**賢明なデフォルト**を備えていますが、**環境変数**や設定ファイルを使ってすべての設定を変更したり更新したりすることができます。
また、スクリプトで開始前の事前ステップを実行することもサポートしている。
Tip
すべての設定とオプションを見るには、Dockerイメージのページをご覧ください: tiangolo/uvicorn-gunicorn-fastapi
公式Dockerイメージのプロセス数¶
このイメージの**プロセス数**は、利用可能なCPU**コア**から**自動的に計算**されます。
つまり、CPUから可能な限り**パフォーマンス**を**引き出そう**とします。
また、**環境変数**などを使った設定で調整することもできます。
しかし、プロセスの数はコンテナが実行しているCPUに依存するため、**消費されるメモリの量**もそれに依存することになります。
そのため、(機械学習モデルなどで)大量のメモリを消費するアプリケーションで、サーバーのCPUコアが多いが**メモリが少ない**場合、コンテナは利用可能なメモリよりも多くのメモリを使おうとすることになります。
その結果、パフォーマンスが大幅に低下する(あるいはクラッシュする)可能性があります。🚨
Dockerfileを作成する¶
この画像に基づいてDockerfile
を作成する方法を以下に示します:
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./app /app
より大きなアプリケーション¶
複数のファイルを持つ大きなアプリケーションを作成するセクションに従った場合、Dockerfile
は次のようになります:
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./app /app/app
いつ使うのか¶
おそらく、Kubernetes(または他のもの)を使用していて、すでにクラスタレベルで複数の**コンテナ**で**レプリケーション**を設定している場合は、この公式ベースイメージ(または他の類似のもの)は**使用すべきではありません**。
そのような場合は、上記のように**ゼロから**イメージを構築する方がよいでしょう: FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI) を参照してください。
このイメージは、主に上記の複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)で説明したような特殊なケースで役に立ちます。
例えば、アプリケーションが**シンプル**で、CPUに応じたデフォルトのプロセス数を設定すればうまくいく場合や、クラスタレベルでレプリケーションを手動で設定する手間を省きたい場合、アプリで複数のコンテナを実行しない場合などです。
または、**Docker Compose**でデプロイし、単一のサーバで実行している場合などです。
コンテナ・イメージのデプロイ¶
コンテナ(Docker)イメージを手に入れた後、それをデプロイするにはいくつかの方法があります。
例えば以下のリストの方法です:
- 単一サーバーの**Docker Compose**
- **Kubernetes**クラスタ
- Docker Swarmモードのクラスター
- Nomadのような別のツール
- コンテナ・イメージをデプロイするクラウド・サービス
Poetryを利用したDockerイメージ¶
もしプロジェクトの依存関係を管理するためにPoetryを利用する場合、マルチステージビルドを使うと良いでしょう。
# (1)
FROM python:3.9 as requirements-stage
# (2)
WORKDIR /tmp
# (3)
RUN pip install poetry
# (4)
COPY ./pyproject.toml ./poetry.lock* /tmp/
# (5)
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
# (6)
FROM python:3.9
# (7)
WORKDIR /code
# (8)
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt
# (9)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# (10)
COPY ./app /code/app
# (11)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
- これは最初のステージで、
requirements-stage
と名付けられます -
/tmp
を現在の作業ディレクトリに設定します ここでrequirements.txt
というファイルを生成します。 -
このDockerステージにPoetryをインストールします
-
pyproject.toml
と
poetry.lockファイルを
/tmp` ディレクトリにコピーします./poetry.lock*
(末尾に*
)を使用するため、そのファイルがまだ利用できない場合でもクラッシュすることはないです。 5. requirements.txt`ファイルを生成します -
これは最後のステージであり、ここにあるものはすべて最終的なコンテナ・イメージに保存されます
- 現在の作業ディレクトリを
/code
に設定します requirements.txt
ファイルを/code
ディレクトリにコピーします このファイルは前のDockerステージにしか存在しないため、--from-requirements-stage
を使ってコピーします。- 生成された
requirements.txt
ファイルにあるパッケージの依存関係をインストールします - app
ディレクトリを
/code` ディレクトリにコピーします - uvicorn
コマンドを実行して、
app.mainからインポートした
app` オブジェクトを使用するように指示します
Tip
"+"の吹き出しをクリックすると、それぞれの行が何をするのかを見ることができます
**Dockerステージ**はDockerfile
の一部で、**一時的なコンテナイメージ**として動作します。
最初のステージは Poetryのインストール**と Poetry の pyproject.toml
ファイルからプロジェクトの依存関係を含むrequirements.txt
を生成**するためだけに使用されます。
この requirements.txt
ファイルは後半の **次のステージ**で pip
と共に使用されます。
最終的なコンテナイメージでは、**最終ステージ**のみが保存されます。前のステージは破棄されます。
Poetryを使用する場合、**Dockerマルチステージビルド**を使用することは理にかなっています。
なぜなら、最終的なコンテナイメージにPoetryとその依存関係がインストールされている必要はなく、**必要なのは**プロジェクトの依存関係をインストールするために生成された requirements.txt
ファイルだけだからです。
そして次の(そして最終的な)ステージでは、前述とほぼ同じ方法でイメージをビルドします。
TLS Termination Proxyの裏側 - Poetry¶
繰り返しになりますが、NginxやTraefikのようなTLS Termination Proxy(ロードバランサー)の後ろでコンテナを動かしている場合は、--proxy-headers
オプションをコマンドに追加します:
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
まとめ¶
コンテナ・システム(例えば**Docker**や**Kubernetes**など)を使えば、すべての**デプロイメントのコンセプト**を扱うのがかなり簡単になります:
- セキュリティ - HTTPS
- 起動時の実行
- 再起動
- レプリケーション(実行中のプロセス数)
- メモリ
- 開始前の事前ステップ
ほとんどの場合、ベースとなるイメージは使用せず、公式のPython Dockerイメージをベースにした**コンテナイメージをゼロからビルド**します。
Dockerfile
と**Dockerキャッシュ**内の命令の**順番**に注意することで、**ビルド時間を最小化**することができ、生産性を最大化することができます(そして退屈を避けることができます)。😎
特別なケースでは、FastAPI用の公式Dockerイメージを使いたいかもしれません。🤓