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_emailAPP_NAME->app_nameitems_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 文件最佳实践
- 不要提交 .env 文件到版本控制:
# .gitignore
.env
.env.local
.env.*.local
- 提供示例文件:
# .env.example
APP_NAME=My API
ADMIN_EMAIL=
DATABASE_URL=
SECRET_KEY=
DEBUG=false
- 区分环境:
.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 的配置优先级(从高到低):
- 初始化时传入的参数
- 环境变量
.env文件- 默认值
# 最高优先级:初始化参数
settings = Settings(app_name="Override")
# 次优先级:环境变量
# export APP_NAME="From Env"
# 再次:.env 文件
# APP_NAME=From Env File
# 最低:默认值
# app_name: str = "Default"
敏感信息处理
对于敏感信息,建议:
- 使用环境变量:不在
.env文件中存储生产密钥 - 使用密钥管理服务:如 AWS Secrets Manager、HashiCorp Vault
- 加密存储:敏感配置加密后存储
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", ""))
小结
本章我们学习了:
- Pydantic Settings 基础:创建配置类,自动读取环境变量
- 配置文件组织:分离配置模块,使用
.env文件 - 依赖注入方式:便于测试和覆盖配置
- 测试配置:在测试中覆盖配置
- 高级配置:嵌套配置、优先级、敏感信息处理
配置管理的最佳实践:
- 敏感信息使用环境变量,不提交到版本控制
- 使用
.env.example提供配置模板 - 使用依赖注入方式便于测试
- 区分不同环境的配置文件
- 使用类型注解和验证确保配置正确
练习
- 创建一个完整的配置系统,包含数据库、缓存、邮件服务配置
- 实现不同环境(开发、测试、生产)的配置切换
- 使用依赖注入方式提供配置,并编写测试用例验证配置覆盖
- 实现一个从 YAML 文件读取配置的扩展(可选)