开发环境过了、生产却不过的真正原因
一句话总结
如果每个环境都重新构建,每个环境得到的就是不同的东西。把只构建一次的制品原样晋级,是解决这个问题唯一的根本办法。
为什么需要它
很多地方的流水线是这个样子的。
dev 브랜치 → 빌드 → dev 배포
stage 브랜치 → 빌드 → stage 배포
main 브랜치 → 빌드 → prod 배포
构建了三次。源码相同,所以人们以为结果也相同,其实不然。两次构建之间,这些东西会发生变化:
- 基础镜像标签移动了(
python:3.12与昨天不一样了) npm install拉取了传递依赖的新补丁- apt 仓库中的软件包版本升高了
- 构建机器的工具链版本不同
所以,在 dev 上通过的测试,对 prod 镜像来说毫无意义。因为测试的东西和部署的东西不是同一个东西。
构建一次,晋级多次
소스 커밋 → 빌드 1회 → 아티팩트(불변) ─┬→ dev 배포 → 테스트
├→ stage 배포 → 검증
└→ prod 배포
关键在于制品是不可变的。在 dev 验证过的那些字节,原样送往 prod。这样“因为环境而不同”这个变量就消失了,剩下的差异只有配置和数据——这是我们可以控制的。
晋级不是重新构建,而是更换引用。
# gitops/prod/kustomization.yaml
images:
- name: registry/backend
newTag: v0820-1500 # ← 이 한 줄을 고치는 커밋이 배포다
LabHub 自身就是这样运转的。镜像只构建一次,提升 dev overlay 的标签进行验证,通过之后,再把同一个标签提升到 prod overlay。部署记录就是 git 日志。
配置放在制品之外
要让同一个制品用于多个环境,就必须把随环境而变的东西挪到外面。
| 项目 | 镜像内 | 镜像外 |
|---|---|---|
| 应用代码 | ✅ | |
| 运行时、库 | ✅ | |
| DB 地址、外部 API URL | ✅ 环境变量/ConfigMap | |
| 凭据 | ✅ Secret | |
| 日志级别、功能开关 | ✅ 配置 |
一旦把 application-prod.yml 构建进镜像,这个镜像就成了 prod 专用,晋级模型也就垮了。
用什么来标识
制品需要一个可以回溯的名字。
- 提交 SHA——最准确。
backend:a1b2c3d。一眼就能知道它出自哪份源码。 - 语义化版本——便于人阅读。用在发布上。
- 摘要(digest)——
@sha256:...。标签可以被移动,但摘要永远不会变。在真正需要万无一失的地方(prod),就用它。
latest 不能用在晋级模型中。无从知道它是哪个时间点的 latest,所以无法确定要回滚到什么。
门禁放在晋级之间
빌드 → [단위 테스트] → dev → [통합 테스트] → stage → [수동 승인] → prod
每个箭头前面的方括号就是门禁。门禁必须用退出码表达通过或阻止。如果在日志里打印“失败”,却返回 exit 0,流水线就会直接放行——这是实际中很常见的 bug。
回滚是晋级的反向操作
如果晋级是提升标签的提交,那么回滚就是退回到之前标签的提交。既不需要重新构建,也不需要紧急补丁。因为之前的制品仍然留在注册表里。
要做到这一点,就不能删除制品。制定保留策略时,如果只保留“最近 N 个”,就回不到比它们更旧的版本。
晋级实际上会在哪里出偏差
“只构建一次,只做晋级”这条原则很简单,但执行起来,会在三个地方漏掉。
标签会移动。 晋级了 myapp:v1.2.3,却有人用同一个标签再次推送,那么昨天验证的和今天部署的就成了不同的东西。用摘要部署,标签只当作人读的标牌使用。 如果注册表有标签不可变的设置,也要一并打开。
docker buildx imagetools inspect myapp:v1.2.3 --format '{{.Manifest.Digest}}'
按环境构建会悄悄复活。 只要混进一个 --build-arg ENV=prod 之类的参数,开发环境中测试过的镜像和生产镜像就在那一刻变成了不同的东西。差异只能放在运行时的配置里。构建参数里只要出现环境名称,就是一个信号。
配置跑进了镜像。 为图方便把默认配置文件放进镜像,那些值总有一天会在生产环境被使用。配置应当从外部注入,并让它缺失时启动失败,这样更安全。悄悄地靠默认值运行是最糟的。
门禁放在晋级之间,但同一件事不要测两遍。 单元测试在构建时做一次,集成测试在晋级到开发环境之后做一次,压力测试在晋级到 staging 之后做一次。有不少流水线会在生产晋级前再跑一遍单元测试,那只是在浪费时间,不会带来任何新信息。
回退必须是重新晋级旧摘要。 为了回退而重新构建的话,因为这期间依赖发生了变化,回退得到的东西就与以前不同。所以不要删除旧制品,至少保留几代。
记录什么在哪里。 如果不能在一个画面上看到每个环境现在是哪个摘要,出故障时就得花时间去查明“生产环境上到底跑着什么”。
在现场相遇的样子
- “在 staging 上是好的啊”→ 重新构建了,上线的是另一个东西。
- 想回滚,却没有之前的镜像 → 保留策略太短。
- 只有 prod 的镜像里带着不同的配置文件 → 一开始就不可能晋级。
下一项检查要关注什么
接下来的实验会亲手验证本文的主张。先根据提交给发布命名,在相当于开发环境的仓库中只生成一次制品,然后不用标签而用摘要来指定,把它迁移到 staging 和生产环境,抓住标签开始指向不同内容的那一刻,并用一个文件记录每个环境里有哪个摘要。实验之后的测验,则要判断构建与晋级的区别、不可变摘要、外部配置和保留策略对回滚可行性的影响。