コンテンツへスキップ

プロキシの背後

多くの場合、FastAPI アプリケーションの前に Traefik や Nginx のようなプロキシを使用します。

これらのプロキシは、HTTPS 証明書やその他の処理を担当できます。

プロキシ転送ヘッダー

アプリケーションの前面にあるプロキシは、通常、リクエストをサーバーに送信する前に、いくつかのヘッダーを動的に設定し、リクエストがプロキシによって転送されたこと、元の(公開)URL(ドメインを含む)、HTTPSを使用していることなどをサーバーに伝えます。

サーバープログラム(例えば、FastAPI CLI を介した Uvicorn)は、これらのヘッダーを解釈し、その情報をアプリケーションに渡すことができます。

しかし、セキュリティ上の理由から、サーバーはそれが信頼できるプロキシの背後にあることを知らないため、それらのヘッダーを解釈しません。

技術的な詳細

プロキシヘッダーは次のとおりです

プロキシ転送ヘッダーを有効にする

FastAPI CLI をCLI オプション --forwarded-allow-ips で起動し、それらの転送ヘッダーを読み取るために信頼されるべき IP アドレスを渡すことができます。

--forwarded-allow-ips="*" に設定すると、すべての受信 IP を信頼します。

サーバーが信頼できるプロキシの背後にあり、プロキシのみがサーバーと通信する場合、これはそのプロキシの IP を受け入れることになります。

$ fastapi run --forwarded-allow-ips="*"

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

HTTPSによるリダイレクト

たとえば、パス操作 /items/ を定義したとします。

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/")
def read_items():
    return ["plumbus", "portal gun"]

クライアントが /items にアクセスしようとすると、デフォルトでは /items/ にリダイレクトされます。

しかし、CLI オプション --forwarded-allow-ips を設定する前は、https://:8000/items/ にリダイレクトされる可能性がありました。

しかし、おそらくアプリケーションは https://mysuperapp.com でホストされており、リダイレクトは https://mysuperapp.com/items/ になるはずです。

--proxy-headers を設定することで、FastAPI は正しい場所にリダイレクトできるようになります。😎

https://mysuperapp.com/items/

ヒント

HTTPS の詳細については、HTTPS についてのガイドを参照してください。

プロキシ転送ヘッダーの仕組み

クライアントとアプリケーションサーバーの間でプロキシが転送ヘッダーを追加する様子を視覚的に示したものです

sequenceDiagram
    participant Client
    participant Proxy as Proxy/Load Balancer
    participant Server as FastAPI Server

    Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items

    Note over Proxy: Proxy adds forwarded headers

    Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items

    Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set)

    Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs

    Proxy->>Client: HTTPS Response

プロキシは元のクライアントリクエストを傍受し、特別な転送ヘッダー(X-Forwarded-*)を追加してから、リクエストをアプリケーションサーバーに渡します。

これらのヘッダーは、そうでなければ失われる元のリクエストに関する情報を保持します

  • X-Forwarded-For: 元のクライアントの IP アドレス
  • X-Forwarded-Proto: 元のプロトコル(https
  • X-Forwarded-Host: 元のホスト(mysuperapp.com

FastAPI CLI--forwarded-allow-ips で構成されている場合、これらのヘッダーを信頼し、たとえばリダイレクトで正しい URL を生成するために使用します。

パスプレフィックスが削除されたプロキシ

アプリケーションにパスプレフィックスを追加するプロキシがあるかもしれません。

このような場合、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 --forwarded-allow-ips="*" --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 --forwarded-allow-ips="*" --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")}

FastAPIroot_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 をリッスンしている場合、プロキシ(パスプレフィックスが削除されていない)は Uvicorn に同じパス http://127.0.0.1:8000/api/v1/app でアクセスします。

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 --forwarded-allow-ips="*" --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"
}

ただし、今回はプロキシによって提供されたプレフィックスパスを含む URL: /api/v1 です。

もちろん、ここでのアイデアは、誰もがプロキシを介してアプリにアクセスすることであり、したがって、パスプレフィックス /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 を作成するためです。

追加サーバー

Warning

これはより高度なユースケースです。スキップしても構いません。

デフォルトでは、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 を賢く使用するため、単に機能します。✨