KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

03 · 环境与配置:让同一份产物能在任何环境跑起来 — keel 龙骨

这一章回答:同一份产物,为什么在测试环境好好的、一上生产就崩?答案九成是配置与环境的问题,而且它几乎总是包含数据库迁移的顺序问题。

这一章回答:同一份产物,为什么在测试环境好好的、一上生产就崩?答案九成是配置与环境的问题,而且它几乎总是包含数据库迁移的顺序问题。

一、环境差异只允许存在于配置里

同一份产物(同一个 digest)
   ├── dev      配置 A:调试日志、mock 外部服务、本地数据库
   ├── staging  配置 B:接近生产的依赖、脱敏数据、较严超时
   └── prod     配置 C:真实凭据、真实限额、完整观测

反模式:"每个环境各自构建一次"。 一旦这么做,测试环境验证过的字节和生产上跑的就不是同一个东西,测试结果失去意义。

可执行的标准:能在不看代码的情况下,回答"这三个环境的差异一共就这 N 个变量"。如果答不上来,说明差异散落在了代码里的 if env == 'prod' 分支中——那是隐形炸弹。

二、配置该放哪:三类东西分三类地方

类型 例子 存放 理由
代码级常量 算法参数、业务常量 代码仓库 需要评审与追溯
环境级配置 数据库地址、超时、并发数、开关 环境变量 / 配置中心(非敏感) 随环境变化,不随代码变化
凭据 DB 密码、API key、私钥 密钥管理服务 / 密钥仓库 绝不能进代码库,也不需要跟着代码评审

三条硬纪律:

  1. 凭据绝不进 Git,包括已经删除的历史 commit——Git 是删除不掉的。轮换(rotate)才是泄露后的正确答案,而不是"删掉那行提交"。
  2. 配置变更也要有评审:很多事故来自"有人在线上改了一个配置"。配置进版本库或配置中心,走同样的变更流程。
  3. 启动即校验:服务启动时把所有必需配置项读一遍,缺一个就快速失败(fail fast)。让配置错误在启动阶段炸出来,而不是在高并发的某个分支里悄悄走错。

三、密钥管理的四个层级

① 环境变量(最低)       本机/单机 Compose 可用;容器 inspect 能看到,易泄漏
② 文件挂载(secret file) 容器内按文件读取,权限可控,不进 env
③ 外部密钥服务           Vault / 云厂商 Secret Manager,集中管理与轮换
④ GitOps 里的密文方案     Sealed Secrets(集群公钥加密,可安全入库)
                          External Secrets Operator(Git 只存引用,运行时拉取)★常用
                          SOPS(YAML 值加密,Git 原生但需做好审阅流程)

推荐的实际组合:有密钥服务就用 External Secrets Operator(或等价物):Git 里只有引用,真值留在密钥服务里并能自动轮换;没有就用 Sealed Secrets 起步;追求 Git 原生流程再用 SOPS。

判断依据很简单:任何"明文凭据出现在 Git 里"的方案,都不是可选项——包括"这个仓库是私有的"这种辩护。

四、数据库迁移:上线流程里最容易出事的一段

迁移之所以危险,是因为新旧版本的代码会在一段时间内同时跑(滚动更新期间就是),所以迁移必须对"上一个版本的代码"保持兼容。

4.1 三阶段写法(expand → migrate → contract)

阶段 做什么 兼容谁
Expand(加) 加新字段/新表,允许为空,旧代码不必知道它 兼容旧版本代码 ✅
Migrate(搬) 后台异步把历史数据搬到新字段,双写或补写 新旧都能跑 ✅
Contract(收) 确认旧字段无人使用后,下一个或下下个版本再删除 到这一步才破坏旧代码

要点是:加字段和删除字段不在同一次发布里发生。 这让"回滚"变成廉价动作——回滚只在已经加字段的版本之间来回,不需要反向迁移。

4.2 破坏性变更清单(每一条都要单独审视)

✗ 重命名列/表            → 走 加新 → 双写 → 切读 → 删旧
✗ 收紧约束(NOT NULL)    → 先补数据再收约束
✗ 改数据类型/缩字段长度   → 先校验数据范围,必要时新建字段
✗ 删索引                  → 确认没有查询在用;先在 staging 验证执行计划
✗ 大表结构变更            → 用在线 DDL 工具(pt-online-schema-change / gh-ost)或分批
✗ 一次性大批量 UPDATE     → 分批 + 限流,避免长事务和锁等待(详见 [数据层调优](../../../backend/database-tuning/course/README.md))

4.3 迁移与代码的发布顺序

推荐顺序(可回滚优先):
  1. 跑迁移(只做 expand 部分)
  2. 部署新代码(同时写新旧字段,只读旧字段)
  3. 数据补齐(后台批量)
  4. 切到读新字段(此刻才真正切换到新路径)
  5. 下一个版本:删掉旧字段(contract)

每一步都能单独回滚到第 4 步之前的状态。这里的通用原则是:让"部署"和"数据形状改变"成为两个可以独立回滚的动作。

五、环境漂移:最后一类隐形差异

最后一类隐形差异是环境状态漂移——有人手工改了 staging 的某项配置,没人知道,某天重新创建环境就跑不起来了。

根治方法只有三种:

方法 做法 适用
IaC Terraform / Pulumi 描述基础设施,变更走评审 云资源层
声明式编排 Compose 文件 / Helm Chart / Kustomize 描述应用拓扑 应用部署层
GitOps 控制器 控制器持续把集群状态对齐到 Git 声明(见 06 章) 两者结合后最强

一个可执行的起步动作:为"重建环境"设一个 deadline——任何时候都能在两小时内新建一个可用的 staging。 做不到,就说明环境里还有没被记录的东西。

动手:可观察结果

动作 产出物 判断标准
列出三个环境的全部差异变量 一张变量清单 能当着人说"差异就这 N 个,其余完全相同"
做一次 fail-fast 校验 启动时的配置校验代码 故意删一个环境变量,服务拒绝启动并在日志说明缺什么
写一次 expand-migrate-contract 三次迁移文件 每一步都能单独回滚,旧代码在 expand 阶段不受影响
把密钥从 env 改成文件挂载 部署清单 容器里 `env
重建一次 staging 计时 + 步骤记录 能在目标时间内完成,且不需要"找某个人要某个参数"

故障注入

注入方式 观察什么 说明的现象
删掉一个必需的环境变量 服务是否快速失败 应当立即失败并报缺哪个变量,而不是带错参数运行
一次发布里同时"加字段 + 删字段" 回滚能否成功 回滚后旧代码找不到刚删的字段 → 必然失败
手工改一次 staging 的配置 重建环境后是否一致 不一致 → 存在未记录的状态(漂移)
把密钥写进 Git 再删掉 git log -p 能否搜到 能 → Git 删除无用,只有轮换才有效
对大表直接加列 锁等待、主从延迟 需要在线 DDL 或分批方案

自测题

  1. 为什么"每个环境各自构建"会让测试结果失去意义?
  2. 配置的三分类是什么?凭据为什么必须单独存放?
  3. expand-migrate-contract 的三步各自兼容哪一边的代码?
  4. 什么情况下"同一个发布里既加字段又删字段"会翻车?
  5. 三种环境漂移根治方法分别治理哪一层的状态?

进入 keel 阅读