# pydantic-settings 実践ガイド：型安全な設定管理とシークレット保護で12-factorを実現する

> pydantic-settings公式ドキュメントに忠実に、BaseSettingsの型付き設定モデル、.envとenv_nested_delimiter、SecretStr/secrets_dirによるシークレット保護、設定の優先順位、settings_customise_sourcesによるJSON/TOML/クラウド(AWS/Azure/GCP)連携、CLIまで、本番運用に耐える設定管理を実コードで解説します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: Python, Pydantic, pydantic-settings, 設定管理, セキュリティ, 型安全
- URL: https://tomodahinata.com/blog/pydantic-settings-configuration-management-secrets-guide

## 要点

- os.environ直読みは「型はstr固定・存在保証なし・デフォルト散在・ログ漏洩」の四重苦。pydantic-settingsは設定を型付きモデルに集約し、起動時にFail Fastさせる
- シークレットはSecretStrで包めばrepr/ログ/シリアライズで自動マスクされ、.get_secret_value()でのみ取り出せる。Docker/k8sのファイルマウントはsecrets_dirで読む
- 値の優先順位は公式仕様で6段階：CLI引数＞初期化引数＞環境変数＞.env＞secrets_dir＞デフォルト。settings_customise_sourcesで並べ替え・拡張できる
- JSON/TOML/YAMLファイルやAWS/Azure/GCPのSecret Managerを設定ソースとして合成でき、環境ごとの設定を型安全に一元化できる
- 設定はlru_cacheでシングルトン化し、テストではSettings(...)への直接注入かmonkeypatchで上書きする。必須項目欠落はデプロイ前に検出する

---

## **導入：`os.environ["..."]` は本番設定の負債である**

設定を `os.environ["DATABASE_URL"]` で都度読むコードは、一見シンプルですが、本番で必ず破綻します。理由は 4 つ。

1. **型が `str` 固定**：`os.environ["MAX_CONNECTIONS"]` は文字列。`int(...)` 変換を読むたびに書き、忘れればバグる。
2. **存在保証がない**：環境変数を設定し忘れても、その値を**実際に使う瞬間**まで誰も気づかない。本番の深夜に `KeyError` で落ちる。
3. **デフォルトが散在する**：`os.environ.get("DEBUG", "false")` が各所にバラまかれ、どれが正なのか分からなくなる（DRY 違反）。
4. **シークレットがログに漏れる**：設定オブジェクトを `print` / ログ出力した瞬間、API キーやパスワードが平文で流出する。

**pydantic-settings は、この 4 つを構造的に解決します**。設定を**型付きの単一モデル**に集約し、環境変数・`.env`・シークレットファイル・クラウドの Secret Manager から自動でロードし、必須項目が欠けていれば**起動時に `ValidationError`** で止める（Fail Fast）。これは 12-factor App の「設定を環境に格納する」原則を、**型安全とシークレット非漏洩の両立**で実現する定石です。

本記事は [公式ドキュメント](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/)（pydantic-settings 2.14.x）に忠実に、それより一段わかりやすく、最小構成から本番のシークレット管理・クラウド連携までを実コードで解説します。Pydantic 本体の基礎は [Pydantic v2 実践ガイド](/blog/pydantic-v2-production-validation-type-safety) を、設定値の検証ロジックは [高度な型・カスタムバリデータ](/blog/pydantic-custom-types-annotated-validators-advanced-guide) を併せて参照してください。

---

## **1. 最小の設定モデル：起動時に Fail Fast させる**

`BaseSettings` は **`pydantic` 本体ではなく別パッケージ**です（v2 で分離されました）。

```bash
pip install pydantic-settings
```

```python
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_")

    database_url: str                       # APP_DATABASE_URL（必須）
    debug: bool = False                     # APP_DEBUG（"1"/"true" を bool へ型強制）
    max_connections: int = Field(default=10, gt=0)  # APP_MAX_CONNECTIONS


# アプリ起動時に一度だけ生成。必須項目が欠けていればここで即座に失敗する
settings = Settings()
```

> ⚠️ **インポート元に注意**：`BaseSettings` / `SettingsConfigDict` は `from pydantic_settings import ...`、一方 `SecretStr` / `Field` / `AliasChoices` は `from pydantic import ...` です。`from pydantic import BaseSettings` は v1 の書き方で、v2 では動きません。

**なぜこれが優れているのか？**
`Settings()` を**アプリの起動時に一度だけ**呼べば、設定が揃っていない状態でアプリは起動できなくなります（Fail Fast）。`settings.max_connections` は静的に `int` と分かり、`settings.databse_url` のようなタイポは型チェッカーが検出する。`debug: bool` は `"1"` / `"true"` のような環境変数文字列を**型強制で `bool` に変換**してくれる。「本番で初めて設定ミスに気づく」のではなく、**デプロイ前に検出**できるのが最大の価値です。

---

## **2. `.env` と複雑な型：ネストした設定を読む**

`SettingsConfigDict` で読み込み挙動を細かく制御できます。

```python
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict


class RedisConfig(BaseModel):
    host: str
    port: int = 6379


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",              # .env を読む
        env_file_encoding="utf-8",
        env_prefix="APP_",            # APP_ プレフィックスで名前衝突を防ぐ
        case_sensitive=False,         # 既定。大文字小文字を無視
        env_nested_delimiter="__",    # ネストを FOO__BAR で表現
        extra="ignore",               # 余分な環境変数を無視（forbid だと起動失敗）
    )

    allowed_hosts: list[str] = []     # JSON としてパースされる
    redis: RedisConfig
```

主要オプションと、環境変数からの読み方は次のとおりです。

| 設定 | 効果 |
| --- | --- |
| `env_file` | `.env` ファイルのパス |
| `env_prefix` | 環境変数名の接頭辞（`APP_DATABASE_URL` 等） |
| `env_nested_delimiter` | ネストモデルの区切り（`APP_REDIS__HOST`） |
| `case_sensitive` | 大文字小文字を区別するか（既定 `False`） |
| `extra` | 未知の環境変数の扱い（`ignore` / `forbid` / `allow`） |

`list` / `dict` / ネストモデルなどの**複雑な型は、環境変数を JSON としてパース**します。

```bash
export APP_ALLOWED_HOSTS='["a.com", "b.com"]'   # JSON 配列
export APP_REDIS='{"host": "cache", "port": 6380}'   # JSON オブジェクト、または↓
export APP_REDIS__HOST=cache                     # env_nested_delimiter で個別指定
export APP_REDIS__PORT=6380
```

> 💡 **JSON と区切り文字は併用できる**：`APP_REDIS` で JSON 全体を渡しつつ、`APP_REDIS__PORT` で一部だけ上書きする、といった指定が可能です。区切り文字（`__`）はフィールド名に含めないでください（パースが壊れます）。

---

## **3. シークレットを守る：`SecretStr` と `secrets_dir`**

設定管理で最も事故が多いのが**シークレットの漏洩**です。API キーやパスワードを普通の `str` で持つと、ログ出力・例外トレース・`model_dump()` のすべてで平文露出のリスクがあります。`SecretStr` はこれを構造的に防ぎます。

```python
from pydantic import SecretStr
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    api_key: SecretStr
    db_password: SecretStr


settings = Settings()

print(settings.api_key)                      # **********  ← マスクされる
print(settings.model_dump())                 # {'api_key': SecretStr('**********'), ...}
print(settings.api_key.get_secret_value())   # 実際の値（明示的に取り出した時だけ）
```

`SecretStr` で包んだ値は、**`repr` / `str` / シリアライズのすべてで `**********` にマスク**され、実際の値は `.get_secret_value()` を**明示的に呼んだときだけ**取り出せます。「うっかりログに出した」事故が、型レベルで起きなくなります。

### **H3: Docker / Kubernetes のシークレットは `secrets_dir`**

コンテナ環境では、シークレットを環境変数ではなく**ファイルとしてマウント**するのがベストプラクティスです（環境変数は `docker inspect` 等で見えてしまうため）。`secrets_dir` は、指定ディレクトリ内の**フィールド名と同名のファイル**を読み込みます。

```python
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    # /var/run/database_password というファイルの中身が database_password に入る
    model_config = SettingsConfigDict(secrets_dir="/var/run")

    database_password: str
```

Kubernetes の Secret を `/var/run` 配下にマウントすれば、コードを一切変えずにシークレットを安全に注入できます。

> ⚠️ **`secrets_dir` の挙動**：ファイル名はフィールド名（または alias）と一致させる必要があります。該当ファイルがなければ静かにスキップされ、次のソース（環境変数等）にフォールバックします。本番でシークレットが空になる事故を避けるため、必須フィールドにしておき、欠落を起動時に検出させてください。

---

## **4. 設定の優先順位：6 段階のルールを把握する**

同じ設定が複数のソースで指定されたとき、どれが勝つか。公式が定める優先順位は、**高い順に 6 段階**です。

| 優先 | ソース | 典型的な用途 |
| --- | --- | --- |
| 1（最強） | CLI 引数（`cli_parse_args` 有効時） | 一時的な上書き・運用コマンド |
| 2 | `Settings(...)` への初期化引数 | テストでの明示的な注入 |
| 3 | 環境変数 | 本番・ステージングの設定 |
| 4 | `.env` ファイル | ローカル開発 |
| 5 | `secrets_dir` のファイル | コンテナのシークレット |
| 6（最弱） | モデルのデフォルト値 | フォールバック |

この順序を理解していれば、「ローカルの `.env` を、本番では環境変数で上書きする」「テストでは初期化引数で全部上書きする」といった運用が予測可能になります。順序そのものは次章の `settings_customise_sources` で**並べ替え可能**です。

---

## **5. ソースを拡張する：JSON/TOML/YAML とクラウド Secret Manager**

設定ソースは環境変数だけではありません。`settings_customise_sources` をオーバーライドすれば、**任意のソースを合成・並べ替え**できます。

```python
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    TomlConfigSettingsSource,
)


class Settings(BaseSettings):
    model_config = SettingsConfigDict(toml_file="config.toml")

    app_name: str
    workers: int

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls,
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        # タプルの順 = 優先順位（先頭が最強）。環境変数 → TOML の順で解決する
        return (env_settings, TomlConfigSettingsSource(settings_cls))
```

組み込みのソースクラスが揃っています：`EnvSettingsSource` / `DotEnvSettingsSource` / `SecretsSettingsSource` / `JsonConfigSettingsSource` / `TomlConfigSettingsSource` / `YamlConfigSettingsSource` / `PyprojectTomlConfigSettingsSource` / `CliSettingsSource`。

### **H3: AWS / Azure / GCP の Secret Manager を直接読む**

pydantic-settings は、主要クラウドの Secret Manager との統合を**公式に同梱**しています。

```python
import os
from pydantic_settings import (
    AWSSecretsManagerSettingsSource,
    BaseSettings,
    PydanticBaseSettingsSource,
)


class Settings(BaseSettings):
    db_password: str
    api_key: str

    @classmethod
    def settings_customise_sources(cls, settings_cls, init_settings, env_settings,
                                   dotenv_settings, file_secret_settings):
        aws = AWSSecretsManagerSettingsSource(
            settings_cls, os.environ["AWS_SECRET_ID"]
        )
        return (init_settings, env_settings, aws)
```

| クラウド | ソースクラス | extra |
| --- | --- | --- |
| AWS | `AWSSecretsManagerSettingsSource` | `pydantic-settings[aws-secrets-manager]` |
| Azure | `AzureKeyVaultSettingsSource` | `pydantic-settings[azure-key-vault]` |
| GCP | `GoogleSecretManagerSettingsSource` | `pydantic-settings[gcp-secret-manager]` |

> 💡 **これがアーキテクチャ上効く理由**：シークレットを「環境変数にコピーするデプロイスクリプト」を書く必要がなくなり、**アプリが起動時に直接 Secret Manager から型付きで読む**。シークレットのローテーションもアプリ側のコード変更なしで反映でき、IAM で最小権限を効かせられる。CLAUDE.md の「シークレットをハードコードしない・最小権限」というセキュリティ原則を、設定レイヤーで体現します。なお各クラウド SDK（`boto3` 等）が必要なので、対応する extra を必ずインストールしてください。

---

## **6. CLI：設定モデルからコマンドラインツールを生やす**

同じ設定モデルから、**コマンドライン引数のパース**まで賄えます。バッチや運用スクリプトで重宝します。

```python
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(cli_parse_args=True)

    workers: int = 4
    verbose: bool = False


# python app.py --workers 8 --verbose で起動できる
settings = Settings()
print(settings.model_dump())
```

より構造化された CLI（サブコマンド等）には `CliApp` を使います。

```python
from pydantic_settings import CliApp

settings = CliApp.run(Settings, cli_args=["--workers", "8"])
```

CLI 引数は優先順位の最上位（第4章）なので、環境変数やデフォルトを一時的に上書きする運用に向きます。ネストフィールドは CLI では**ドット記法**（`--redis.port 6380`）で、環境変数では `__` 区切りで指定する、という違いに注意してください。

---

## **7. テストとベストプラクティス**

### **H3: シングルトン化（`lru_cache`）**

`Settings()` は毎回ソースを読みに行きます。アプリ全体で**一度だけ生成**するため、`lru_cache` でシングルトン化するのが定石です（FastAPI なら `Depends` と組み合わせます）。

```python
from functools import lru_cache


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

### **H3: テストでの上書き**

設定のテストは、**初期化引数による直接注入**が最もクリーンです（プロセスの環境変数を汚さないため、並列テストでも安全）。環境ロード経路そのものを検証したいときだけ `monkeypatch` を使います。

```python
def test_uses_injected_config():
    # 環境に依存せず、値を直接注入して上書き（優先順位2位＝環境変数より強い）
    settings = Settings(database_url="sqlite://", max_connections=1)
    assert settings.max_connections == 1


def test_reads_from_env(monkeypatch):
    monkeypatch.setenv("APP_MAX_CONNECTIONS", "20")  # プレフィックス付きで設定
    assert Settings().max_connections == 20
```

> 💡 **環境別設定の作法**：`env_file=(".env", f".env.{os.environ.get('ENV', 'local')}")` のように複数の `.env` を重ねるか、環境ごとに `Settings` サブクラスを分けます。いずれにせよ「設定の真実源は型付きモデル一つ」という原則を崩さないこと。設定のテスト容易性は [Pydantic テスト戦略](/blog/pydantic-testing-polyfactory-hypothesis-strategy-guide) でさらに掘り下げます。

---

## **結論：設定を「型システムの一部」にする**

pydantic-settings は、設定管理という地味だが事故の多い領域を、**型安全・Fail Fast・シークレット保護**の三点で本番品質に引き上げます。本記事の要点を再掲します。

1. **`BaseSettings`（別パッケージ）** で設定を型付きモデルに集約し、起動時に **Fail Fast** させる。
2. **`.env` / `env_prefix` / `env_nested_delimiter`** で柔軟に読み込み、複雑な型は JSON でパースする。
3. **`SecretStr`** でシークレットを自動マスクし、コンテナでは **`secrets_dir`** でファイルから読む。
4. **優先順位は 6 段階**（CLI＞init＞env＞.env＞secrets＞default）。`settings_customise_sources` で並べ替え・拡張できる。
5. **JSON/TOML/YAML とクラウド Secret Manager** を設定ソースとして合成し、シークレットを直接・安全に読む。
6. 設定は **`lru_cache` でシングルトン化**し、テストでは**直接注入**で上書きする。

「動くコード」と「10 年運用できるコード」の差は、設定とシークレットを**どこで・どう堰き止めるか**に表れます。pydantic-settings は、その境界を型で表現する最良の道具です。

公式の一次情報として、以下を本記事の観点で再読することをお勧めします。

- [Settings Management](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/)
- [pydantic-settings API](https://pydantic.dev/docs/validation/latest/api/pydantic_settings/)

---

### **本番運用に耐える設定・シークレット管理のご相談**

筆者は、環境分野のサーバーレス決済プラットフォームをはじめ、シークレットと設定の取り扱いが事業の信頼性に直結するシステムを設計・運用してきました。環境変数の散在・シークレットの平文露出・設定ミスによる本番障害——これらは「設定を型付きモデルに集約し、境界で検証する」だけで構造的に防げます。pydantic-settings を用いた **12-factor 準拠の設定管理・クラウド Secret Manager 連携・環境別設定の整理**を、生成 AI を活用して高速かつ高品質に実装します。お気軽にご相談ください。
