配置与密钥
同一份代码要跑在开发机、预发环境和生产环境上,差别只该在配置里。配置写进代码,等于给每个环境维护一个分支。
配置在运行时给
配置的两种常见载体是环境变量和配置文件。前者适合容器与 CI,后者适合项目里有一堆结构化选项的场景。
import os
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///local.db")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
API_KEY = os.environ["API_KEY"] # 必填,缺失直接抛 KeyErrordatabase:
url: 'postgresql://localhost/myapp'
pool_size: 5
server:
host: '0.0.0.0'
port: 8080
debug: false判断标准很简单:同一份代码能否只靠改配置部署到另一个环境。需要改代码才能上线,说明有配置被硬编码了。
不是所有东西都该进环境变量。数据库地址、超时、开关属于配置;业务规则、算法常量属于代码。边界一乱,配置就变成一堆没人知道是什么意思的字符串。
环境变量有个细节:值永远是字符串。os.environ["DEBUG"] 拿到的是 "false",它在 Python 里是真值,必须显式比较。YAML 里的 debug: false 会被解析成布尔假,两种载体别混着推理。
读取配置的代码要集中在一处。散落在十几个模块里的 os.environ 调用,最后没人能列出完整的配置面,也就没法校验和写文档。
一份代码多处部署
配置来源通常有多个,冲突时按固定顺序取,一般是:命令行参数高于环境变量,环境变量高于配置文件,配置文件高于代码里的默认值。
| 来源 | 谁改 | 典型用途 |
|---|---|---|
| 命令行参数 | 开发者临时调试 | --port 8001 |
| 环境变量 | 部署平台、CI | 数据库地址、密钥 |
| 配置文件 | 运维、仓库 | 业务参数 |
| 代码默认值 | 开发者 | 本地开箱即用 |
顺序背后的逻辑是「越临时的越优先」:一次调试不该改动仓库里的文件,一个环境的差异不该污染另一个环境。
缺失必填项要在启动时炸掉,不要等请求打进来才 500。启动即校验能省掉大量「明明部署成功了」的排查。
常见反模式是 if ENV == "prod": ...。代码里出现环境名判断,说明某个差异没被提炼成配置项,而是被写成了分支,改配置就变成了改代码。
密钥
密钥和普通配置的区别在于泄露后果。它不该出现在代码、日志、镜像层和 git 历史里。
# .env 只在本地,进 .gitignore
set -a
source .env
set +a
docker run --env-file .env myapp| 做法 | 问题 |
|---|---|
密钥写进 config.yaml | 提交即泄露,历史删不掉 |
密钥写进 Dockerfile 的 ENV | 留在镜像层里,docker history 可见 |
| 密钥打进前端 bundle | 任何人打开开发者工具都能看到 |
| 密钥写进 CI 日志 | 日志权限往往比仓库宽 |
仓库里放 .env.example,列出键名和格式,不放值;.env 进 .gitignore。CI 和部署平台各自注入真实值。
密钥要能轮换,这要求它从配置里读、不从代码里算,也要求有多个有效密钥并存的时间窗口。定期轮换一次,能提前发现那些把密钥写死在别处的角落。
权限也要收窄:一个只读 token 够用就不要发读写 token,一把 key 只给一个服务。泄露时范围可控,吊销时也只影响一个地方。
环境变量也不是保险箱:docker inspect、/proc/<pid>/environ、崩溃转储都能把它打出来。规模上来后改用密钥管理服务,让应用在运行时按需取、拿短期凭证。
泄露往往发生在提交那一刻。pre-commit 挂上密钥扫描,能在第一道门拦住;已经有了长期暴露的凭据,扫描只是事后止损,该换的还是得换。
容器里的配置
容器把配置从「改文件」变成「注入」,方式有三条,敏感度不同:
| 方式 | 写法 | 适用 |
|---|---|---|
environment | 直接写键值 | 非敏感参数 |
env_file | 指向一个 .env 文件 | 本地与测试 |
secrets | 挂载到 /run/secrets/<name> | 生产密钥 |
前两种在 docker inspect 里能直接看到,第三种以文件形式出现在容器内,不进镜像层也不进 docker inspect 的输出。
配置项也是接口,改动要像改 API 一样谨慎。把环境变量名、默认值、取值范围写进 README,比让人去翻代码快得多。