华为云 CodeArts:从手工部署到流水线自动发布的改造实录

举报
辣椒酱来了 发表于 2026/11/03 12:39:14 2026/11/03
【摘要】 一、手工上线的隐形成本先算一笔账。我们的上线清单是这样的:12 个步骤,写在文档里,每次上线对着勾。本地确认 git status 干净拉最新代码创建虚拟环境、装依赖跑单元测试手动改配置文件的数据库地址打包上传SSH 上服务器,备份旧代码停服务解压覆盖执行数据库迁移启服务看日志确认正常表面上每次 40 分钟。但真实成本不止这些:心理成本。 12 步里任何一步出错,都要判断"现在处于什么状态...

一、手工上线的隐形成本

先算一笔账。

我们的上线清单是这样的:12 个步骤,写在文档里,每次上线对着勾。

  1. 本地确认 git status 干净
  2. 拉最新代码
  3. 创建虚拟环境、装依赖
  4. 跑单元测试
  5. 手动改配置文件的数据库地址
  6. 打包上传
  7. SSH 上服务器,备份旧代码
  8. 停服务
  9. 解压覆盖
  10. 执行数据库迁移
  11. 启服务
  12. 看日志确认正常

表面上每次 40 分钟。但真实成本不止这些:

心理成本。 12 步里任何一步出错,都要判断"现在处于什么状态、要不要回退"。第 9 步失败和第 10 步失败,处理方式完全不同。这种状态判断消耗的注意力,远超过操作本身。

频率成本。 因为上线有成本,大家本能地"攒一波再发"。结果每次上线包含 30 个变更,一旦出问题,排查范围是 30 个改动,定位难度指数级上升。

一致性成本。 第 5 步"手动改配置"是所有事故的高发区。有一次改完忘了改回来,本地测试环境连到了生产库。

那次"忘了跑数据库迁移"的事故,是压垮骆驼的最后一根稻草:新代码上线了,新字段没建,所有涉及新字段的页面全报 500。而迁移脚本就躺在仓库里,第 10 步被跳过了——因为当时急着发版。

结论很清楚:靠清单和纪律保证一致性,一定会失败。 一致性必须由机器保证。

二、流水线的本质:把清单变成不可跳过的代码

CI/CD 流水线做的事情,本质就一句:把上线清单写成代码,让机器执行,且无法跳步。

CodeArts 的流水线由三类元素组成:

元素 作用 我们能填什么
阶段(Stage) 逻辑分组 构建、测试、部署
任务(Task) 具体动作 Shell 脚本、构建、部署、代码检查
参数与变量 环境差异 测试/生产环境地址、镜像版本号

分阶段不是为了好看,是为了失败时能停下。构建失败就不该进测试,测试失败就不该进部署。这个"停"的能力,是流水线比手工清单强的最关键一点。

三、五个阶段的完整设计

我们的流水线分了五个阶段,每个阶段的职责和失败处理都不一样:

┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│ 1.代码检查│──▶│ 2.构建镜像│──▶│ 3.自动化测试│──▶│ 4.部署测试 │──▶│ 5.部署生产│
└──────────┘   └──────────┘   └──────────┘   └──────────┘   └──────────┘
   静态扫描      产出镜像       单元+集成       验证通过       需人工审批
   阻断严重问题   打唯一标签     覆盖率门槛      冒烟测试        灰度+观测

3.1 阶段一:代码检查

这一阶段的价值常被低估。它能在代码还没进入构建时就拦下问题:语法错误、明显的安全漏洞(硬编码密钥)、圈复杂度超标。

# 概念示意:代码检查任务的关键配置
检查规则集: 通用编码规范 + 安全规范
阻断条件:
  - 严重级别问题数 > 0        # 一个严重问题就阻断
  - 新增代码圈复杂度 > 15

注意"新增代码"这个限定词很重要——历史代码可能有几百个警告,如果对所有代码都卡阈值,流水线永远是红的,团队很快就会集体无视告警。只卡新增部分,既能防止恶化,又不至于无法推进。

3.2 阶段二:构建与镜像

# 构建脚本(流水线内执行)
set -e   # 任何一步失败立即退出,不要吞错误

VERSION=$(git describe --tags --always)   # 用 git 信息做版本号
echo "building version: ${VERSION}"

docker build \
  --build-arg BUILD_VERSION="${VERSION}" \
  -t "${SWR_HOST}/${ORG}/myapp:${VERSION}" .

docker push "${SWR_HOST}/${ORG}/myapp:${VERSION}"

# 把版本号传给下游阶段
echo "${VERSION}" > version.txt

set -e 是这里的关键。默认情况下 Shell 脚本遇到错误会继续往下执行,docker build 失败了还会去 docker push 一个不存在的镜像,最后报一个完全不相干的错。每个 CI 脚本都应该以 set -e 开头。

版本号用 git describe 而不是时间戳,好处是能从镜像名反查到具体提交。

3.3 阶段三:自动化测试

set -e

pip install -r requirements.txt
pip install pytest pytest-cov

# --cov-fail-under 让覆盖率不达标直接失败
pytest tests/ \
  --cov=app \
  --cov-report=term \
  --cov-fail-under=70 \
  -x          # 遇到第一个失败就停,不要跑完所有用例浪费时间

覆盖率门槛(70%)这个数字要慎重设。设太高(90%)会逼着大家写无意义的断言去凑数;设太低(30%)起不到保护作用。建议从当前实际覆盖率往上加 5 个点,逐步提升。

同时要区分两类测试:

类型 跑在哪 耗时 依赖
单元测试 阶段三 秒级 无外部依赖,全 mock
集成测试 阶段四 分钟级 需要真实数据库、缓存

把慢测试挪到部署后是个重要设计:单元测试在构建阶段快速给出反馈,集成测试在测试环境里验证真实链路。混在一起会让每次提交都等 10 分钟。

3.4 阶段四:部署测试环境 + 冒烟

set -e
VERSION=$(cat version.txt)

# 更新镜像版本(CCE 场景)
kubectl set image deployment/myapp \
  myapp="${SWR_HOST}/${ORG}/myapp:${VERSION}" \
  --namespace=test

# 等滚动更新完成,最多等 5 分钟
kubectl rollout status deployment/myapp \
  --namespace=test --timeout=300s

# 冒烟测试:验证核心接口真的能用
curl -sf "https://test.example.com/healthz" || exit 1
curl -sf "https://test.example.com/api/version" | grep "${VERSION}" || exit 1

冒烟测试里那句 grep "${VERSION}" 特别有用。它验证的不只是"服务活着",而是"跑的是我这次要发布的那个版本"。少了这一步,可能出现流水线全绿但线上还是旧版本的情况——部署命令执行了,但因为镜像标签没变,K8s 认为无需更新,什么都没做。

3.5 阶段五:部署生产(带审批)

生产部署一定要人工审批卡一道。这不是不信任自动化,而是自动化无法判断的两种情况:

  • 现在是业务高峰期吗?
  • 有没有正在进行的运营活动不能被打断?

审批通过后走灰度:

# 第一步:先更新 1 个实例(共 3 个)
kubectl set image deployment/myapp \
  myapp="${SWR_HOST}/${ORG}/myapp:${VERSION}" \
  --namespace=prod
kubectl rollout pause deployment/myapp --namespace=prod

# 观察 5 分钟:看错误率和延迟
sleep 300
ERROR_RATE=$(query_error_rate)
if [ "${ERROR_RATE}" -gt 1 ]; then
  kubectl rollout undo deployment/myapp --namespace=prod
  exit 1
fi

# 第二步:确认正常,继续滚动
kubectl rollout resume deployment/myapp --namespace=prod

rollout pause + 指标判断 + resume/undo 这三步,构成了一个最小可用的自动灰度。关键在于指标判断放在流水线里而不是靠人看监控——人看监控容易"再等等看",机器不会。

四、四个必须踩过才知道的坑

4.1 密钥不能写进流水线配置

流水线的 YAML 是要提交到仓库的。把数据库密码、AK/SK 写进去,等于把密钥公开发布。

正确做法是用平台的凭据管理功能:密钥存成加密凭据,流水线里只引用凭据 ID,运行时注入成环境变量。

更彻底的做法是根本不用长期密钥——部署步骤通过委托获取临时凭证,凭证几分钟就过期。

4.2 数据库迁移必须在部署前跑,且要能重复执行

那次事故的根因就是顺序错了。正确的顺序是:

① 跑迁移(向后兼容,即旧代码也能在新表结构上运行)
② 部署新代码
③ 确认正常后,再清理旧字段

这叫扩展-收缩模式。核心是第①步的迁移必须"新旧代码都能用":加字段可以,改字段名不行(要分两步:先加新字段双写,再删旧字段)。

迁移脚本还必须幂等——重复执行不报错。因为流水线重试是常态,跑第二遍就报"表已存在"的迁移,会让重试机制失效。

4.3 流水线会掩盖"环境差异"

自动化之后有个新问题:测试环境全绿,生产还是炸。原因往往是两个环境的差异没有体现在配置里——测试环境 4 核,生产 2 核;测试环境数据库版本 8.0,生产 5.7。

解法是把所有环境差异显式化成变量,并且在流水线里对关键变量做校验。比如部署前断言"生产环境的数据库版本必须是 8.0",不满足就拒绝部署。

4.4 别让流水线变成"黑盒"

上线是自动化了,但出问题时没人知道流水线到底做了什么。

必须做的两件事:一是每个阶段都输出结构化日志(做什么、耗时多少、结果如何),二是构建产物永久可追溯——给定一个线上运行的镜像版本号,能反查到它是哪次提交、哪个流水线运行、跑了哪些测试。

做不到这两点,自动化就从"减少工作量"变成了"减少可见性"。

五、一个意想不到的结果

流水线上线三个月后,我们对比了前后数据:

指标 手工时代 流水线时代
单次上线耗时 40 分钟 12 分钟(且无需人工盯)
上线频率 每周 1 次 每天 1~2 次
上线事故率 约 1/5 次 约 1/20 次
平均回滚耗时 25 分钟 90 秒
变更批量 平均 30 个改动 平均 3 个改动

最值得注意的不是"耗时降低",而是变更批量从 30 个降到 3 个。

这是自动化真正改变的地方:当上线变得廉价,团队就会小步走。小步走带来的直接收益是故障定位范围缩小一个数量级——出问题时只需要检查 3 个改动,而不是 30 个。事故率下降 4 倍,主要功劳在这里,而不在自动化本身有多聪明。

这形成了一个正循环:上线越容易 → 每次改动越少 → 出问题越少 → 越敢频繁上线。

六、常见问题 Q&A

Q1:小项目有必要上流水线吗?

看上线频率。一周发不了一次的项目,手工部署够用。但只要到了"一周多次"或者"多人协作",流水线的价值就立刻显现——因为手工部署的风险和频率成正比,而流水线的成本是固定的。

Q2:流水线跑一次要 10 分钟,太慢了怎么办?

先分层:单元测试必须快(2 分钟内),慢测试挪到部署后。再并行:不相关的检查任务并发跑。最后优化缓存:依赖包、Docker 层都要缓存。我们优化完从 11 分钟降到 4 分钟,主要靠依赖缓存。

Q3:怎么防止有人绕过流水线直接手工上线?

技术上堵不住(有服务器权限就能登)。真正的办法是让合规路径比违规路径更省事:流水线 12 分钟全自动,手工部署要 40 分钟还担风险,人自然会选前者。另外给生产环境配置审计日志,让手工操作留痕。
Q4:灰度发布发现有问题,怎么快速回滚?

kubectl rollout undo 或把镜像标签改回上个版本,几十秒完成。前提是镜像必须带唯一版本号(不能用 latest),否则无法回到"确定的那个旧版本"。

Q5:数据库迁移失败了怎么办?

这是最难的场景。所以迁移要遵循两条纪律:一是只做可逆的变更(加字段可逆,删字段不可逆,删字段要单独排期),二是上线前在测试环境用生产数据量演练一遍。真遇到不可逆的迁移失败,只能靠备份恢复,代价很高。

Q6:流水线要不要跑在 K8s 里?

CodeArts 自带执行机,中小规模够用。当并发流水线多、需要特殊环境(GPU、大内存)时,可以接入自管执行集群。我们的做法是:普通构建用平台执行机,需要 GPU 的测试任务放到 CCE 的临时节点上跑完即销毁。

七、总结

流水线的价值,很多人理解成"省时间"。但省下的 28 分钟其实不是重点。

重点在三个隐性收益:一致性(每次都是同一套动作,不依赖谁记得哪一步)、可见性(失败在哪一步、什么原因、谁触发的,全有记录)、心理安全感(不用担心忘记某一步,于是敢改小步)。

第三条最容易被忽视,但影响最大。手工部署时代,工程师不是不知道该小步走,而是"每发一次都这么麻烦,不如攒一攒"。自动化解除了这个约束,工程实践才可能往更好的方向走。

所以判断一条流水线做得好不好,别看它跑得多快,看一个指标:团队的上线频率有没有提高。如果没有,说明自动化只是把手工包装了一遍,没有真正降低上线的门槛。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。