在上一部分中,我们讨论了如何把 FastAPI 应用部署到生产环境,并通过环境变量在同一个镜像下切换不同环境的数据库地址和运行模式。但是我们并没有系统地去讨论“配置”这件事本身应该怎么设计和管理。 配置管理看起来琐碎,却是应用在不同环境中正确运行、安全运行的基础。
这一部分我们会专门把“应用配置管理”拆开来讲,从“为什么不能把配置写死在代码里”开始,到如何用 pydantic-settings 做类型安全的环境变量加载,再到 .env 文件、多环境差异和密钥管理,建立起一套清晰可维护的配置体系。
很多初学者会习惯把数据库连接串、第三方 API 的地址和密钥直接写在代码里,比如在一个 config.py 里写死 DATABASE_URL = "postgresql://..."。这样在本地跑起来很快,但一旦要换环境就会出问题。开发时连的是本地数据库,测试环境要连另一套库,生产环境又要连托管在云上的数据库;如果这些差异都靠改代码或改配置文件再提交到仓库,就很容易把生产密码误提交进去,或者把测试环境的配置部署到生产。更合理的方式是:代码和镜像里不包含任何环境特有的、尤其是敏感的信息,所有因环境而变的量都通过“配置”在运行时注入,而配置本身由部署环境提供,例如环境变量、外部密钥管理服务或受权限控制的配置文件。
这样做的另一个好处是可重复性和一致性。同一份代码、同一个镜像,在开发机上用一套环境变量连开发库,在 CI 里用另一套变量跑测试,在生产用再一套变量连生产库,行为完全由配置决定,而不是由“这台机器上曾经改过哪行代码”决定。排查问题时,我们只需要看当前环境注入的配置是什么,而不必去猜代码里写死了什么。因此,在讨论具体技术方案之前,我们先确立一个原则:应用需要的所有因环境而变的项,都应视为配置,并通过明确的、非代码的渠道注入;代码只负责读取配置并使用,不负责“写死”任何环境或密钥信息。
在“十二要素应用”等现代应用方法论里,环境变量被推荐为配置的主要来源。环境变量由进程的启动环境提供,不需要在代码或镜像里包含敏感内容,也便于在 Docker、Kubernetes、systemd 或各类云平台中统一注入。在 Python 里,我们最直接的做法是用 os.environ 或 os.getenv 读取。例如应用需要数据库连接串和当前环境名称,可以这样写:
|import os DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./dev.db") ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
这里用 getenv 的第二个参数提供默认值,在本地未设置环境变量时会退回到开发常用的 SQLite 和 development。这种方式简单,但缺点也很明显。一是没有类型,DATABASE_URL 在代码里是字符串,如果某处误以为它是 None 或别的类型,要等到运行时才暴露问题。二是没有集中校验,如果生产环境漏配了 DATABASE_URL,应用可能直到第一次访问数据库时才报错,而不是在启动时就失败。三是默认值散落在各处,改起来容易遗漏。所以我们更推荐把“从环境变量读取并校验”这件事交给专门的配置层,在应用启动时一次性加载并校验,通过即使用,不通过则直接退出并报错。在 FastAPI 生态里,这一层通常由 pydantic-settings 完成。
pydantic-settings 基于 Pydantic 模型,把环境变量(以及可选的 .env 文件)映射到带类型的字段上,并在实例化时做校验。这样我们就得到一份“配置对象”:所有字段都有明确类型,缺失必填项或格式不对会在启动时立刻抛错,默认值也集中定义在一处。先安装依赖:
|pip install pydantic-settings
然后我们定义一个配置类,继承自 BaseSettings。在 Pydantic v2 时代,对应的基类来自 pydantic_settings:
|from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) app_name: str = "My FastAPI App" environment: str = "development" debug: bool = False
这里我们通过类型注解声明了每个配置项的类型和默认值。secret_key 没有默认值,因此必须在环境变量或 .env 里提供,否则创建 Settings() 时就会抛出验证错误。model_config 里我们指定了 env_file=".env",这样在本地开发时可以从项目根目录的 .env 文件加载变量,而部署时仍然可以完全依赖真实环境变量,因为环境变量的优先级通常高于 .env 文件。extra="ignore" 表示未在模型中声明的环境变量会被忽略,不会导致报错,这样就不会因为多设了一些与当前应用无关的变量而影响启动。
在应用入口处,我们只创建一次配置实例,并尽量通过依赖注入或模块级单例提供给需要它的地方,避免在多个模块里各自读环境变量:
|from fastapi import FastAPI, Depends from .config import Settings def get_settings() -> Settings: return Settings() app = FastAPI() @app.get("/info") def info(settings: Settings = Depends(get_settings)): return { "app_name": settings.app_name, "environment": settings.environment, "debug": settings.debug, }
这样,所有路由只要通过 Depends(get_settings) 就能拿到同一份配置,类型明确,也方便在测试里用不同的配置实例替换。若在启动时 Settings() 校验失败,FastAPI 应用根本不会正常跑起来,从而避免“跑起来才发现配置错了”的情况。

默认情况下,pydantic-settings 会把模型中的字段名转成大写,然后去环境变量里查找同名键。例如字段 database_url 对应环境变量 DATABASE_URL,secret_key 对应 SECRET_KEY。如果我们的应用会和其他服务一起部署,希望所有配置都带一个统一前缀以免冲突,可以在 model_config 里设置 env_prefix。例如:
|class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_prefix="MYAPP_", extra="ignore", ) database_url: str = "sqlite:///./dev.db"
这样只有名为 MYAPP_DATABASE_URL 的环境变量会被读取并映射到 database_url,而其他进程设置的 DATABASE_URL 不会影响我们。在微服务或 sidecar 共处同一环境时,前缀能有效避免配置串台。若某个字段需要和与默认命名不同的环境变量对应,可以使用 Pydantic 的 Field(alias="ENV_VAR_NAME") 显式指定别名,这样既能保持代码里的字段名清晰,又能兼容已有的部署脚本或 CI 中使用的变量名。
pydantic-settings 在解析每个字段时,会按一定顺序合并多个来源的值,通常环境变量的优先级高于 .env 文件中的同名项,这样在 Docker 或 Kubernetes 里显式注入的变量会覆盖镜像或挂载文件里的默认值。若我们除了 .env 之外还有多份环境文件(例如 .env.development、.env.test),可以通过 env_file 传入多个路径,或根据当前环境动态选择要加载的文件名,从而在本地开发时也能用“多环境文件”模拟不同配置,而不必改 .env 内容。理解来源与优先级后,我们就能在“本地默认”“CI 覆盖”“生产 Secret 覆盖”之间做到清晰分层,既不把敏感信息写进仓库,又保证每套环境拿到正确的值。
利用 Pydantic 的能力,我们不仅可以为配置项指定类型,还可以加上校验器,确保格式或取值范围合理。例如我们希望 environment 只能是有限的几个值,database_url 在非开发环境下不能是 SQLite,或者某些数值必须在合理区间内。下面是一个稍完整的例子:
|from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) app_name: str = "My FastAPI App" environment: str = "development"
这样,如果有人在环境变量里把 ENVIRONMENT 设成 prod(拼写错误)或把 API_PORT 设成 99999,应用在启动时就会直接报错并退出,并给出清晰的校验错误信息。对于更复杂的规则,我们还可以使用模型级校验(如 model_validator),例如“当 environment 为 production 时,database_url 不能包含 sqlite”等。把这类规则集中在配置模型里,可以避免配置错误悄悄进入运行时,也便于后续维护和文档化。
当某条规则涉及多个配置项时,单字段校验器就不够用了,这时可以在配置类上使用 model_validator。例如我们要求:当 environment 为 production 时,database_url 不能是 SQLite,且 secret_key 不能是占位值。可以在模型里加一个 @model_validator(mode="after") 方法,在实例构造完成后检查这些条件,不满足则抛出 ValueError。这样,所有“环境与配置组合是否合法”的逻辑都收拢在配置模型内,调用方只需拿到一个已校验过的 Settings 实例,无需再在业务代码里散落判断。
有些配置项在某些环境下不需要,或者我们希望在“未设置”时有一个明确的语义。在 Pydantic 里,我们可以把字段设为 Optional[T] 并给 None 作为默认值,这样环境变量未设置时该字段就是 None,业务代码可以根据是否为 None 决定是否启用某功能。另一些项我们希望“必须有值但在开发环境可以弱一点”,例如 secret_key 在开发时可以给一个占位字符串,在生产则必须由外部注入且足够强。这类策略可以通过默认值 + 校验器组合实现:开发默认值写在模型里,校验器里根据 environment 判断,若为 production 则对 secret_key 做长度或复杂度检查。这样既保证了生产安全,又不会在本地开发时强迫大家设置复杂密钥。
在本地开发时,我们通常不希望每次都在终端里导出一大串环境变量,而是希望把“本地专用”的配置写在一个文件里,由 pydantic-settings 自动读取。这个文件一般命名为 .env,放在项目根目录,并且务必加入 .gitignore,避免把本地或敏感信息提交到仓库。一个典型的 .env 示例:
|ENVIRONMENT=development DEBUG=true DATABASE_URL=sqlite:///./dev.db SECRET_KEY=dev-secret-do-not-use-in-production API_PORT=8000
这里所有键名与我们在 Settings 里定义的字段名对应(pydantic-settings 默认会把字段名转成大写并从环境变量中查找)。在本地运行 uvicorn app.main:app --reload 时,只要工作目录是项目根目录,pydantic-settings 就会自动加载 .env,我们无需再手动 export。若同时存在环境变量和 .env 中的同名变量,通常环境变量优先,这样在 CI 或容器里显式注入的值会覆盖 .env,避免本地文件影响远程环境。
不要把 .env 提交到版本控制。应在 .gitignore 中加入 .env,并视情况提供 .env.example 或 .env.sample,只列出变量名和示例值(无真实密钥),供其他开发者或部署文档参考。
我们可以提供一个 .env.example,例如:
|ENVIRONMENT=development DEBUG=true DATABASE_URL=sqlite:///./dev.db SECRET_KEY=your-secret-key-here API_PORT=8000
新成员克隆项目后,复制为 .env 并填入本地或环境专用的值即可,既降低了上手成本,又不会把真实密钥写进仓库。
当配置项很多时,我们可能希望把“嵌套”结构映射到环境变量里,例如把“Redis 的主机、端口、数据库编号”组织成一个逻辑组。pydantic-settings 支持通过 env_nested_delimiter 把带分隔符的变量名解析成嵌套结构,例如设置 env_nested_delimiter="__" 后,环境变量 REDIS__HOST、REDIS__PORT 会对应到配置模型中的一个 redis 子对象,其字段为 host 和 port。这样我们既能保持配置模型的结构清晰,又能在 .env 或部署脚本里用扁平键名设置,适合需要区分多组外部服务(数据库、缓存、消息队列等)的项目。在本地开发时,可以把这些变量都写在 .env 里,按组组织注释,便于阅读和修改。
同一套代码会在开发机、CI 测试环境、预发环境和生产环境中运行,而不同环境下的数据库、日志级别、外部 API 地址甚至功能开关往往不同。我们应通过“配置”区分这些环境,而不是通过改代码或打分支。一种常见做法是用一个顶层配置项(如 ENVIRONMENT)表示当前环境,其余配置项再根据该值或直接通过环境变量区分。例如在生产环境我们关闭 debug、使用强密钥、连接生产数据库;在测试环境我们可能使用内存数据库或独立测试库,并打开更多日志。这些都可以在同一份 Settings 模型中完成,只是在不同环境中注入不同的环境变量(或在 CI/容器编排中挂载不同的 .env 或 Secret)。
在代码里,我们应避免到处写 if settings.environment == "production" 的散落判断,而是尽量把“环境相关”的差异封装在配置或依赖里。例如数据库连接串、日志级别、是否启用 Swagger 等,都由配置决定,业务逻辑只依赖“当前配置下的值”,这样测试时只要换一套配置即可模拟不同环境,而不必改业务代码。
FastAPI 的 lifespan 和依赖注入非常适合做“按配置初始化”。在 lifespan 里我们可以读取 get_settings(),根据 settings.environment 或具体项决定是否打开调试文档、连接哪套数据库、注册哪些中间件。例如只有在非生产环境下才把 Swagger UI 挂到 /docs,或者根据 database_url 在启动时创建连接池并在关闭时释放。依赖函数里同样可以基于 Settings 返回不同的实现,例如测试时 get_db 返回内存库或测试库的会话,生产时返回真实库的会话,这样业务路由无需关心当前环境,只要依赖注入的接口一致即可。把“环境差异”收敛到配置和依赖层,能让核心业务代码更干净,也更容易做单测和集成测试。
在编写单元测试或集成测试时,我们经常希望应用使用测试专用配置,例如内存数据库、固定密钥、关闭外部 API 调用等。由于配置是通过 Depends(get_settings) 注入的,我们可以在测试里用 FastAPI 的 app.dependency_overrides 把 get_settings 替换成一个返回“测试用 Settings 实例”的函数,这样无需改环境变量或启动子进程,就能让被测应用在测试配置下运行。例如在 pytest 的 fixture 里构造一个 Settings(database_url="sqlite://", environment="testing", ...) 并设置 app.dependency_overrides[get_settings] = lambda: test_settings,之后所有请求都会使用这份配置。这种做法的好处是测试与生产使用同一套配置模型,只是值不同,避免了“测试里硬编码另一套逻辑”导致的行为不一致。

数据库密码、第三方 API 密钥、JWT 签名密钥等都属于敏感配置。它们绝不能出现在代码仓库、镜像层或日志里,而应通过环境变量或专门的密钥管理服务在运行时注入。在应用内部,我们应尽量缩小“能接触到明文密钥”的范围,例如只在必要的模块中通过 Settings 读取,而不是把密钥当作普通字符串在多层之间传递。若框架或库支持从回调或环境变量按需读取,也可以避免在内存中长期持有明文。
即使是本地开发,也建议养成习惯:.env 中的 SECRET_KEY 使用明显的占位值(如 dev-only-secret),并确保该文件不提交;生产环境的密钥则只在部署流水线或密钥管理服务中设置,不写入任何代码或普通配置文件。这样一旦仓库或镜像泄露,攻击者也无法直接拿到生产密钥。
在打印或记录配置时,我们必须避免把 secret_key、database_url(可能含密码)等敏感字段原样输出到日志或错误页。一种做法是在配置类上实现一个“安全序列化”方法,返回一份用于日志的字典,其中敏感字段被替换成 *** 或仅显示前几位。另一种做法是在日志格式化时统一过滤掉已知的敏感键名。若使用 Pydantic,也可以为敏感字段设置 json_schema_extra 或自定义序列化,让 model_dump() 的某种变体自动脱敏。这样即使误把配置对象传入日志,也不会泄露密钥。
在规模较大的团队或云上部署时,把密钥写在环境变量或服务器上的文件里仍然有泄露和分散管理的风险。此时可以引入密钥管理服务,由专门的服务集中存储和下发密钥,应用在启动或运行时向该服务请求所需密钥,而不是在部署脚本或配置文件中明文书写。常见的方案包括云厂商提供的托管服务(如 AWS Secrets Manager、Azure Key Vault、阿里云 KMS/Secrets Manager)以及自建的 HashiCorp Vault。这类服务通常支持权限控制、审计日志和密钥轮换,应用通过 IAM 角色或短期凭证访问,拿到的密钥只在内存中使用,不落盘、不写日志。
在 FastAPI 应用中,我们可以把“从密钥管理服务拉取并填充到配置”的逻辑放在启动阶段。例如在创建 Settings 之前,先调用云厂商 SDK 或 Vault API,把需要的键值取出来写入进程环境变量,然后再实例化 Settings,这样对现有代码来说配置来源仍然是环境变量,只是这些变量的值来自密钥服务而非手动配置。另一种方式是为 Settings 提供自定义的“来源”,在 pydantic-settings 中可以通过自定义 SettingsSource 从任意来源读取键值,从而把“从 Vault 拉取”集成进配置加载流程,实现集中管理和轮换时无需改业务代码。
若希望配置模型“原生”支持从 Vault 等外部源加载,可以实现一个继承自 pydantic_settings.SettingsSource 的类,在 get_field_value 中调用 Vault API,把指定键对应的值返回,然后在 Settings 的 model_config 里通过 customise_sources 把这个来源加入来源列表,并设定其与环境变量、.env 的优先级。这样在实例化 Settings() 时,pydantic-settings 会按顺序从各来源拉取值,Vault 中的键可以覆盖或补充环境变量,而业务代码仍然只依赖一个统一的 Settings 对象,无需关心值究竟来自哪里。轮换密钥时,只需在 Vault 中更新值,应用在下次重启或按需拉取时就会拿到新值,无需重新构建镜像或改代码。
密钥轮换是指定期更换数据库密码、API 密钥等敏感配置。使用密钥管理服务后,可以在服务端更新密钥,应用在下次拉取或重启时自动拿到新值,无需重新打包镜像或改代码,从而既提升安全性,又便于运维。

下面我们把这一部分的内容串起来,给出一份可在项目中直接复用的结构。假设项目目录为 app,配置模块为 app/config.py,主入口为 app/main.py。
app/config.py 中定义配置模型并导出单例(或通过依赖注入提供):
|from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) app_name: str = "My FastAPI App" environment: str = "development"
在 app/main.py 中创建应用并挂载依赖:
|from contextlib import asynccontextmanager from fastapi import FastAPI, Depends from app.config import Settings, get_settings @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() # 可选:根据 settings 初始化数据库、缓存等 yield # 关闭资源 app = FastAPI(lifespan=lifespan) @app.get("/info") def info
需要连接数据库或使用密钥的路由,同样通过 Depends(get_settings) 拿到 settings,再从中取 database_url、secret_key 等即可。例如在“连接数据库”一章里我们介绍的数据库依赖,可以写成 def get_db(settings: Settings = Depends(get_settings)),在依赖内部用 settings.database_url 创建引擎或会话;认证逻辑里用 settings.secret_key 做 JWT 签名。这样,配置的加载、校验和注入都集中在一处,多环境与密钥管理只需在“如何为进程提供环境变量或自定义来源”这一层解决,应用代码保持简洁且可测。
这节课我们专门讨论了应用配置管理。我们明确了配置不应写死在代码里,而应通过环境变量或外部配置在运行时注入,以保证同一份代码和镜像能在多环境中复用,并避免敏感信息进入仓库。 我们介绍了使用 pydantic-settings 将环境变量(及可选的 .env 文件)映射到带类型和校验的配置模型,在应用启动时一次性加载和校验,并通过 FastAPI 的依赖注入把配置提供给路由和业务逻辑。 我们还讨论了 .env 在本地开发中的使用方式、多环境下的配置差异处理,以及敏感配置与密钥管理服务在安全与轮换方面的价值。
在接下来的学习中,我们会展望 FastAPI 的未来发展与生态,对框架的最新特性和改进计划进行探讨。