持续集成、持续交付与持续部署
三个词经常被混用,但它们描述的其实是自动化程度递进的三件事:
- 持续集成(CI):开发者频繁把代码合入主干,每次合入都自动构建并跑测试,尽早发现集成问题。
- 持续交付(CD,Delivery):在 CI 之上,保证任何一次通过验证的变更都能随时部署到生产,只是部署这步由人点击确认。
- 持续部署(CD,Deployment):连那一次人工确认也去掉,通过流水线的变更自动进入生产。
大多数团队的现实目标是持续交付:把「能上线」变成常态,把「要不要上线」留给人的判断。真正做到持续交付后,发版会从一件需要提前一周准备的大事,变成一天里可以发生很多次的小事。
持续交付衡量的不是流水线有多快,而是「从提交到可以上线」这段距离有多短、有多稳。
- 四阶段构建、测试、制品、部署,每阶段产出可验证的中间物。
- 制品不可变一次构建、多次部署,用镜像摘要而非 tag 保证一致。
- 灰度策略蓝绿、金丝雀、滚动各有取舍,按风险与资源选。
- 回滚预案发布前先想好怎么退,回滚要能一条命令完成。
流水线的四个阶段
不管用什么工具,一条健康的流水线都可以拆成四个阶段。理解每个阶段的职责,比记住某个工具的语法重要得多。
| 阶段 | 做什么 | 产出与判定 |
|---|---|---|
| 构建 Build | 装依赖、编译、打包 | 产物 / 镜像,失败即阻断 |
| 测试 Test | 单元、集成、静态检查 | 测试报告与覆盖率 |
| 制品 Artifact | 打标签、推送到仓库 | 带版本号的不可变制品 |
| 部署 Deploy | 按策略发布到各环境 | 健康检查通过、指标正常 |
设计时有两条硬原则:
- 快速失败:把最便宜、最容易失败的检查放最前面(lint、单元测试),让问题在 1 分钟内暴露,而不是等 10 分钟后才在部署阶段报错。
- 同源制品:测试环境验证过的那个制品,应当原封不动地部署到生产。中间重新构建一次,就等于引入了一次未经测试的变量。
用 GitHub Actions 写第一条流水线
下面这份配置覆盖了「构建镜像并推送到镜像仓库」这条最小闭环。它放在仓库的 .github/workflows/ 目录下,推送到主干或打标签时自动触发。
name: build-and-push
on:
push:
branches: [ main ]
tags: [ 'v*' ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
run: echo "$REGISTRY_TOKEN" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin
- name: Build and push
run: |
IMAGE="ghcr.io/$GITHUB_REPOSITORY:$GITHUB_SHA"
docker buildx build --push --cache-from type=gha --cache-to type=gha,mode=max -t "$IMAGE" .
几个刻意写进去的细节值得留意:
- 用 commit SHA 做 tag:比
latest可追溯,任何一次部署都能定位到具体代码。 - 开启构建缓存:
cache-from/to让后续构建提速数倍。 - 显式声明权限:最小权限原则,避免 job 拿到不必要的写权限。
- 凭据不进仓库:
REGISTRY_USER与REGISTRY_TOKEN配在仓库的 Settings → Secrets 里,工作流在该步骤的env中以secrets上下文引用它们;YAML 与脚本里只出现变量名,永远不出现明文口令。
制品管理与版本可追溯
很多团队上了流水线却依然发愁,问题往往出在制品管理:镜像用 latest 覆盖,出了问题不知道线上跑的到底是哪一版。把制品管理做对,回滚和审计才有基础。
- 制品不可变:一旦推送就不再覆盖,用内容摘要(digest)标识唯一版本。
- 元数据齐全:每个制品记录提交哈希、构建时间、构建者、流水线运行号。
- 版本可追溯:从线上运行的镜像 digest,能反查到对应的代码提交与流水线记录。
- 保留策略:按需保留最近 N 个版本,老版本归档而非随意删除。
# 部署时固定到具体 digest,杜绝「同名不同内容」
docker run -d ghcr.io/acme/api@sha256:9f2c1ab7e41b5d8c
# 查询线上容器实际使用的镜像摘要
docker inspect api | jq -r '.[0].Image'
# 从镜像标签反查代码提交
docker inspect api:1.4.2 | jq -r '.[0].Config.Labels["org.opencontainers.image.revision"]'
把这几条写进团队规范后,一个常见问题会自然消失——「线上那个 bug 到底修没修」不再靠人回忆,而是查一下 digest 就有答案。
部署策略:蓝绿、金丝雀与滚动
部署不只是「把新版本换上去」,而是「如何让新版本在不伤害用户的前提下接管流量」。常用策略有三种,取舍点在于风险、资源与回滚速度。
| 策略 | 做法 | 优点 / 代价 |
|---|---|---|
| 滚动 Rolling | 分批替换旧实例,新实例就绪后再动下一批 | 省资源;但新旧版本会短暂共存 |
| 蓝绿 Blue-Green | 起一套全新环境,验证后流量整体切换 | 切换与回滚极快;需要双倍资源 |
| 金丝雀 Canary | 先放少量真实流量给新版,观察指标再逐步放大 | 风险最低;需要流量治理能力 |
金丝雀是实践中最值得投入的一种:它把「发布」变成一次可观测的实验。典型做法是 5% 流量给新版,观察 15 分钟的错误率与延迟,若无异常再放大到 25%、50%、100%;任何一项指标越界就自动回滚。判断好坏要有明确的放行标准,而不是肉眼看面板决定。
没有观测的灰度等于赌博。灰度发布的前提,是你能对两个版本的关键指标做对比。
质量门禁与自动化测试
流水线的价值,很大程度上取决于它能拦住什么。没有门禁的流水线只是一条自动化的传送带,照样会把缺陷送到线上。建议按成本从低到高设置门禁:
- 静态检查:格式、lint、类型检查,秒级完成,拦掉最低级的错误。
- 单元测试:覆盖核心逻辑,要求新增代码覆盖率不低于基线。
- 集成测试:拉起依赖(数据库、缓存)跑端到端接口,验证真实协作。
- 安全扫描:依赖漏洞扫描、镜像扫描、密钥泄露检测。
- 制品校验:签名、SBOM(软件物料清单)核对,确保制品来源可信。
门禁的尺度要稳住:对主干严格,对个人分支宽容。主干合并必须全部通过,个人推送允许先跑快检查。另外,偶发失败的测试(flaky test)是流水线公信力的头号杀手,一旦发现应当立即修复或隔离,绝不「再跑一次就过了」。
发布回滚与可观测
发布流程的设计,应当从回滚倒推。问三个问题:出问题时怎么退?退到什么版本?退的代价是几秒还是几小时?能把这三个问题回答清楚,发布才敢做得频繁。
- 回滚优先于修复:线上故障先回滚止损,再在安全环境里研究根因。
- 回滚要一条命令:回滚到上一个已知良好的 digest,而不是重新走一遍构建。
- 数据库变更要兼容:先做向前兼容的加字段,再改代码,最后清理旧字段,避免回滚时代码与库结构不匹配。
- 发布即观测:发布后自动盯关键指标(错误率、延迟、业务转化),越界自动触发回滚。
# 一条命令回滚到上一个已知良好版本
kubectl rollout undo deployment/api
# 查看发布历史与当前版本
kubectl rollout history deployment/api
# 观察滚动状态,失败会中止
kubectl rollout status deployment/api --timeout=180s
把发布与观测绑定在一起,是成熟度的一个重要标志:不再是「发完了祈祷平安」,而是「发完自动体检」。当回滚足够可靠、观测足够灵敏,团队才敢把发布频率提上去,而高频发布又会反过来暴露流水线里的薄弱环节,形成正循环。