4.10 CI/CD 生产实践——多环境、审批与交付流水线

预计阅读时间:17 分钟

📖 目录

4.9:CI/CD 持续部署 介绍了 GitHub Actions 的基础流水线。生产环境的 CI/CD 需要更多工程细节:多环境分支策略、审批门禁、artifact 版本管理、密钥轮换和回滚机制。本文用一个完整的交付流水线案例串联这些实践。

学习目标

  • 理解 Trunk-based Development 与多环境分支策略的对应关系
  • 能编写包含 lint、test、build、deploy 分层的生产级 GitHub Actions 流水线
  • 掌握用 GitHub Environments + Required reviewers 配置审批门禁
  • 能设计 SemVer + Git SHA 双 tag 的镜像版本管理与回滚方案
  • 掌握 Secrets 的分环境管理与定期轮换策略
  • 理解 CI/CD 成熟度模型并能评估自身流水线所处阶段

前置知识

1. 多环境分支策略

环境分支触发器审批
开发(dev)feature/*PR 创建 / push自动
集成(staging)developPR merged to developCode Review + 自动测试
预发(preprod)release/*手动触发QA 审批
生产(prod)mainrelease tag push运维审批
推荐模型 Trunk-based Development(Git Flow 的简化版):feature branch → develop → 自动构建 staging → release tag → 部署 production。避免 long-lived feature branch。

Git Flow vs Trunk-based Development

维度Git FlowTrunk-based(推荐)
主分支main + develop 双长分支单主干 main,随时可发布
功能分支生命周期数周(feature → develop → release → main)1-2 天,短命分支即合并
发布方式release 分支冻结 + 修复tag 即发布,小步高频
合并冲突概率高(长分支偏离主干)
热修复hotfix 分支双向合并,易漏主干直接修 + tag 发布
适用场景发布节奏慢、强合规(金融/电信)SaaS、发布频率 ≥ 每周

环境配置管理

多环境的核心矛盾:同一份代码,不同环境的地址、密钥、开关不同。推荐的配置分层:

# 代码仓库内只放非敏感、可公开的默认配置
config/
├── base.yaml          # 所有环境公共项
├── dev.yaml           # 环境差异(域名、副本数)
├── staging.yaml
└── prod.yaml

# 敏感项(数据库密码、API Key)不进代码仓库:
# 方案 A:GitHub Actions 的 Environment Secrets(推荐入门)
# 方案 B:运行时从 Vault / Secrets Manager 拉取(见第 8 节)
# 方案 C:部署时由 CI 注入 .env 文件,仓库只留 .env.example

# 流水线中按环境选择配置文件
- name: Select config
  run: |
    cp config/${{ github.ref_name }}.yaml config/active.yaml
    echo "${{ secrets.ACTIVE_ENV_FILE }}" > .env

本地预检:把错误拦在 push 之前

CI 的反馈循环再快也要分钟级,本地预检把它压到秒级。pre-commit 只做三件"几乎不误报"的事,避免拖慢提交:

# .pre-commit-config.yaml(pre-commit 框架)
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets          # 阻止密钥提交进仓库

# Node 项目用 husky 在 commit 前跑 lint(package.json)
#   "husky": { "hooks": { "pre-commit": "npm run lint" } }
# 纯 git 钩子写法(.git/hooks/pre-commit)
#!/bin/sh
npm run lint || exit 1
npx tsc --noEmit || exit 1

# 本地预检守则:
# 1. 总时长 < 60s,超过就砍掉不必要的内容
# 2. 全绿才能 push,红色直接拦下
# 3. 与 CI 分工:lint/typecheck 本地跑,矩阵测试/镜像构建留给 CI

2. 生产级流水线模板

# .github/workflows/deploy.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
    tags: ['v*']
  pull_request:
    branches: [develop]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck

  test:
    needs: lint
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
      redis:
        image: redis:7-alpine
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  build:
    needs: test
    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Extract version
        id: version
        run: |
          if [[ $GITHUB_REF == refs/tags/v* ]]; then
            echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
          else
            echo "version=sha-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
          fi
      - name: Build & Push Docker image
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/main'
    environment: staging
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to staging
        run: |
          # SSH 到 staging 服务器拉取新版本
          ssh ${{ secrets.STAGING_HOST }} "
            docker compose pull web
            docker compose up -d web
          "

  deploy-production:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production
    runs-on: ubuntu-latest
    concurrency: production
    steps:
      - name: Deploy to production
        run: |
          ssh ${{ secrets.PROD_HOST }} "
            docker compose pull web
            docker compose up -d web
          "
      - name: Health check
        run: |
          sleep 15
          curl -f https://example.com/health
      - name: Rollback on failure
        if: failure()
        run: |
          ssh ${{ secrets.PROD_HOST }} "
            cd /opt/app && \
            sed -i 's|image: myapp:.*|image: myapp:${{ env.PREV_VERSION }}|' docker-compose.yml && \
            docker compose up -d web
          "

3. 审批门禁(Environment + Review)

GitHub Environments 的 Required reviewers 功能为敏感环境添加人工审批:

# 在仓库 Settings > Environments 中配置:
# production 环境:
#   - Required reviewers: 2 人
#   - Wait timer: 5 分钟
#   - Deployment branches: main only(仅 main 分支可部署生产)

# workflow 中通过 environment 字段关联
deploy-production:
  environment: production  # 自动触发审批流程

# 部署审批通过后自动执行,拒绝则跳过

其他 CI 平台的审批实现

# Jenkins——Input Step 实现人工批准后继续
stage('Deploy to Production') {
    input {
        message "确认部署到生产环境?"
        ok "批准部署"
        submitter "ops-team,release-manager"   # 只有这两个组的成员可批准
        parameters {
            string(name: 'VERSION', defaultValue: env.IMAGE_TAG, description: '镜像版本')
        }
    }
    steps {
        sh "sed -i 's|image: myapp:.*|image: myapp:${params.VERSION}|' docker-compose.yml"
        sh "docker compose up -d web"
    }
}

# GitLab——受保护环境 + 审批规则(UI 配置)
# Settings > CI/CD > Protected environments > production
#   允许部署的分支:main only
#   审批人数:2(部署前必须 2 人批准)
# .gitlab-ci.yml 中声明环境:
deploy-prod:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  script:
    - docker compose pull web
    - docker compose up -d web
  only:
    - tags
审批与自动化平衡 审批门禁放在「高危且不可逆」的动作上:生产部署、数据迁移、密钥轮换。低风险动作(staging 部署、镜像构建)过度加审批只会拖慢交付。

门禁设计的通用原则

门禁不是越多越好:每加一道门,交付延迟就增加一分。判断一道门该不该存在的标准是——它拦得住真实的错误吗?永远全绿的门(仪式感门禁)趁早删掉;拦得住但不常触发的门(如安全扫描)降级为定期巡检。

门禁拦截目标放置位置通过标准
Lint / Typecheck语法与低级错误PR 阶段零 error(warning 可选)
单元测试逻辑回归PR 阶段全绿 + 覆盖率不下降
镜像漏洞扫描(Trivy)高危漏洞进产物构建后无 CRITICAL 级 CVE
集成 / E2E 测试跨服务破坏staging 部署后关键路径用例通过
人工审批高风险变更生产部署前按风险分级要求人数(见第 9 节)
健康检查部署失败外溢部署后 15 分钟内/health 连续 3 次 200
门禁的元规则 门禁必须"快、稳、可解释":PR 阶段门禁总耗时超过 10 分钟,开发者就会开始绕过它;门禁自身波动(网络超时、缓存未命中导致的偶发失败)要优先于业务代码修复;每个门禁的失败信息要给出"修什么、去哪看"的指引。

4. Artifact 版本管理

容器镜像的 tag 策略直接影响回滚速度和可追溯性:

策略示例优点缺点
SemVer tagv1.2.3语义清晰,与 Git tag 一致需手动打 tag
Git SHAsha-a1b2c3d唯一可追溯,自动生成不直观
latestlatest部署方便不可追溯,不应上生产
# 生产推荐:SemVer tag + Git SHA
docker build -t myapp:v1.2.3 -t myapp:sha-a1b2c3d .

# 回滚时先把 compose 文件的 image 指回上一版本 tag,再重新部署
sed -i 's|image: myapp:.*|image: myapp:v1.2.2|' docker-compose.yml
docker compose up -d --pull=always web

语义化版本与产物命名规范

# SemVer 规则:MAJOR.MINOR.PATCH + 预发布后缀(-rc.1)
# MAJOR:不兼容的 API 变更
# MINOR:向后兼容的新功能
# PATCH:向后兼容的缺陷修复

# 产物命名规范(镜像 / 安装包 / 软件包统一):
# 镜像:<registry>/<team>/<app>:<version>-<sha>
ghcr.io/platform/checkout:v2.4.1-ga1b2c3d

# 安装包:<app>_<version>_<os>_<arch>.<ext>
checkout_2.4.1_linux_amd64.deb

# 版本来源单一化:发布脚本统一打 Git tag → 生成 SemVer → 构建产物
# 禁止各环境各自生成版本号,保证「代码-镜像-版本」一一对应
发布类型Git tag镜像 tag回滚目标
正式版v2.4.1v2.4.1-ga1b2c3d上一正式版
候选版v2.5.0-rc.1v2.5.0-rc1-ga1b2c3d当前稳定版
热修复v2.4.2v2.4.2-ga1b2c3dv2.4.1

5. 密钥管理

# GitHub Actions Secrets(仓库 Settings > Secrets and variables)
# 每个环境独立配置
# dev/staging/prod 各自一套密钥

# 不要在 workflow 文件中写明文密钥
# 正确做法:
- name: Configure env
  run: |
    echo "${{ secrets.PROD_ENV_FILE }}" > .env
    docker compose up -d

# 密钥轮换策略:
# - 每 90 天自动轮换数据库密码
# - 轮换后立即更新 GitHub Secrets
# - 紧急泄漏时立即轮换(可在 GitHub UI 操作)

6. 回滚策略

方式操作速度
镜像回滚改 compose image 为旧 tag 后 docker compose up -d web秒级
Git revertgit revert HEAD && git push分钟级(重新走 CI)
蓝绿切换切 Nginx 上游指向旧版本秒级
数据库回滚复杂 应用代码回滚容易,数据库 schema 回滚(migration rollback)需要提前规划。建议:每个 migration 必须有对应的 down script,且数据库回滚与应用回滚协调执行。

金丝雀与蓝绿部署对比

维度金丝雀(Canary)蓝绿(Blue/Green)
原理新版本先放 5% 流量,逐步放量新旧两套环境并存,一次性切换
步骤① 部署 canary 副本 ② 分流 5% ③ 观察指标 ④ 25%/50%/100% 逐步放量① 部署 green 并验证 ② 切 Nginx/网关上游 ③ 观察 ④ 回收 blue
回滚速度立即把新版本流量切回 0%切回旧上游,秒级
资源成本低(仅多一份小副本)高(双倍环境常驻)
适用持续交付、发布频率高大版本升级、关联数据库迁移
陷阱小流量下统计偏差,异常难发现长连接未断开导致切回不干净
# 金丝雀发布脚本要点(K8s + Istio 为例)
# 1. 部署 v2 Deployment(replicas: 1)
# 2. VirtualService 权重:v1 95% / v2 5%
# 3. 观察 15 分钟错误率与 P99(v2 错误率 > 0.5% 立即权重归零)
# 4. 逐步 25% → 50% → 100%,全程监控
kubectl apply -f deploy-v2.yaml
kubectl apply -f virtualservice-95-5.yaml
sleep 900 && check_metrics
kubectl apply -f virtualservice-50-50.yaml
sleep 900 && check_metrics
kubectl apply -f virtualservice-100-0.yaml

回滚决策:什么时候"撤",比怎么撤更重要

回滚的犹豫期往往比回滚本身更久。提前把触发条件写进 runbook,事故时照单执行:

  • 立即回滚:错误率 > 1%、P99 超过 SLO 持续 5 分钟、核心链路不可用、数据写坏
  • 观察后再定:仅个例告警、指标缓慢劣化、与旧版本行为差异(先对比新旧版本配置与流量)
  • 不回滚直接修:热修复已在路上(10 分钟内)、回滚本身比修复风险更高(如回滚会触发重复支付)
  • 回滚后的动作:保留现场(日志、监控截图、流量记录)→ 通知相关方 → 复盘会上定位根因,避免"滚回来就完事"

7. 流水线成熟度模型

级别特点
L1 手动SSH 上服务器手动部署
L2 基础 CI有 lint + test,但部署仍手动
L3 自动 CI/CD全自动构建 + 部署 staging,生产有审批门禁
L4 渐进式交付Canary / 蓝绿部署,自动回滚,监控驱动

8. 密钥轮换自动化

手动改 Secrets 容易漏、慢、留后门。生产团队应把密钥的读取与轮换交给密钥管理系统:

# 方案 A:HashiCorp Vault——流水线运行前动态拉取,密钥不落地
- name: Fetch secrets from Vault
  uses: hashicorp/vault-action@v3
  with:
    url: https://vault.internal.example:8200
    method: approle
    roleId: ${{ secrets.VAULT_ROLE_ID }}
    secretId: ${{ secrets.VAULT_SECRET_ID }}
    secrets: |
      secret/data/prod/db DATABASE_URL | db_url ;
      secret/data/prod/api APP_API_KEY

# 数据库密码轮换(Vault 数据库动态凭据引擎):
# vault write database/rotate-root/my-db
# 轮换后旧密码立即失效,应用无需重启(动态凭据自动续租)

# 方案 B:AWS Secrets Manager 自动轮换
# 配置 Lambda 轮换函数,RDS 密码 30 天自动轮换
aws secretsmanager rotate-secret \
  --secret-id prod/db --rotation-rules '{"AutomaticallyAfterDays": 30}'

# 季度轮换演练:
# 1. 按最小权限创建临时密钥并部署验证
# 2. 验证应用无感知切换后吊销旧密钥
# 3. 记录轮换耗时,目标 < 10 分钟
轮换失败预案 密钥轮换前必须确认应用支持「重读密钥」或「无缝重连」。不支持的应用(配置只在启动时读取)需安排重启窗口,轮换当天发布变更冻结。

9. 发布日历与变更管理

发布不只是「跑流水线」。生产事故多发生在变更后,规范变更流程比优化构建速度更重要:

环节负责人检查项
变更申请开发变更内容、影响范围、回滚方案、数据库迁移脚本
评审技术负责人代码评审 + 变更风险分级(低/中/高)
发布窗口运维避开业务高峰(电商:夜间;B2B:周末)
审批运维 + 业务负责人高风险变更双人审批(对应第 3 节门禁)
发布后观察监控值班错误率 / 延迟 / 资源使用 30-60 分钟观察期
复盘全组发布失败 → 根因分析、改进项、回滚耗时记录
# 发布日历(示例)
# 周一 10:00-12:00  例行低风险发布(PATCH 级)
# 周三 14:00-16:00  中风险发布(MINOR 级,需审批)
# 周五 16:00 后     禁止发布(避免周末事故)
# 每季度一次        MAJOR 升级,提前 2 周发布通知

# 变更冻结期(change freeze):
# - 财年末审计周、双十一/黑五等大促前 1 周
# - 冻结期只接受安全补丁与紧急修复(需 VP 级审批)
发布前 5 分钟自查清单 ① 回滚脚本已准备好且验证过 ② 数据库迁移 down script 存在 ③ 监控面板已打开 ④ 审批已通过 ⑤ 值班人已通知。

变更风险分级与审批矩阵

级别变更示例审批要求发布窗口
低风险文案修改、配置开关、依赖 PATCH 升级代码评审 + 流水线全绿任意时段(避开大促窗口)
中风险接口行为变更、依赖 MINOR 升级、慢查询修复技术负责人审批 + 回滚方案发布日历时段
高风险数据库迁移、核心链路重构、认证/支付变更技术负责人 + 运维双人审批、金丝雀放量夜间低峰 + 值班人待命

分级的关键是让"风险"有共识定义:影响用户数、影响面宽度、回滚难度三个维度各打 1-3 分,总分 ≥7 即高风险——用表格代替"我觉得挺危险"的口头判断,变更评审会才能快而准。

10. 交付可观测性:让流水线自己可诊断

流水线也会"生病":构建越来越慢、失败率悄悄上升、发布到恢复的耗时越来越长。用 DORA 四指标给交付过程装上仪表盘,问题在变成事故前被发现:

指标定义优秀目标数据来源
变更前置时间(Lead Time)代码合入到上线的时间< 1 天PR merged → 部署时间
部署频率(Deploy Frequency)生产部署次数 / 周≥ 每天部署流水线执行记录
变更失败率(CFR)失败部署 / 总部署< 15%部署后 1 小时内回滚或修复
恢复时间(MTTR)故障到恢复的时间< 1 小时告警开始 → 服务恢复
# 用 GitHub Actions API 统计部署频率与失败率(每月跑一次)
gh run list --workflow=deploy.yml --limit 100 \
  --json status,conclusion,createdAt,updatedAt \
  | jq -r '.[] | "\(.conclusion) \(.createdAt) \(.updatedAt)"' > /tmp/runs.txt
# 失败率 = failure 行数 / 总行数
awk '$1=="failure"{f++} END{print "failure_rate:", f/NR*100"%"}' /tmp/runs.txt

# 发布后 30 分钟观察窗自动化(错误率是回滚决策的依据)
curl -s "http://prometheus:9090/api/v1/query" \
  --data-urlencode 'query=sum(rate(http_requests_total{job="web",status=~"5.."}[5m]))/sum(rate(http_requests_total{job="web"}[5m]))' \
  --data-urlencode 'time='"$(date +%s)"''
# 判断规则:错误率 > 1% 或 P99 > SLO,持续 5 分钟 → 触发回滚流程

把四指标画进 Grafana,月度复盘直接看趋势:Lead Time 变长说明门禁过重或构建变慢,CFR 上升说明测试覆盖在退化——指标会告诉你流水线该往哪个方向调。

常见错误

  • 镜像 tag 用 latest 上生产——生产环境必须用明确的 SemVer 或 SHA tag,latest 无法追溯和回滚
  • Secrets 硬编码在 workflow 文件中——永远不要在 .yml 中写明文密钥,使用 secrets.* 引用 GitHub Secrets
  • 健康检查超时设置过短——容器启动后应用可能需要更长时间初始化,将 sleep 和重试机制结合使用
  • 没有数据库迁移回滚脚本——每个 migration 必须配套 down script,否则应用回滚后数据库 schema 不兼容
  • 所有环境共享同一套密钥——dev/staging/prod 应各自独立配置 Secrets,避免测试密钥泄漏到生产
  • PR 门禁总耗时超过 10 分钟——开发者会开始批量提交、绕过测试。把重活(矩阵测试、E2E)放到合入后或 staging 部署后,PR 阶段只留秒级门禁
  • 回滚脚本没验证过——事故时的回滚动作与日常部署路径不同,最容易在关键时刻出错。每季度做一次回滚演练,把耗时记进发布台账

最佳实践

  • 采用 Trunk-based Development——短生命周期的 feature branch,快速合并到 develop,避免长期分支合并冲突
  • 容器镜像双 tag——同时打 SemVer(如 v1.2.3)和 Git SHA(如 sha-a1b2c3d),兼顾语义清晰和唯一追溯
  • 流水线分层执行——lint → test → build → deploy,每层失败立即终止,节省资源和时间
  • 生产部署使用 concurrency 锁——防止多个生产部署同时执行导致状态不一致
  • 定期轮换密钥——每 90 天轮换一次数据库密码和 API Key,泄漏时立即轮换
  • 本地预检与 CI 分工——lint/typecheck 在本地 pre-commit 完成,CI 专注矩阵测试、镜像构建与部署,反馈循环各司其职
  • 用 DORA 指标复盘交付——季度复盘对比 Lead Time、部署频率、变更失败率、恢复时间四指标,用数据决定流水线优化方向

练习题

  1. 为一个 Node.js 项目编写完整的 GitHub Actions workflow,包含 lint、test(矩阵测试 Node 18/20/22)、build 镜像并推送到 GHCR,要求生产部署前有审批门禁。
  2. 设计一个蓝绿部署方案:用 Docker Compose 管理两个容器组(blue/green),通过 Nginx 上游切换实现零停机回滚,编写对应的回滚脚本。
  3. 模拟生产事故:在 staging 环境故意部署一个有 bug 的版本,验证自动健康检查失败后能否正确触发回滚流程,并记录回滚耗时。
  4. 给现有流水线配置"发布后 30 分钟观察窗":采集错误率与 P99,设置自动回滚触发条件(错误率 > 1% 持续 5 分钟),并做一次演练。
  5. 用 DORA 四指标评估你当前的交付流程:列出每个指标的计算方法与当前数据,找出最薄弱的一环并给出改进方案。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 CI/CD 在生产环境中的最佳实践尝试向他人讲解
命令操作能不查文档完成 CI/CD 生产配置、流水线优化在终端实际执行
原理掌握能说出 CI/CD 的安全控制和审计机制画出流程图
故障排查能独立排查生产环境部署失败、回滚问题模拟故障并修复
最佳实践能说明为什么需要为生产环境配置蓝绿部署和金丝雀发布对比不同方案

本章总结

生产级 CI/CD 的核心是把"构建、发布、部署"的每个环节显式化、可审计化:分支策略决定触发范围,审批门禁守住生产入口,SemVer + SHA 双 tag 让每次发布可追溯、可秒级回滚,密钥独立分环境并定期轮换。流水线成熟度从手动部署到渐进式交付逐级提升,每一步都依赖清晰的门禁与回滚能力兜底。

延伸阅读

↑ 回到顶部