配置与密钥管理:环境变量与多环境
同一份镜像要能在开发机、测试环境和生产上跑,差别只在配置——数据库地址、Redis 密码、第三方密钥、日志级别。把配置写进代码或打进镜像,就等于每次改一个地址都要重新发版,还会把密钥散落在 Git 历史里。12-Factor 的第 3 条说得很直接:配置存在环境里,不存进代码。
配置该放在哪里
| 做法 | 是否推荐 | 问题 |
|---|---|---|
| 写死在代码里 | 不推荐 | 改配置要发版,多环境无法共用一份代码 |
| 打包进镜像 | 不推荐 | 镜像里有密钥,任何能拉镜像的人都能拿到 |
| 提交到 Git 的配置文件 | 不推荐 | 密钥永久留在历史里,删了也还在 |
| 环境变量(进程级) | 推荐 | 与代码解耦,容器友好 |
| 密钥文件挂载(权限 600) | 推荐 | 适合证书、SSH 私钥、大段凭据 |
.env 的用法
# 应用要读的环境变量,写到 .env(提交的是 .env.example)
cat > .env <<'EOF'
APP_ENV=prod
APP_PORT=8000
LOG_LEVEL=info
DATABASE_URL=mysql://app:change-me@mysql:3306/appdb
REDIS_URL=redis://:change-me@redis:6379/0
TZ=Asia/Shanghai
EOF
chmod 600 .env
# 本地调试时载入(生产交给 systemd 的 EnvironmentFile 或 Compose 的 environment)
set -a && source .env && set +a
规则很简单:.env 里放真值且绝不入库,仓库里放 .env.example 只保留变量名与说明。两者结构要一致,新同事复制改名改值即可,不要靠口口相传。
不要提交到 Git
cat >> .gitignore <<'EOF'
.env
.env.*
!.env.example
*.pem
*.key
*.p12
secrets/
EOF
# 检查是否已经有密钥进了历史(历史里的密钥删文件是删不掉的)
git log --all --pretty=format: --name-only | sort -u | grep -E '\.env|\.pem|\.key' || echo 'clean'
一旦密钥进过 Git,正确做法是立刻轮换密钥(作废旧的、生成新的),而不是只删文件——历史与各人本地克隆里都还留着。
密钥管理
| 方式 | 安全性 | 适用场景 |
|---|---|---|
| 环境变量 | 中,容易被日志或错误页打印出来 | 数据库地址、非敏感开关 |
| 密钥文件(600,属主为运行用户) | 较高 | 证书、SSH 私钥、服务账号 JSON |
| 云 KMS / 阿里云 KMS、腾讯云 SSM、AWS Secrets Manager | 高,有审计与轮换 | 多人协作、合规要求高的场景 |
| Vault 等自建密钥服务 | 高,运维成本也高 | 已有成熟运维体系 |
# 密钥文件权限:属主是运行用户,其他人一律不可读
sudo install -d -m 700 -o deploy -g deploy /home/deploy/apps/myapi/shared
sudo install -m 600 -o deploy -g deploy secrets.json /home/deploy/apps/myapi/shared/secrets.json
sudo -u deploy stat -c '%a %U:%G %n' /home/deploy/apps/myapi/shared/secrets.json # 期望 600 deploy:deploy
常见误区:以为把配置放进环境变量就安全。事实上应用打印 os.environ、框架把配置回显到错误页、docker inspect 都能看到环境变量。真正的密钥更适合走密钥文件或密钥管理服务。
多环境配置分层与覆盖顺序
| 环境 | 配置来源 | 特点 |
|---|---|---|
| dev 本地开发 | .env + 默认值 | 可随便改,连本地数据库 |
| staging 预发 | .env.staging + CI 注入的密钥 | 与生产同构,数据可随便造 |
| prod 生产 | 平台注入环境变量 + EnvironmentFile + KMS | 只有运维能改,改动要留痕 |
覆盖顺序固定为:代码内默认值 < 配置文件 < 环境变量 < 密钥服务。后面的覆盖前面的,且只允许更外层的来源覆盖更内层,避免出现“本地 .env 把生产地址覆盖掉了”的事故。
# 部署时显式指定环境与配置来源,避免误用本地配置
APP_ENV=prod \
DATABASE_URL="$(cat /home/deploy/apps/myapi/shared/db_url)" \
/home/deploy/apps/myapi/current/app --config shared/config.prod.yaml
配置校验:启动时 fail-fast
缺一个必填的 DATABASE_URL 就让进程启动失败,比带着坏配置跑起来、在第一个请求时报 500 要好得多。
# Python 示例:启动时一次性校验并给出明确报错
import os, sys
REQUIRED = ["APP_ENV", "DATABASE_URL", "REDIS_URL", "JWT_SECRET"]
missing = [k for k in REQUIRED if not os.getenv(k)]
if missing:
sys.exit(f"missing required env: {', '.join(missing)}")
if len(os.environ["JWT_SECRET"]) < 32:
sys.exit("JWT_SECRET too short, expect >= 32 chars")
# 上线前用同一条命令在服务器上预检一遍
sudo -u deploy env $(grep -v '^#' /home/deploy/apps/myapi/shared/.env | xargs) \
/home/deploy/apps/myapi/current/app --check-config
改配置要不要重启
| 配置类型 | 建议 | 理由 |
|---|---|---|
| 数据库地址、密钥、端口 | 重启 | 连接池与监听地址基本不可热改,硬改容易出半生效状态 |
| 日志级别 | 热加载或发信号 | 排查时需要临时调低,重启代价大 |
| 业务开关、限流阈值 | 热加载 / 配置中心 | 频繁调整,重启不现实 |
| 定时任务周期 | 重启 | 涉及调度器状态,重启更干净 |
原则:默认重启,只有明确高频调整的项才做热加载。热加载要能打印一条“已重新加载配置”的日志,否则出问题时无法判断何时生效。systemd 下可用 ExecReload=/bin/kill -HUP $MAINPID,应用捕获 SIGHUP 重新读配置。
验证方法
# 1) 仓库里确实没有密钥
git grep -nE 'password|secret|token|BEGIN .*PRIVATE KEY' -- . ':!*.md' | head || echo clean
# 2) 运行中的进程看到了正确的配置(换掉敏感值再打印)
docker inspect tools-site-web-1 --format '{{json .Config.Env}}' | tr ',' '\n' | grep APP_ENV
# 3) 密钥文件权限
sudo -u deploy stat -c '%a %U:%G %n' /home/deploy/apps/myapi/shared/.env
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
.env 提交进仓库 | 密钥泄露,历史无法清理 | 只提交 .env.example,密钥轮换 |
| 用环境变量传所有密钥 | 日志、错误页、inspect 都能看到 | 敏感值走密钥文件或 KMS |
| 密钥文件权限 644 | 同机任何用户可读 | chmod 600,属主设为运行用户 |
| 缺配置也能启动 | 第一个请求才 500,排查成本高 | 启动时 fail-fast 校验必填项 |
多环境共用一份 .env | 测试环境连到生产库 | 按环境分层,覆盖顺序固定 |
| 改了配置没重启 | 新配置没生效,误判为代码问题 | 明确哪些项需要重启并写进文档 |
| 热加载没有日志 | 不知道何时生效 | 重载成功/失败都要打日志 |
小结:配置与代码分离,环境变量承载大部分配置、密钥走 600 权限文件或云 KMS;.env 永不入库,仓库里只留 .env.example,进过 Git 的密钥必须轮换;多环境按“默认值 → 配置文件 → 环境变量 → 密钥服务”的顺序覆盖;启动时校验必填项并 fail-fast,改配置默认重启,只有高频调整项才做有日志的热加载。