跳到内容

第 6 卷 / 第 6 章 / 第 2 课

6.2 制品晋升、部署策略与可恢复交付 ​

流水线已经能生成制品,接下来必须明确:谁可以晋升制品、什么条件下允许晋升,以及同一份制品怎样从测试环境进入生产。

Continuous Delivery 指软件始终处于可发布状态,生产发布可以按需执行;Continuous Deployment 则把每个通过流水线的变更自动部署到生产。二者都建立在 CI 之上,但是否自动进入生产是不同的业务选择。

构建一次,按制品身份晋升 ​

错误流程会在测试、预发和生产分别重建:

text
commit -> staging build
       -> production build   # 依赖或环境可能已经不同

更可靠的流程是:

text
commit
  -> build + test
    -> image@sha256:...
      -> staging 部署同一 digest
        -> production 部署同一 digest

标签便于人阅读,但可以移动;镜像 digest 才标识具体内容。latest 不能回答线上究竟运行哪次构建,也让回滚目标含糊。

一个更安全的镜像 ​

此 Dockerfile 面向已有的 Java 21 Spring Boot 应用,使用 Gradle Groovy DSL,目录含 settings.gradle、build.gradle、gradlew、gradle/ 和 src/。若项目依赖 gradle.properties、buildSrc、约定插件或额外模块,也须按实际结构复制。先在 build.gradle 固定可执行 JAR 名称,避免普通 JAR 也被运行阶段的 COPY 匹配:

groovy
tasks.named('bootJar') {
    archiveFileName = 'app.jar'
}
dockerfile
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /src
COPY --chmod=0755 gradlew ./
COPY settings.gradle build.gradle ./
COPY gradle ./gradle
COPY src ./src
RUN ./gradlew --no-daemon check bootJar

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /src/build/libs/app.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

构建阶段执行 check 后才导出镜像,并且只把固定名称的可执行 JAR 复制到 JRE 镜像。check 只运行项目已配置的检查,不会自动补上集成测试。需要 Docker 或外部服务的测试还须相应的 builder/测试环境,因此这个紧凑示例假定检查可在构建阶段内运行。USER 指定数字运行身份,应用如需写目录要明确提供权限;EXPOSE 只是声明端口,不会对外发布。实际仓库还应:

  • 用 .dockerignore 排除 Git、构建产物和本地凭据;
  • 固定并定期更新基础镜像 digest;
  • 以非 root 用户运行;
  • 扫描 OS 包和应用依赖;
  • 不把 secret 放进 ARG、ENV 或镜像层;
  • 为容器定义健康与优雅退出行为。

固定 digest 提供可重复性,却不会自动获得安全补丁;需要自动提出更新并重新验证。

发布流水线输出 digest 和来源证明 ​

下面是可保存为 .github/workflows/publish-image.yml 的完整发布工作流示例。把 IMAGE_NAME 换成仓库有权发布的、小写 GHCR 路径,保护 main 并要求合并前验证通过。它会发布镜像和证明,应在指定的受信任仓库运行;它没有部署应用。Action 标签用于阅读,实际采用时固定经核验的完整 SHA。

yaml
name: publish-image

on:
  push:
    branches: [main]

permissions:
  contents: read

env:
  IMAGE_NAME: ghcr.io/example/tournament

jobs:
  publish:
    runs-on: ubuntu-24.04
    timeout-minutes: 30
    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write
    steps:
      - uses: actions/checkout@v6
        with:
          persist-credentials: false

      - uses: docker/setup-buildx-action@v4

      - 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: ${{ env.IMAGE_NAME }}:${{ github.sha }}

      - uses: actions/attest@v4
        with:
          subject-name: ${{ env.IMAGE_NAME }}
          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 把“部署”与“功能发布”分开,但不是回滚所有问题的万能开关:进程启动失败、数据库破坏和资源泄漏仍需部署级恢复。

灰度必须预先定义判断窗口和停止条件,例如错误率、尾延迟、业务成功率和资源饱和度。只观察“容器还活着”不能判断版本健康。新旧版本应按可比较的路由与流量总体观察,判断分位数或错误率前还须积累足够请求。10 次请求零失败,对 99.9% 的目标提供不了多少证据。提前说明越线后是停止晋升、把流量切回,还是也回滚实例。

数据库变更要允许新旧版本共存 ​

应用回滚到旧版本时,数据库可能已经前进。破坏性迁移应采用 expand–migrate–contract:

text
1. Expand:增加兼容字段/表,旧代码仍可运行
2. Migrate:回填数据,必要时双读/双写并核对
3. Switch:新代码切换到新结构
4. Contract:确认无旧版本依赖后删除旧结构

每一步都定义幂等、暂停、恢复和数据校验。把“DROP COLUMN”和依赖旧列的代码放在同一次发布中,会让回滚失去退路。

例如用 display_name 替换 name,只加可空列再回填一次还不够:旧实例可能继续修改 name。共存阶段应保留一个权威表示,同步后续写入、按检查点恢复回填,并在切读前核对两列。等旧读写方退出且回滚窗口结束后,才删除旧列;回填行数正确不代表值一直是新的。

回滚、前滚与故障恢复 ​

部署前要回答:

  • 上一个健康制品的 digest 是什么;
  • 当前数据库 schema 是否允许旧应用运行;
  • 回滚由谁或什么条件触发;
  • 配置和 feature flag 怎样恢复;
  • 消息消费者回退后能否理解新事件;
  • 回滚需要多久,是否定期演练。

如果数据已经发生不可逆变化,修复后前滚可能比应用回滚更安全。流水线不能替团队自动做这个判断,因此恢复手册要覆盖两条路径。

并发与审计 ​

同一环境应避免两个部署相互覆盖。下面是部署 job 的配置片段,与该 job 的 runs-on、steps 同级:

yaml
concurrency:
  group: deploy-production
  cancel-in-progress: false

environment: production

concurrency 分组防止同一仓库内匹配 job 重叠,Environment 提供已配置的保护规则与 secret 边界。默认队列最多保留一个等待项,即使 cancel-in-progress 为 false,新任务也可能替换已有等待项;该设置保护的是正在运行的 job,不是所有待部署版本。若每版都必须部署,要配置相应队列或发布控制器并核对顺序。其他仓库和手工部署不受这把锁约束,job 也应等外部 rollout 结束后再退出。部署记录至少关联 commit、制品 digest、操作者/工作流、目标环境、时间和结果。

DevOps 不是把运维工作交给开发者 ​

DevOps 强调开发、测试、安全、平台和运维围绕同一交付过程协作。自动化的目标是减少手工漂移并缩短恢复时间,但紧急访问、人工审批和变更管理在某些系统中仍然必要;关键是最小权限、可审计、可演练。

交付就绪检查表 ​

  • PR 与发布工作流是否分权?
  • 是否构建一次并按 digest 晋升?
  • 外部 Action、基础镜像和依赖是否可追踪更新?
  • 生产凭据是否使用短期身份和最小权限?
  • 环境门禁是否与风险相称?
  • 数据库迁移是否向前/向后兼容?
  • 灰度是否有停止条件和自动观测?
  • 回滚与前滚是否都演练过?
  • 部署记录能否回答“谁把什么部署到哪里”?

参考资料 ​

Built with VitePress | Software Systems Atlas