从"四个坑"到"怎么避"
上篇讲完了四个坑,但读完之后大概率是这种反应——
"我知道这些坑了,但我手头的项目已经在跑了,里面堆了一堆历史包袱。总不能推倒重来吧?我现在能改什么?"
这是个好问题。
要避坑,不一定要推倒重做。真正要做的是把 workflow 这件事拆成清晰的"几层"——把判断、数据、执行放在不同的位置,让上篇的四个坑在结构上就没有立足之地。
本文要做的就是这件事:用具体的发布场景演示如何拆,最后给一张判断表,让不同阶段的团队知道"自己该做到什么程度"。
避坑的朴素思路:分清楚三层
反面:所有事都堆在一个文件里
某团队的发布配置一开始长这样(简化版):
# 发布配置(早期版本)
name: publish
on:
push:
branches: [main]
jobs:
deploy:
steps:
- name: 拉代码
run: git clone ...
- name: 跑测试
run: npm test
- name: 检查是不是新功能
run: |
if grep -q "feat:" commit.log; then
echo "new_feature=true" >> $GITHUB_ENV
fi
- name: 检查是不是涉及支付链路
run: |
if grep -q "payment" commit.log; then
echo "touch_payment=true" >> $GITHUB_ENV
fi
- name: 业务规则判断:能不能发
run: |
if [ "$new_feature" = "true" ] && [ "$touch_payment" = "true" ]; then
echo "本次发布包含新功能 + 支付链路变更,禁止周五上线"
exit 1
fi
- name: 打镜像
run: docker build ...
- name: 推镜像
run: docker push ...这个配置叠了三类事情:
- 执行——拉代码、跑测试、打镜像、推镜像。
- 决策——"本次发布能不能发"(业务规则判断)。
- 数据——新功能、支付链路等业务参数。
三类事揉在一个 YAML 里,每次业务规则一改,整个 pipeline 都要重测;改一次发布配置,要担心是不是会破坏业务逻辑。Jenkinsfile 越长,维护成本越高,最后没人敢动。
正面:拆成三层
把这件事拆成清晰的三层:
第一层:执行层(只管"做什么动作")
执行层只回答"做什么"——拉代码、跑测试、打镜像、推送。不关心业务规则、不关心业务参数。YAML 里只有动作,没有"为什么这样做"。
# 执行层(pipeline YAML)
name: execute
on:
push:
branches: [main]
jobs:
build:
steps:
- name: 拉代码
run: git clone ...
- name: 跑测试
run: npm test
- name: 打镜像
run: docker build ...
- name: 推镜像
run: docker push ...
deploy:
needs: build
steps:
- name: 拉决策结果
run: ./fetch-decision.sh # 从决策层拉"该不该发"
- name: 推送到目标环境
run: ./deploy.sh第二层:决策层(只管"该不该发")
决策层独立成一个服务或脚本。它从数据层取参数,按规则判断,把判断结果返回给执行层。
# 决策层(独立脚本或服务)
def should_publish(commit_info, params):
new_feature = "feat:" in commit_info
touch_payment = "payment" in commit_info
is_friday = datetime.now().weekday() == 4
if new_feature and touch_payment and is_friday:
return {"publish": False, "reason": "周五禁止支付链路变更发布"}
return {"publish": True, "reason": "ok"}决策层可以单独演进——业务规则变了,只改这一处,不动执行层。
第三层:数据层(业务参数独立存放)
业务参数(订单阈值、发布窗口、用户分群)放在专门的配置中心或数据库里,不在 pipeline YAML 里出现。
# 数据层(配置中心 / 数据库)
publish:
friday_blackout_categories: ["payment"]
rolling_window_users: 0.1
emergency_stop: false决策层从数据层取数,执行层从决策层取结果。谁也不从谁那里偷数据。
为什么这样拆能避坑
把上篇的四个坑放到三层结构里看,每个坑都没有立足之地:
| 上篇的坑 | 在三层结构里为什么不成立 |
|---|---|
| 第一个坑:业务规则出现在不该出现的地方 | 业务规则只在决策层出现,执行层 YAML 不再写"if 新功能 + 支付链路 → 禁止发" |
| 第二个坑:错误被拖到最后才暴露 | 执行层每个步骤独立校验;决策层独立运行,错了不影响执行 |
| 第三个坑:决定的事和被决定的事搅在一起 | 执行层是动作,决策层是判断,数据层是参数——三者通过清晰接口对接,不直接相互嵌入 |
| 第四个坑:流程反向吞噬工作 | 三层职责清晰:执行者改执行层、决策者改决策层、参数管理者改数据层。改一件事不会牵动其他事 |
三层结构不是新发明,是 workflow 在结构上的最小必要复杂度。再简单一点,三个坑的毛病全回来;再复杂一点,就是过度设计。
一个端到端的实例
下面用一个真实风格的发布场景把三层串成一条线。
场景
一个 SaaS 产品要做一次发布:
- 开发者 push 代码到 main。
- 系统自动跑测试、打镜像。
- 系统判断这次发布能不能发(决策层)。
- 系统按判断结果决定推送到哪个环境(执行层)。
- 推送完成后监控指标(数据层反馈)。
端到端流程
[开发者] git push
↓
[执行层 1] 拉代码 + 跑测试 + 打镜像
↓ (产物:镜像 + commit 信息)
[决策层] should_publish(commit_info, params)
↓ (结果:{"publish": true, "env": "staging", "ratio": 0.1})
[执行层 2] 推到 staging(10% 流量灰度)
↓ (持续 30 分钟观察)
[数据层] 监控指标 → 决定是否放量 / 回滚
↓
[执行层 3] 全量发布 / 自动回滚每层各管一件事:
- 执行层——把镜像从 A 推到 B,不关心该不该推。
- 决策层——判断该不该推、推多少,调用数据层取参数。
- 数据层——参数存放 + 监控指标采集 + 决策反馈。
反例对照
如果三层不拆,会出现什么?
某团队把所有事塞在一个 Jenkinsfile 里:
// 反例:所有事塞在一起
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker build .'
}
}
stage('ShouldPublish') {
// 业务规则硬编码在 Jenkinsfile
steps {
script {
def commit = sh(script: 'git log -1 --pretty=%B', returnStdout: true)
if (commit.contains('feat:') && commit.contains('payment') && isFriday()) {
currentBuild.result = 'FAILURE'
error('周五禁止支付链路发布')
}
}
}
}
stage('Deploy') {
// 业务参数散落在 Jenkinsfile
steps {
sh './deploy.sh staging 0.1'
}
}
}
}问题:
- 业务规则硬编码在执行层——改规则要改 Jenkinsfile,要重新走一遍 CI。
- 业务参数散落在执行层——订单阈值、灰度比例要改 Jenkinsfile。
- 监控反馈没接进来——出问题只能靠人盯日志。
- 改一处牵动全局——Jenkinsfile 越长,越没人敢动。
把三层拆开后,这些问题都没了。
团队在不同阶段该做到什么程度
三层拆法不是越早做越好,也不是做得越细越好。下面是一张梯队判断表。
阶段一:初创团队(5-10 人)
目标:让执行层不越界
- 执行层:直接用 GitHub Actions YAML / GitLab CI / Jenkinsfile。不要自建 CI 系统。
- 决策层:人工审批 + 一段简单判断脚本。复杂业务规则先用文档记下来。
- 数据层:环境变量 / Secrets /
.env文件即可,不要引入配置中心。
关键纪律:
- 执行层 YAML 里只写动作,不写业务规则。
- 业务规则放在注释或单独的文档里,人工 review。
- 业务参数放在
.env或 Secrets,不写在 YAML 里。
典型反例:
- 在 Jenkinsfile 里写"周五不上线"。
- 在 pipeline YAML 里塞业务阈值。
- 用同一个 Jenkinsfile 既管 dev 又管 prod。
阶段二:成长期团队(20-50 人)
目标:让决策层和数据层独立
- 执行层:开始分层(构建 pipeline / 测试 pipeline / 部署 pipeline 拆开)。可以引入 Jenkins + Jenkinsfile / GitLab CI / 自建 CI 平台。
- 决策层:自动化审批 + 灰度规则脚本。可以引入简单的"发布决策服务"(哪怕是个内部工具)。
- 数据层:引入配置中心(Apollo / Nacos / Vault / 内部参数服务)。业务参数独立存放。
关键纪律:
- 决策层从数据层取参数,不从执行层取参数。
- 执行层只调用决策层的接口,不内嵌业务规则。
- 数据层参数变更要有审计日志。
典型反例:
- 决策层写在执行层 YAML 里("如果 commit 信息里有 feat 就打镜像 tag v2")。
- 数据层参数硬编码在执行层("灰度比例 0.1 写在 Jenkinsfile 里")。
- 决策层直接读数据库,不走专门接口。
阶段三:规模化团队(100+ 人)
目标:让决策层能复用、能独立演进
- 执行层:CI/CD 平台化,统一调度(Tekton / Argo Workflows / 内部 CI 平台)。
- 决策层:决策引擎(Open Policy Agent / 内部规则引擎)+ 灰度平台 + 自动化运维。
- 数据层:完整的配置/参数治理 + 业务参数中台 + 监控 + 反馈。
关键纪律:
- 决策层是独立的产品,有版本、有 owner、有演进路径。
- 数据层参数变更走完整的发布流程。
- 反馈层(监控、告警、A/B 结果)反哺决策层,形成闭环。
典型反例:
- 决策层散落在多个 Jenkinsfile / GitHub Actions 里,没人能说清"现在到底有几条业务规则"。
- 数据层参数改了没人知道——配置中心有 200 个 key,没人维护。
- 决策层没有版本——业务规则改了,灰度回不去了。
判断表
| 团队规模 | 执行层做到什么 | 决策层做到什么 | 数据层做到什么 |
|---|---|---|---|
| 初创(5-10) | 单 YAML,动作清单 | 人工 + 简单脚本 | .env / Secrets |
| 成长期(20-50) | 分层 pipeline | 自动化审批 + 灰度脚本 | 配置中心 |
| 规模化(100+) | CI/CD 平台化 | 决策引擎 + 规则平台 | 参数中台 + 反馈闭环 |
关键问题不是"我该不该上 K8s",而是"我现在的执行 / 决策 / 数据三层有没有分开"。
收尾:方法论的朴素边界
最后讲几句方法论的边界,避免走偏。
第一,上工具 ≠ 数智化。
把 Jenkins 换成 Argo CD、GitHub Actions 换成 Tekton,不会自动让流程变好。如果三层不拆,换什么工具都是在同一团乱麻上叠加更复杂的工具。
第二,画流程图 ≠ 流程设计。
流程图画得再漂亮,只要执行 / 决策 / 数据三层没分开,上篇的四个坑迟早会回来。流程图是设计的结果,不是设计本身。真正的设计是"职责怎么划分、边界怎么定、接口怎么对"。
第三,复杂度的最低必要原则。
三层不是越多越好。初创团队硬上配置中心和决策引擎,反而会让流程变慢——因为决策层和数据层还没稳定下来,先把它们独立出去只会增加协调成本。
什么时候该往上走一层? 当现有形态开始出现上篇讲的四个坑里的任意一个,就该考虑往上走一层。但不要一次走完所有层,按"先执行层不越界 → 决策层独立 → 数据层独立"的顺序逐级演进。
数智化不是终点,是把流程做到让工作者感受不到流程的存在。
下篇到此为止。回到系列起点:流程即镜像:审批流程和 CI 流水线撞上的同一组坑(上)。
参考
- 流程驱动工作:从纸质审批到智能门闩 —— 上篇四个坑里"流程侧的糟心例"的源头
- 基础篇总结:从手动部署到声明式流水线 —— 上篇四个坑里"流水线侧的糟心例"的源头
- 流程即镜像:审批流程和 CI 流水线撞上的同一组坑(上) —— 本文上篇