6.2 制品晋升、部署策略与可恢复交付
流水线已经能生成制品,但议事厅仍为“测试通过后由谁、何时、怎样进入生产”争论不休。
Continuous Delivery 指软件始终处于可发布状态,生产发布可以按需执行;Continuous Deployment 则把每个通过流水线的变更自动部署到生产。二者都建立在 CI 之上,但是否自动进入生产是不同的业务选择。
构建一次,按制品身份晋升
错误流程会在测试、预发和生产分别重建:
commit -> staging build
-> production build # 依赖或环境可能已经不同更可靠的流程是:
commit
-> build + test
-> image@sha256:...
-> staging 部署同一 digest
-> production 部署同一 digest标签便于人阅读,但可以移动;镜像 digest 才标识具体内容。latest 不能回答线上究竟运行哪次构建,也让回滚目标含糊。
一个更安全的镜像
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /src
COPY gradlew settings.gradle build.gradle ./
COPY gradle ./gradle
COPY src ./src
RUN ./gradlew --no-daemon bootJar
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /src/build/libs/*.jar app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]多阶段构建让最终镜像不包含编译工具。实际仓库还应:
- 用
.dockerignore排除 Git、构建产物和本地凭据; - 固定并定期更新基础镜像 digest;
- 以非 root 用户运行;
- 扫描 OS 包和应用依赖;
- 不把 secret 放进
ARG、ENV或镜像层; - 为容器定义健康与优雅退出行为。
固定 digest 提供可重复性,却不会自动获得安全补丁;需要自动提出更新并重新验证。
发布流水线输出 digest 和来源证明
下面只展示关键结构:
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v6
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ghcr.io/example/tournament:${{ github.sha }}
- uses: actions/attest@v4
with:
subject-name: ghcr.io/example/tournament
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true来源证明把制品与仓库、工作流和 commit 关联起来,但不证明代码没有漏洞。只有部署或消费方实际验证证明,并检查它是否来自允许的仓库和工作流,供应链策略才会在部署时生效。
环境不是固定三套
本地、临时预览、集成、预发和生产都是可能的环境。数量应由验证需求决定,不是必须 dev/staging/prod 三套。
环境之间应保持:
- 同一制品身份;
- 相同配置 schema 和部署机制;
- 关键依赖的兼容版本;
- 不同的凭据、账户、网络边界和数据;
- 明确的访问、审批与审计策略。
预发无法完整复制生产流量、数据分布和外部故障。它是验证层,不是“与生产完全相同”的承诺。优先使用合成数据;若确需生产派生数据,必须经过最小化、脱敏、访问控制、保留期和合规评审。
GitHub Environment 可以给生产 job 配置审批、允许的分支/标签、等待时间与环境级 secret。凭据在保护规则通过前不应暴露给 job。
部署策略解决的是不同风险
| 策略 | 做法 | 主要代价 |
|---|---|---|
| Rolling | 逐批替换实例 | 新旧版本会短暂共存 |
| Blue–Green | 两套完整环境切换流量 | 资源成本、状态与迁移协调 |
| Canary | 先让小部分真实流量进入新版 | 流量分配、指标判断和自动停止 |
| Feature Flag | 部署代码后按条件开放行为 | 分支组合、清理和一致性复杂度 |
Feature flag 把“部署”与“功能发布”分开,但不是回滚所有问题的万能开关:进程启动失败、数据库破坏和资源泄漏仍需部署级恢复。
灰度必须预先定义判断窗口和停止条件,例如错误率、尾延迟、业务成功率和资源饱和度。只观察“容器还活着”不能判断版本健康。
数据库变更要允许新旧版本共存
应用回滚到旧版本时,数据库可能已经前进。破坏性迁移应采用 expand–migrate–contract:
1. Expand:增加兼容字段/表,旧代码仍可运行
2. Migrate:回填数据,必要时双读/双写并核对
3. Switch:新代码切换到新结构
4. Contract:确认无旧版本依赖后删除旧结构每一步都定义幂等、暂停、恢复和数据校验。把“DROP COLUMN”和依赖旧列的代码放在同一次发布中,会让回滚失去退路。
回滚、前滚与故障恢复
部署前要回答:
- 上一个健康制品的 digest 是什么;
- 当前数据库 schema 是否允许旧应用运行;
- 回滚由谁或什么条件触发;
- 配置和 feature flag 怎样恢复;
- 消息消费者回退后能否理解新事件;
- 回滚需要多久,是否定期演练。
如果数据已经发生不可逆变化,修复后前滚可能比应用回滚更安全。流水线不能替团队自动做这个判断,因此恢复手册要覆盖两条路径。
并发与审计
同一环境应避免两个部署相互覆盖:
concurrency:
group: deploy-production
cancel-in-progress: false
environment: productionconcurrency 和 Environment 是两个独立机制:前者串行化 job,后者提供保护规则和凭据边界。部署记录至少关联 commit、制品 digest、操作者/工作流、目标环境、时间和结果。
DevOps 不是把运维工作交给开发者
DevOps 强调开发、测试、安全、平台和运维围绕同一交付过程协作。自动化的目标是减少手工漂移并缩短恢复时间,但紧急访问、人工审批和变更管理在某些系统中仍然必要;关键是最小权限、可审计、可演练。
交付就绪检查表
- PR 与发布工作流是否分权?
- 是否构建一次并按 digest 晋升?
- 外部 Action、基础镜像和依赖是否可追踪更新?
- 生产凭据是否使用短期身份和最小权限?
- 环境门禁是否与风险相称?
- 数据库迁移是否向前/向后兼容?
- 灰度是否有停止条件和自动观测?
- 回滚与前滚是否都演练过?
- 部署记录能否回答“谁把什么部署到哪里”?