コンテンツにスキップ

プロキシの背後

状況によっては、アプリケーションからは見えない追加のパス プレフィックスを追加する構成で、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_pathFastAPI に渡すことは、--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/v1root_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/v1url 値を持つ、自動生成されたサーバーに注目してください。

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 をスマートに使用するため、正常に機能します。✨