サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

FastAPIの設定管理入門:pydantic-settingsで環境変数・.env・Secretsを安全に扱う

green snake

Photo by Pixabay on Pexels.com

FastAPIの設定管理では、環境ごとに変わる値をアプリケーションコードから分離し、起動時に型変換と検証を済ませます。

pydantic-settingsを使うと、環境変数、.env、Secretsの値を一つの設定モデルへ集約できるため、設定漏れや形式の違いをアプリケーションの起動時に見つけやすくなります。

この記事では、最小構成から多環境、秘密情報、テスト、Feature Flagまでを、コピーして調整できる例で整理します。

設定と秘密情報を分けて考える

設定とは、デプロイ先によって変わるデータベース接続先、ログレベル、許可するオリジンなどの値です。

秘密情報とは、漏洩すると第三者に悪用される可能性があるパスワード、APIキー、署名鍵などを指します。

設定ソースは、それらの値を読み込む場所です。

設定と秘密情報は同じモデルで検証できますが、保存場所と閲覧権限は分けて設計します。

FastAPI自体の役割から確認したい場合は、FastAPIでAPIを作るときの設計ポイントも参照してください。

値が競合したときの読み込み順

pydantic-settingsは、同じ項目が複数の場所にある場合、既定では次の優先順位で値を選びます。

既定の設定ソース優先順位
優先度 設定ソース 主な用途
1 CLI引数 CLI解析を有効にした場合の一時指定
2 Settings(...)の引数 テストや明示的な上書き
3 プロセス環境変数 ステージングと本番の注入値
4 .env ローカル開発の補助
5 Secretsディレクトリ コンテナへマウントした秘密
6 モデルの既定値 秘密ではない安全な初期値

この記事のようにCLI解析を使わないWebアプリでは、コンストラクター引数、環境変数、.env、Secrets、既定値の順になります。

この挙動はPydantic Settings公式ドキュメントで確認できます。

The Twelve-Factor AppのConfigが示すように、デプロイごとに変わる値をコードへ埋め込まない設計にすると、同じコードを環境ごとに書き換えずに運用できます。

最小のSettingsクラス

依存パッケージは、実際に動作確認した組み合わせをロックファイルへ固定します。

pip install fastapi pydantic-settings python-dotenv

python-dotenvは、.envを読み込む場合に必要です。

次のモデルは、文字列、真偽値、URL、リスト、秘密情報を一か所で受け取り、項目間の矛盾も検査します。

from functools import lru_cache
from typing import Literal

from pydantic import AnyUrl, Field, SecretStr, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


Environment = Literal["dev", "stg", "prod"]


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    app_name: str = "My FastAPI"
    environment: Environment = "dev"
    log_level: Literal["debug", "info", "warning", "error"] = "info"

    database_url: str
    cors_origins: list[str] = Field(
        default_factory=lambda: ["http://localhost:3000"]
    )

    external_api_base: AnyUrl | None = None
    external_api_key: SecretStr | None = None
    secret_key: SecretStr

    enable_new_search: bool = False

    @model_validator(mode="after")
    def validate_combinations(self):
        api_base_set = self.external_api_base is not None
        api_key_set = self.external_api_key is not None

        if api_base_set != api_key_set:
            raise ValueError(
                "external_api_baseとexternal_api_keyは同時に設定してください"
            )

        if self.environment == "prod" and self.log_level == "debug":
            raise ValueError("本番環境ではdebugログを許可しません")

        return self


@lru_cache
def get_settings() -> Settings:
    return Settings()

Literalは受け付ける文字列を限定し、AnyUrlはURLとして解釈できるかを検証します。

SecretStrはrepr()やstr()で値を伏せますが、秘密へのアクセス権を管理する仕組みではありません。

実値を取り出すget_secret_value()を使った結果や設定モデル全体を、ログやAPIレスポンスへ渡さない運用が必要です。

Pydanticの型と検証を先に学ぶ場合は、Pydantic v2による入力検証の解説が補助になります。

.envはローカル開発の補助にする

.env.exampleには項目名と安全な例だけを置き、実値を入れた.envはバージョン管理から除外します。

APP_NAME="My FastAPI"
ENVIRONMENT=dev
DATABASE_URL="sqlite:///./app.db"
CORS_ORIGINS='["http://localhost:3000"]'
SECRET_KEY="replace-this-for-local-development"
ENABLE_NEW_SEARCH=false

環境変数は文字列として渡され、pydantic-settingsが宣言した型へ変換します。

listやdictなどの複合型は既定でJSONとして解釈されるため、CORS_ORIGINSにはカンマ区切りの文字列ではなくJSON配列を渡します。

env_file='.env'の相対パスは、原則としてアプリケーションを起動した作業ディレクトリを基準に解決されます。

実行場所が変わる構成では、起動手順を固定するか、確認可能なパスを明示します。

開発、ステージング、本番の差分

環境名ごとに別のアプリケーションコードを持つのではなく、同じSettingsモデルへ環境固有の値を注入します。

環境ごとの設定ソース例
環境 主な設定ソース 扱い方
開発 .envと安全な既定値 実データの認証情報を共有しない
ステージング CI/CDまたは実行基盤の環境変数 本番と同じ項目を別の値で検証する
本番 Secret ManagerまたはSecretsのマウント 権限、監査、失効、更新手順を決める

環境差分を値として渡せば、CORS、ログ、データベース、外部APIの切り替えをアプリケーション内の分岐へ散らさずに済みます。

設定ファイルをどこへ置くかは、FastAPIのプロジェクト構成と合わせて決めると役割が明確になります。

設定が増えたら関心ごとに分ける

データベースやメールなどの項目が増えた場合は、ネストしたモデルへ分けると、同じ用途の値をまとめて検証できます。

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseSettings(BaseModel):
    url: str
    pool_size: int = 5


class NestedSettings(BaseSettings):
    model_config = SettingsConfigDict(env_nested_delimiter="__")

    db: DatabaseSettings
DB__URL="postgresql+psycopg://user:password@db/app"
DB__POOL_SIZE=10

env_nested_delimiter='__'を指定すると、DB__URLをdb.urlへ対応付けられます。

区切り文字を設定せず、変数名だけを二重アンダースコアにしても、ネストした項目としては読み込まれません。

アプリケーションでは同じ設定インスタンスを使う

Settings()は生成するたびに設定ソースを読み直すため、FastAPIではキャッシュした依存関数を共有できます。

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.core.settings import Settings, get_settings


settings = get_settings()
app = FastAPI(title=settings.app_name)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.cors_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/meta")
def read_meta(
    current: Annotated[Settings, Depends(get_settings)],
):
    return {
        "app": current.app_name,
        "environment": current.environment,
        "log_level": current.log_level,
    }

モジュール読み込み時にget_settings()を呼ぶこの例では、必須値の不足や検証エラーによってアプリケーションの起動が止まります。

FastAPI公式の設定ガイドも、lru_cacheで同じインスタンスを再利用する構成を示しています。

環境変数や秘密を実行中に更新しても、キャッシュ済みの値は自動では変わりません。

変更の反映は、プロセスの再起動を基本とし、無停止の再読み込みが必要な場合だけ整合性と失敗時の扱いを別途設計します。

秘密情報を保存場所からアプリへ渡す

本番の秘密情報は、リポジトリやコンテナイメージへ書き込まず、実行基盤から環境変数またはファイルとして渡します。

pydantic-settingsには、ファイル名を項目名としてSecretsディレクトリから値を読む仕組みがあります。

model_config = SettingsConfigDict(
    secrets_dir="/run/secrets",
)

たとえば/run/secrets/secret_keyの内容をsecret_keyへ読み込めます。

同じ項目が環境変数にも存在する場合は、既定の優先順位では環境変数が選ばれます。

Docker Secretsはコンテナ内の/run/secretsへ秘密をマウントでき、Kubernetes SecretはPodへ環境変数またはファイルとして渡せます。

ただし、Kubernetes Secretは作成しただけで保存時暗号化が保証されるものではありません。

暗号化、最小権限のRBAC、アクセス対象の制限、必要に応じた外部Secret Storeを組み合わせます。

クラウド側の保管、ローテーション、監査を詳しく検討する場合は、AWS Secrets Managerの運用ガイドも役立ちます。

Kubernetes側の構成要素は、FastAPIとKubernetesのデプロイ手順で確認できます。

バリデーションをテストとCIで確かめる

設定モデルの単体テストでは、.envを読ませずに必要な値を直接渡すと、検証条件だけを確認できます。

import pytest
from pydantic import ValidationError

from app.core.settings import Settings


def test_prod_rejects_debug_log():
    with pytest.raises(ValidationError):
        Settings(
            _env_file=None,
            environment="prod",
            log_level="debug",
            database_url="sqlite:///./test.db",
            secret_key="test-only-secret",
        )

依存関数を通すテストで環境変数を変更した場合は、テストの前後でget_settings.cache_clear()を呼び、別のテストへキャッシュを持ち越さないようにします。

CIでは、デプロイ前にSettingsを一度生成する検査を置くと、必須値の欠落と形式不正を早い段階で検出できます。

python -c "from app.core.settings import Settings; Settings()"

Feature Flagを設定として扱う範囲

Feature Flagは、デプロイせずに機能の有効と無効を切り替える値です。

単純な真偽値は環境単位の切り替えに向きます。

def search(query: str, settings: Settings):
    if settings.enable_new_search:
        return new_engine(query)
    return legacy_engine(query)

利用者ごとの段階配布には、対象の決め方、割り当ての一貫性、計測、停止条件が別に必要です。

Feature Flagは認可の代わりにはならないため、利用権限の判定は認証と認可の仕組みに残します。

期限と削除担当を決め、ロールアウト後に不要になった分岐を残さない運用も組み込みます。

症状から確認箇所を絞る

設定管理で起きやすい問題
症状 確認する箇所
本番だけ必須値がない 実行基盤の注入名とSettingsの項目名が一致しているか
リストを読み込めない 環境変数の値がJSON配列になっているか
DB__URLが反映されない env_nested_delimiter='__'を指定したか
変更した値が反映されない キャッシュまたは実行中のプロセスに古い値が残っていないか
秘密がログに出た 実値を取り出した箇所と設定モデル全体の出力処理を確認する
Kubernetes Secretの保護が不十分 保存時暗号化、RBAC、Podからの参照範囲を確認する
Feature Flagが増え続ける 期限、担当者、削除条件が登録されているか

導入時の確認リスト

  1. 環境ごとに変わる設定をコードからSettingsへ移す。
  2. 秘密ではない既定値と、必須入力にする値を分ける。
  3. .env.exampleだけを共有し、実値を含む.envを除外する。
  4. 複合型のJSON表現と、ネスト項目の区切り文字をチームで統一する。
  5. 項目間の矛盾をバリデーションへ書き、CIでSettingsを生成する。
  6. 本番の秘密について、保管、権限、監査、失効、ローテーションの手順を決める。
  7. 設定変更の反映方法を、プロセス再起動または設計済みの再読み込みに統一する。

最初の一歩は、散在している環境変数を一つのSettingsモデルへ集め、必須値の欠落を起動時に検出できる状態へ変えることです。

その後に秘密の保存場所と更新手順を整えれば、開発環境と本番環境の差をアプリケーションコードへ持ち込まずに管理できます。

公式資料

この記事に関連する株式会社greedenの取り組み

設定管理は、コードだけでなく、環境設計と運用手順まで一続きに整えて初めて機能します。株式会社greedenは、FastAPIを含む技術選定から設計、実装、テスト、リリース後の保守改善まで、Webシステム開発を一気通貫で支援します。

モバイルバージョンを終了