fastapi/docs/ja/docs/advanced/behind-a-proxy.md

20 KiB
Raw Blame History

プロキシの背後

多くの状況で、FastAPI アプリの前段に TraefikNginx のようなプロキシを置きます。

これらのプロキシは HTTPS 証明書などの処理を担います。

プロキシの転送ヘッダー

アプリケーションの前段にある プロキシ は通常、リクエストを サーバー に送る前に、そのリクエストがプロキシによって転送されたことを知らせるためのヘッダーを動的に付与し、使用中の元の公開URLドメインを含むや HTTPS 使用などの情報を伝えます。

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

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

/// note | 技術詳細

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

///

プロキシ転送ヘッダーを有効化

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 を伴うリダイレクト

例えば、path operation /items/ を定義しているとします:

{* ../../docs_src/behind_a_proxy/tutorial001_01_py310.py hl[6] *}

クライアントが /items にアクセスすると、既定では /items/ にリダイレクトされます。

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

ですが、アプリケーションは https://mysuperapp.com で公開されており、https://mysuperapp.com/items/ にリダイレクトされるべきかもしれません。

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

https://mysuperapp.com/items/

/// tip | 豆知識

HTTPS について詳しく知りたい場合は、HTTPS について{.internal-link target=_blank} を参照してください。

///

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

クライアントと アプリケーションサーバー の間で、プロキシ がどのように転送ヘッダーを追加するかを図示します:

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_pathFastAPI が Starlette を通して基づいているASGI 仕様で提供されている仕組みです。

root_path はこの種のケースを扱うために使われます。

これはサブアプリケーションをマウントする際にも内部的に使用されます。

ここでいう「パスプレフィックスを削除するプロキシ」とは、コード上では /app というパスを宣言していても、その上にプロキシ層を追加して FastAPI アプリケーションを /api/v1 のようなパスの下に配置することを指します。

この場合、元のパス /app は実際には /api/v1/app で提供されます。

すべてのコードは /app だけを前提に書かれているにもかかわらず、です。

{* ../../docs_src/behind_a_proxy/tutorial001_py310.py hl[6] *}

そしてプロキシは、アプリサーバー(おそらく FastAPI CLI 経由の Uvicornに転送する前に、その場で パスプレフィックス を**「削除」**し、アプリケーション側には自分が /app で提供されているように見せかけます。これにより、コードのすべてを /api/v1 のプレフィックス付きに書き換える必要がありません。

ここまでは通常どおりに動作します。

しかし、統合ドキュメント UIフロントエンドを開くと、OpenAPI スキーマを /api/v1/openapi.json ではなく /openapi.json から取得しようとします。

そのため、フロントエンド(ブラウザで動作)は /openapi.json にアクセスしようとして、OpenAPI スキーマを取得できません。

このアプリには /api/v1 のパスプレフィックスを付与するプロキシがあるため、フロントエンドは /api/v1/openapi.json から取得する必要があります。

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

/// tip | 豆知識

IP 0.0.0.0 は、そのマシン/サーバーで利用可能なすべての IP で待ち受けることを意味する表現として一般的に使われます。

///

ドキュメント UI では、この API の server が(プロキシの背後で)/api/v1 にあることを宣言する OpenAPI スキーマも必要です。例えば:

{
    "openapi": "3.1.0",
    // ほかの項目
    "servers": [
        {
            "url": "/api/v1"
        }
    ],
    "paths": {
            // ほかの項目
    }
}

この例では「Proxy」は Traefik のようなもの、サーバーは Uvicorn と FastAPI CLI で FastAPI アプリケーションを実行しているものを想定しています。

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 オプションがあります。

/// note | 技術詳細

このユースケース向けに、ASGI 仕様は root_path を定義しています。

そして --root-path コマンドラインオプションは、その root_path を提供します。

///

現在の root_path の確認

各リクエストでアプリケーションが使用している現在の root_path は取得できます。これはASGI 仕様の一部である)scope 辞書に含まれます。

ここではデモのため、メッセージに含めています。

{* ../../docs_src/behind_a_proxy/tutorial001_py310.py hl[8] *}

そのうえで、次のように 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 を設定できます:

{* ../../docs_src/behind_a_proxy/tutorial002_py310.py hl[3] *}

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 にアクセスしてくることを想定しており、その上に追加の /api/v1 プレフィックスを付けるのはプロキシの責務です。

パスプレフィックスを削除するプロキシについて

パスプレフィックスを削除するプロキシは、設定方法の一例にすぎない点に注意してください。

多くの場合、プロキシはパスプレフィックスを削除しない設定が既定でしょう。

そのような場合(パスプレフィックスを削除しない場合)は、プロキシは https://myawesomeapp.com のようなアドレスで待ち受け、ブラウザが https://myawesomeapp.com/api/v1/app にアクセスし、サーバー(例: Uvicornhttp://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 を使用するよう指示します。

/// tip | 豆知識

標準の 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"
}

/// tip | 豆知識

http://127.0.0.1:8000/app にアクセスしているにもかかわらず、オプション --root-path から取得した root_path/api/v1 と表示されている点に注目してください。

///

次に、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-pathroot_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 の既定の serverroot_path の URL で生成するためです。

追加のサーバー

/// warning | 注意

これは高度なユースケースです。読み飛ばしても構いません。

///

既定では、FastAPI は OpenAPI スキーマ内に root_path の URL を持つ server を作成します。

しかし、ステージングと本番の両方と同じドキュメント UI で対話させたい場合など、別の servers を指定することもできます。

カスタムの servers リストを渡していて、かつ root_pathAPI がプロキシの背後にあるため)が設定されている場合、FastAPI はこの root_path を用いた「server」をリストの先頭に挿入します。

例えば:

{* ../../docs_src/behind_a_proxy/tutorial003_py310.py hl[4:7] *}

次のような OpenAPI スキーマが生成されます:

{
    "openapi": "3.1.0",
    // ほかの項目
    "servers": [
        {
            "url": "/api/v1"
        },
        {
            "url": "https://stag.example.com",
            "description": "Staging environment"
        },
        {
            "url": "https://prod.example.com",
            "description": "Production environment"
        }
    ],
    "paths": {
            // ほかの項目
    }
}

/// tip | 豆知識

root_path から取得した url/api/v1 を持つ server が自動生成されている点に注目してください。

///

ドキュメント UIhttp://127.0.0.1:9999/api/v1/docs)では次のように表示されます:

/// tip | 豆知識

ドキュメント UI は、選択した server と対話します。

///

/// note | 技術詳細

OpenAPI 仕様の servers プロパティは任意です。

servers パラメータを指定せず、かつ root_path/ の場合、生成される OpenAPI スキーマからは servers プロパティが既定で完全に省略されます。これは、url/ の server が 1 つあるのと同等です。

///

root_path 由来の自動 server を無効化

root_path を用いた自動的な server を FastAPI に含めてほしくない場合は、パラメータ root_path_in_servers=False を使用します:

{* ../../docs_src/behind_a_proxy/tutorial004_py310.py hl[9] *}

すると、OpenAPI スキーマには含まれません。

サブアプリケーションのマウント

root_path を伴うプロキシを使用しつつサブアプリケーションをマウントする必要がある場合でも(サブアプリケーション - マウント{.internal-link target=_blank} 参照)、通常どおりに行えます。

FastAPI は内部で root_path を適切に扱うため、そのまま動作します。