プロキシの背後¶
状況によっては、Traefik や Nginx のようなプロキシサーバーを、アプリケーションからは見えない余分なパスプレフィックスを追加する構成で使用する必要がある場合があります。
このような場合、root_path
を使用してアプリケーションを設定できます。
root_path
は、ASGI 仕様(FastAPI が Starlette を介して構築されている)によって提供されるメカニズムです。
root_path
は、これらの特定のケースを処理するために使用されます。
また、サブアプリケーションをマウントする際にも内部的に使用されます。
パスプレフィックスが削除されたプロキシ¶
この場合、パスプレフィックスが削除されたプロキシとは、コード内で /app
というパスを宣言できるが、その上に(プロキシという)レイヤーを追加し、FastAPIアプリケーションを /api/v1
のようなパスの下に置くことを意味します。
この場合、元のパス /app
は実際には /api/v1/app
で提供されます。
すべてのコードは /app
しかないと仮定して書かれているにもかかわらずです。
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/app")
def read_main(request: Request):
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
そしてプロキシは、リクエストをアプリサーバー(おそらくFastAPI CLIを介したUvicorn)に送信する前に、パスプレフィックスをオンザフライで**「削除」**し、アプリケーションが/app
で提供されていると確信させます。これにより、/api/v1
というプレフィックスをすべてのコードに含める必要がなくなります。
ここまでで、すべてが通常通り動作します。
しかし、統合されたドキュメント UI(フロントエンド)を開くと、/api/v1/openapi.json
ではなく、/openapi.json
でOpenAPIスキーマを取得することを期待します。
そのため、フロントエンド (ブラウザで実行される) は /openapi.json
にアクセスしようとしますが、OpenAPI スキーマを取得できません。
アプリに /api/v1
のパスプレフィックスを持つプロキシがあるため、フロントエンドは /api/v1/openapi.json
で OpenAPI スキーマを取得する必要があります。
graph LR
browser("Browser")
proxy["Proxy on http://0.0.0.0:9999/api/v1/app"]
server["Server on http://127.0.0.1:8000/app"]
browser --> proxy
proxy --> server
ヒント
IP 0.0.0.0
は、そのマシン/サーバーで利用可能なすべての IP でプログラムがリッスンすることを意味するために一般的に使用されます。
ドキュメントUIはまた、このAPIのサーバー
が/api/v1
(プロキシの背後)にあることを宣言するためにOpenAPIスキーマを必要とします。例えば、
{
"openapi": "3.1.0",
// More stuff here
"servers": [
{
"url": "/api/v1"
}
],
"paths": {
// More stuff here
}
}
この例では、「プロキシ」はTraefikのようなものであり、サーバーはUvicornを搭載したFastAPI CLIのようなもので、FastAPIアプリケーションを実行します。
root_path
を指定する¶
これを実現するには、--root-path
のようなコマンドラインオプションを使用できます。
$ fastapi run main.py --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Hypercornを使用している場合、それも--root-path
オプションを持っています。
技術的な詳細
ASGI 仕様は、この使用例のために root_path
を定義しています。
そして、--root-path
コマンドラインオプションは、その root_path
を提供します。
現在の root_path
を確認する¶
アプリケーションが各リクエストで使用する現在の root_path
を取得できます。これは scope
ディクショナリの一部です(これは ASGI 仕様の一部です)。
ここでは、デモンストレーション目的でのみメッセージに含めています。
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/app")
def read_main(request: Request):
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
Uvicorn を起動すると、
$ fastapi run main.py --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
応答は次のようになります
{
"message": "Hello World",
"root_path": "/api/v1"
}
FastAPI アプリで root_path
を設定する¶
あるいは、--root-path
のようなコマンドラインオプションや同等のものを提供する方法がない場合は、FastAPI アプリを作成する際に root_path
パラメータを設定できます。
from fastapi import FastAPI, Request
app = FastAPI(root_path="/api/v1")
@app.get("/app")
def read_main(request: Request):
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
FastAPI に root_path
を渡すことは、Uvicorn または Hypercorn に --root-path
コマンドラインオプションを渡すことと同じです。
root_path
について¶
サーバー (Uvicorn) は、その root_path
をアプリに渡す以外の目的には使用しないことに注意してください。
しかし、ブラウザで http://127.0.0.1:8000/app にアクセスすると、通常の応答が表示されます。
{
"message": "Hello World",
"root_path": "/api/v1"
}
したがって、http://127.0.0.1:8000/api/v1/app
でアクセスされることは期待しません。
Uvicorn は、プロキシが http://127.0.0.1:8000/app
で Uvicorn にアクセスすることを期待し、その上で追加の /api/v1
プレフィックスを追加するのはプロキシの責任となります。
パスプレフィックスが削除されたプロキシについて¶
パスプレフィックスが削除されたプロキシは、構成方法の1つにすぎないことに注意してください。
おそらく多くのケースでは、プロキシにパスプレフィックスが削除されていないのがデフォルトでしょう。
そのような場合 (パスプレフィックスが削除されていない場合)、プロキシは https://myawesomeapp.com
のようなものを受け付け、ブラウザが https://myawesomeapp.com/api/v1/app
にアクセスし、サーバー (例: Uvicorn) が http://127.0.0.1:8000
を受け付ける場合、プロキシ (パスプレフィックスが削除されていない場合) は同じパス http://127.0.0.1:8000/api/v1/app
で Uvicorn にアクセスします。
Traefik でローカルにテストする¶
Traefik を使用して、パスプレフィックスが削除された状態でローカルで簡単に実験を実行できます。
Traefik をダウンロードしてください。これは単一のバイナリで、圧縮ファイルを展開してターミナルから直接実行できます。
次に、traefik.toml
というファイルを作成し、
[entryPoints]
[entryPoints.http]
address = ":9999"
[providers]
[providers.file]
filename = "routes.toml"
これは Traefik にポート 9999 でリッスンし、別のファイル routes.toml
を使用するよう指示します。
ヒント
標準の HTTP ポート 80 の代わりにポート 9999 を使用しているのは、管理者 (sudo
) 権限で実行する必要がないようにするためです。
次に、もう1つのファイル routes.toml
を作成します。
[http]
[http.middlewares]
[http.middlewares.api-stripprefix.stripPrefix]
prefixes = ["/api/v1"]
[http.routers]
[http.routers.app-http]
entryPoints = ["http"]
service = "app"
rule = "PathPrefix(`/api/v1`)"
middlewares = ["api-stripprefix"]
[http.services]
[http.services.app]
[http.services.app.loadBalancer]
[[http.services.app.loadBalancer.servers]]
url = "http://127.0.0.1:8000"
このファイルは、Traefik がパスプレフィックス /api/v1
を使用するように設定します。
そして、Traefik はリクエストを http://127.0.0.1:8000
で実行されている Uvicorn にリダイレクトします。
次に Traefik を起動します。
$ ./traefik --configFile=traefik.toml
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
次に、--root-path
オプションを使用してアプリを起動します。
$ fastapi run main.py --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
レスポンスを確認する¶
これで、Uvicorn のポートを含む URL: http://127.0.0.1:8000/app にアクセスすると、通常の応答が表示されます。
{
"message": "Hello World",
"root_path": "/api/v1"
}
ヒント
http://127.0.0.1:8000/app
でアクセスしているにもかかわらず、オプション --root-path
から取得された /api/v1
の root_path
が表示されていることに注目してください。
次に、Traefik のポートを含む URL を、パスプレフィックスを含めて開きます: http://127.0.0.1:9999/api/v1/app。
同じ応答が得られます。
{
"message": "Hello World",
"root_path": "/api/v1"
}
ただし、今回はプロキシが提供するパスプレフィックス /api/v1
を含む URL です。
もちろん、ここでの考え方は、誰もがプロキシ経由でアプリにアクセスすることであり、したがって、パスプレフィックス /api/v1
を持つバージョンが「正しい」ものです。
そして、パスプレフィックスのないバージョン (http://127.0.0.1:8000/app
) は、Uvicorn によって直接提供され、プロキシ (Traefik) がアクセスするためだけに利用されます。
これは、プロキシ (Traefik) がパスプレフィックスをどのように使用し、サーバー (Uvicorn) が --root-path
オプションから root_path
をどのように使用するかを示しています。
ドキュメント UI を確認する¶
しかし、ここが面白いところです。✨
アプリにアクセスする「公式な」方法は、私たちが定義したパスプレフィックスを持つプロキシを経由することです。したがって、私たちが期待するように、URLにパスプレフィックスなしでUvicornが直接提供するドキュメントUIを試しても、プロキシ経由でアクセスされることを期待しているため、機能しません。
http://127.0.0.1:8000/docs で確認できます
しかし、ポート 9999
のプロキシを使用して、「公式」URL の /api/v1/docs
でドキュメント UI にアクセスすると、正しく機能します!🎉
http://127.0.0.1:9999/api/v1/docs で確認できます
まさに私たちが望んだとおりです。✔️
これは、FastAPI がこの root_path
を使用して、OpenAPI で root_path
によって提供される URL を持つデフォルトの server
を作成するためです。
追加のサーバー¶
警告
これはより高度な使用例です。スキップしても構いません。
デフォルトでは、FastAPI は OpenAPI スキーマに root_path
の URL を持つ server
を作成します。
ただし、ステージング環境と本番環境の両方と対話するために同じドキュメントUIを使用したい場合など、他の代替の servers
を提供することもできます。
カスタムの servers
のリストを渡し、root_path
が存在する場合 (API がプロキシの背後にあるため)、FastAPI はこの root_path
を持つ「サーバー」をリストの先頭に挿入します。
例えば
from fastapi import FastAPI, Request
app = FastAPI(
servers=[
{"url": "https://stag.example.com", "description": "Staging environment"},
{"url": "https://prod.example.com", "description": "Production environment"},
],
root_path="/api/v1",
)
@app.get("/app")
def read_main(request: Request):
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
以下のような OpenAPI スキーマが生成されます。
{
"openapi": "3.1.0",
// More stuff here
"servers": [
{
"url": "/api/v1"
},
{
"url": "https://stag.example.com",
"description": "Staging environment"
},
{
"url": "https://prod.example.com",
"description": "Production environment"
}
],
"paths": {
// More stuff here
}
}
ヒント
root_path
から取得された、url
の値が /api/v1
の自動生成されたサーバーに注目してください。
http://127.0.0.1:9999/api/v1/docs のドキュメント UI では、次のようになります。
ヒント
ドキュメント UI は、選択したサーバーと対話します。
root_path
からの自動サーバーを無効にする¶
FastAPI に root_path
を使用した自動サーバーを含めたくない場合は、root_path_in_servers=False
パラメータを使用できます。
from fastapi import FastAPI, Request
app = FastAPI(
servers=[
{"url": "https://stag.example.com", "description": "Staging environment"},
{"url": "https://prod.example.com", "description": "Production environment"},
],
root_path="/api/v1",
root_path_in_servers=False,
)
@app.get("/app")
def read_main(request: Request):
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
そうすれば、OpenAPI スキーマには含まれません。
サブアプリケーションをマウントする¶
サブアプリケーションをマウントする必要があり(サブアプリケーション - マウントで説明されているように)、同時に root_path
を持つプロキシを使用する場合でも、通常通り、期待通りに実行できます。
FastAPI は内部的に root_path
を賢く使用するため、そのまま動作します。✨