プロキシの背後¶
状況によっては、アプリケーションからは見えない追加のパス プレフィックスを追加する構成で、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 server
が /api/v1
(プロキシの背後)にあることを宣言するために OpenAPI スキーマも必要とします。例えば
{
"openapi": "3.1.0",
// More stuff here
"servers": [
{
"url": "/api/v1"
}
],
"paths": {
// More stuff here
}
}
この例では、「プロキシ」はTraefikのようなものになります。そして、サーバーは FastAPI アプリケーションを実行するUvicornを使用した FastAPI CLI のようなものになります。
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")}
root_path
を FastAPI
に渡すことは、--root-path
コマンドラインオプションを Uvicorn または Hypercorn に渡すことと同じです。
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
) 権限で実行する必要はありません。
次に、別のファイル 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
を含むバージョンが「正しい」バージョンであるということです。
そして、Uvicorn によって直接提供されるパスプレフィックスのないバージョン (http://127.0.0.1:8000/app
) は、*プロキシ* (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
を使用して、root_path
によって提供される URL を持つ OpenAPI のデフォルト server
を作成するためです。
追加のサーバー¶
警告
これは、より高度なユースケースです。スキップしても構いません。
デフォルトでは、**FastAPI** は root_path
の URL を使用して OpenAPI スキーマに 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
から取得した /api/v1
の url
値を持つ、自動生成されたサーバーに注目してください。
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
を持つプロキシを使用しているときに、(サブアプリケーション - マウント で説明されているように)サブアプリケーションをマウントする必要がある場合は、期待どおりに normalt に実行できます。
FastAPI は内部で root_path
をスマートに使用するため、正常に機能します。✨