配置与密钥管理:环境变量与多环境

同一份镜像要能在开发机、测试环境和生产上跑,差别只在配置——数据库地址、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,改配置默认重启,只有高频调整项才做有日志的热加载。

笔记加载中…