跳到主要内容

Settings 与环境变量

在实际应用中,经常需要处理外部配置,如数据库连接字符串、API密钥、密钥等敏感信息。这些配置通常是可变的、敏感的,不应该硬编码在代码中。FastAPI 推荐使用 Pydantic Settings 来管理这些配置。

为什么需要配置管理

配置管理的常见场景:

  • 环境区分:开发、测试、生产环境使用不同配置
  • 敏感信息:数据库密码、API密钥等不应出现在代码中
  • 动态配置:配置可能随环境变化
  • 安全要求:敏感配置不应提交到版本控制系统

Pydantic Settings

安装

pip install pydantic-settings
# 或安装 FastAPI 全部依赖
pip install "fastapi[all]"

基本使用

创建一个 Settings 类,继承 BaseSettings

from fastapi import FastAPI
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
"""应用配置"""
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
debug: bool = False

# 创建配置实例
settings = Settings()

app = FastAPI()

@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}

环境变量读取

Pydantic Settings 会自动从环境变量读取配置。环境变量名称不区分大小写:

# 设置环境变量并运行应用
ADMIN_EMAIL="[email protected]" APP_NAME="My API" fastapi dev main.py

当环境变量设置后,Settings 类会自动读取:

  • ADMIN_EMAIL -> admin_email
  • APP_NAME -> app_name
  • items_per_user 保持默认值 50

类型转换与验证

Pydantic Settings 继承了 Pydantic 的所有验证功能:

from pydantic import Field, validator
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
# 自动类型转换
database_port: int = 5432

# 使用 Field 添加验证
max_connections: int = Field(default=10, ge=1, le=100)

# 字符串验证
api_key: str = Field(min_length=32, max_length=64)

# 自定义验证器
@validator('api_key')
def validate_api_key(cls, v):
if not v.isalnum():
raise ValueError('API Key 只能包含字母和数字')
return v

配置文件组织

分离配置模块

将配置放在单独的文件中,便于管理:

app/
├── __init__.py
├── config.py # 配置定义
├── main.py # 应用入口
└── routers/
└── users.py

config.py:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
database_url: str
secret_key: str
debug: bool = False

# 模型配置
model_config = {
"env_file": ".env", # 从 .env 文件读取
"case_sensitive": False # 环境变量名不区分大小写
}

# 创建全局配置实例
settings = Settings()

main.py:

from fastapi import FastAPI
from .config import settings

app = FastAPI(title=settings.app_name)

@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
}

使用 .env 文件

当配置项很多时,使用 .env 文件更方便管理。

创建 .env 文件

# .env
APP_NAME=ChimichangApp
ADMIN_EMAIL=[email protected]
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=your-secret-key-here
DEBUG=true

配置读取 .env 文件

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
database_url: str
secret_key: str
debug: bool = False

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False
)

settings = Settings()

.env 文件最佳实践

  1. 不要提交 .env 文件到版本控制
# .gitignore
.env
.env.local
.env.*.local
  1. 提供示例文件
# .env.example
APP_NAME=My API
ADMIN_EMAIL=
DATABASE_URL=
SECRET_KEY=
DEBUG=false
  1. 区分环境
.env.development   # 开发环境
.env.testing # 测试环境
.env.production # 生产环境

根据环境加载不同配置:

import os
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
# ... 配置项

model_config = SettingsConfigDict(
env_file=f".env.{os.getenv('ENVIRONMENT', 'development')}"
)

依赖注入方式

使用依赖注入提供配置,便于测试时覆盖:

定义配置依赖

config.py:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
database_url: str

model_config = {"env_file": ".env"}

main.py:

from functools import lru_cache
from typing import Annotated
from fastapi import Depends, FastAPI
from .config import Settings

app = FastAPI()

@lru_cache
def get_settings() -> Settings:
"""获取配置(缓存)"""
return Settings()

# 类型别名
SettingsDep = Annotated[Settings, Depends(get_settings)]

@app.get("/info")
async def info(settings: SettingsDep):
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
}

lru_cache 的作用

@lru_cache 装饰器确保 Settings 对象只创建一次:

  • 避免每次请求都读取 .env 文件
  • 提高性能,减少 I/O 操作
  • 但仍然可以通过依赖覆盖进行测试

测试配置

使用依赖注入方式后,测试时可以轻松覆盖配置:

from fastapi.testclient import TestClient
from .config import Settings
from .main import app, get_settings

# 测试配置
def get_settings_override():
return Settings(
app_name="Test API",
admin_email="[email protected]",
database_url="sqlite:///:memory:"
)

# 覆盖依赖
app.dependency_overrides[get_settings] = get_settings_override

client = TestClient(app)

def test_info():
response = client.get("/info")
assert response.json() == {
"app_name": "Test API",
"admin_email": "[email protected]",
}

# 清除覆盖
app.dependency_overrides.clear()

完整示例

项目结构

app/
├── __init__.py
├── config.py
├── database.py
├── main.py
└── routers/
├── __init__.py
└── users.py
.env
.env.example

配置文件

config.py:

from functools import lru_cache
from pydantic import Field, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
"""应用配置"""

# 应用配置
app_name: str = "FastAPI App"
app_version: str = "1.0.0"
debug: bool = False

# 服务器配置
host: str = "0.0.0.0"
port: int = Field(default=8000, ge=1, le=65535)

# 数据库配置
database_url: str
database_pool_size: int = Field(default=5, ge=1)
database_max_overflow: int = Field(default=10, ge=0)

# 安全配置
secret_key: str = Field(min_length=32)
access_token_expire_minutes: int = Field(default=30, ge=1)

# CORS 配置
cors_origins: list[str] = Field(default_factory=lambda: ["*"])

# 模型配置
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False
)

@lru_cache
def get_settings() -> Settings:
"""获取配置实例(单例)"""
return Settings()

数据库配置

database.py:

from typing import Annotated
from fastapi import Depends
from sqlmodel import Session, create_engine
from .config import get_settings, Settings

def get_session(settings: Annotated[Settings, Depends(get_settings)]):
"""获取数据库会话"""
engine = create_engine(
settings.database_url,
pool_size=settings.database_pool_size,
max_overflow=settings.database_max_overflow
)

with Session(engine) as session:
yield session

SessionDep = Annotated[Session, Depends(get_session)]

应用入口

main.py:

from contextlib import asynccontextmanager
from typing import Annotated

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

from .config import Settings, get_settings
from .database import SessionDep

@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时执行
settings = get_settings()
print(f"启动应用: {settings.app_name}")
yield
# 关闭时执行
print("应用关闭")

def create_app() -> FastAPI:
settings = get_settings()

app = FastAPI(
title=settings.app_name,
version=settings.app_version,
debug=settings.debug,
lifespan=lifespan
)

# CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

return app

app = create_app()

@app.get("/")
async def root(settings: Annotated[Settings, Depends(get_settings)]):
return {
"app_name": settings.app_name,
"version": settings.app_version
}

@app.get("/health")
async def health():
return {"status": "healthy"}

.env.example

# 应用配置
APP_NAME=FastAPI App
APP_VERSION=1.0.0
DEBUG=false

# 服务器配置
HOST=0.0.0.0
PORT=8000

# 数据库配置
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
DATABASE_POOL_SIZE=5
DATABASE_MAX_OVERFLOW=10

# 安全配置
SECRET_KEY=your-secret-key-at-least-32-characters-long
ACCESS_TOKEN_EXPIRE_MINUTES=30

# CORS 配置(逗号分隔)
CORS_ORIGINS=["http://localhost:3000", "https://example.com"]

高级配置

环境嵌套配置

复杂配置可以使用嵌套模型:

from pydantic import BaseModel
from pydantic_settings import BaseSettings

class DatabaseConfig(BaseModel):
"""数据库配置"""
url: str
pool_size: int = 5
echo: bool = False

class AuthConfig(BaseModel):
"""认证配置"""
secret_key: str
token_expire: int = 30

class Settings(BaseSettings):
"""应用配置"""
app_name: str = "API"
database: DatabaseConfig
auth: AuthConfig

model_config = {"env_nested_delimiter": "__"}

# 环境变量:
# DATABASE__URL=postgresql://...
# DATABASE__POOL_SIZE=10
# AUTH__SECRET_KEY=...

配置优先级

Pydantic Settings 的配置优先级(从高到低):

  1. 初始化时传入的参数
  2. 环境变量
  3. .env 文件
  4. 默认值
# 最高优先级:初始化参数
settings = Settings(app_name="Override")

# 次优先级:环境变量
# export APP_NAME="From Env"

# 再次:.env 文件
# APP_NAME=From Env File

# 最低:默认值
# app_name: str = "Default"

敏感信息处理

对于敏感信息,建议:

  1. 使用环境变量:不在 .env 文件中存储生产密钥
  2. 使用密钥管理服务:如 AWS Secrets Manager、HashiCorp Vault
  3. 加密存储:敏感配置加密后存储
import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
# 从环境变量读取敏感配置
secret_key: str = Field(default_factory=lambda: os.getenv("SECRET_KEY", ""))
database_password: str = Field(default_factory=lambda: os.getenv("DB_PASSWORD", ""))

小结

本章我们学习了:

  1. Pydantic Settings 基础:创建配置类,自动读取环境变量
  2. 配置文件组织:分离配置模块,使用 .env 文件
  3. 依赖注入方式:便于测试和覆盖配置
  4. 测试配置:在测试中覆盖配置
  5. 高级配置:嵌套配置、优先级、敏感信息处理

配置管理的最佳实践:

  • 敏感信息使用环境变量,不提交到版本控制
  • 使用 .env.example 提供配置模板
  • 使用依赖注入方式便于测试
  • 区分不同环境的配置文件
  • 使用类型注解和验证确保配置正确

练习

  1. 创建一个完整的配置系统,包含数据库、缓存、邮件服务配置
  2. 实现不同环境(开发、测试、生产)的配置切换
  3. 使用依赖注入方式提供配置,并编写测试用例验证配置覆盖
  4. 实现一个从 YAML 文件读取配置的扩展(可选)