コンテンツへスキップ

プロキシの背後

状況によっては、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/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 を持つバージョンが「正しい」ものです。

そして、パスプレフィックスのないバージョン (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 からの自動サーバーを無効にする

FastAPIroot_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 を賢く使用するため、そのまま動作します。✨