本文参考《使用 k8s 搭建一个小巧但健全的 CI/CD 系统》的完整链路,但不沿用旧的云厂商命令和组件版本。重点改为:一条小团队真正能维护、能验证、能回滚的发布路径需要哪些不变量。

“用了 Kubernetes”不等于拥有 CI/CD。真正的流水线必须能回答:这次发布对应哪次提交、构建产物是否唯一、谁有生产权限、部署有没有完成、失败后回到哪里。

先定义最小完成条件

一次发布至少满足:

  • 同一提交可以重复构建;
  • 测试失败不会生成可发布版本;
  • 镜像使用不可变标识;
  • 生产部署有明确权限边界;
  • 流水线等待 rollout 完成,而不是执行 kubectl apply 后就报成功;
  • 可以找到上一稳定版本并回滚;
  • 数据库变更与应用回滚兼容;
  • 发布记录能关联 commit、镜像 digest、操作者和环境。

缺少任何一项,自动化可能只是“更快地执行一组命令”。

把 CI 和 CD 分开

CI 负责证明产物可以交付

典型步骤:

  1. 检出固定 commit;
  2. 安装锁定版本的依赖;
  3. 运行静态检查、单元测试和必要的集成测试;
  4. 构建镜像;
  5. 扫描依赖与镜像;
  6. 推送到镜像仓库;
  7. 输出镜像 digest 和构建元数据。

CD 负责把已证明的产物放进环境

CD 不应重新编译。测试环境和生产环境应提升同一个镜像 digest,只改变配置和部署策略。否则“测试通过的产物”和“生产运行的产物”可能不是同一个东西。

镜像不要只用 latest

至少为镜像保存 commit SHA:

IMAGE="registry.example.com/order-service:${GITHUB_SHA}"

docker build --pull -t "$IMAGE" .
docker push "$IMAGE"

更严格的部署使用 registry 返回的 digest:

registry.example.com/order-service@sha256:...

tag 可能被移动,digest 指向具体内容。即使保留 mainstable 等便捷 tag,生产记录也应保存解析后的 digest。

Runner 是安全边界,不是普通构建机

自托管 runner 会执行仓库提供的代码。如果它同时能访问生产集群、Docker socket、云元数据或长期密钥,一次恶意 PR 就可能变成基础设施入侵。

基本原则:

  • 公共仓库不要让未受信任 PR 进入有生产网络权限的自托管 runner;
  • 构建 runner 与部署 runner 分离;
  • runner 尽量一次一用、执行后销毁;
  • GITHUB_TOKEN 默认只读,按 job 增加最小权限;
  • 第三方 Action 固定到完整 commit SHA;
  • .github/workflows/ 使用 CODEOWNERS 和人工评审;
  • 不把 /var/run/docker.sock 挂给不受信任任务;
  • 生产环境使用 required reviewers。

需要访问云平台时优先使用 OIDC 换取单 job 有效的短期凭据,而不是在仓库中长期保存云密钥。

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    environment: production
    # 通过云平台官方 OIDC 流程取得短期身份

OIDC 也不是自动安全。云端信任策略应限制仓库、分支、环境和可扮演角色。

两种适合小团队的 CD 方式

方式一:流水线直接部署

CI/CD 在受控 runner 中更新 Deployment:

kubectl -n production set image \
  deployment/order-service \
  order-service="$IMAGE"

kubectl -n production rollout status \
  deployment/order-service \
  --timeout=5m

优点是简单、依赖少;缺点是集群真实状态与 Git 可能不一致,runner 需要集群写权限。

适合环境少、团队小、发布频率不高的项目,但仍要保存 manifest、镜像和发布记录。

方式二:GitOps

流水线只更新环境仓库中的镜像 digest,集群内控制器负责拉取并收敛:

应用仓库提交
  → 构建并推送镜像
  → 更新环境仓库 digest
  → 评审/合并
  → 集群控制器同步

优点是期望状态和变更历史都在 Git 中,生产凭据不必交给普通构建 runner;代价是多一个控制器、仓库和故障面。

不要因为“GitOps 更先进”就直接增加复杂度。当环境数量、审计要求或多人协作让直接部署难以控制时,再切换更合理。

Deployment 要能判断成功和失败

一个可发布 Deployment 至少需要:

  • readiness probe:未准备好的 Pod 不接收流量;
  • liveness 或 startup probe:只在确实需要时重启;
  • requests/limits:让调度与过载行为可预测;
  • progressDeadlineSeconds:rollout 卡住后明确失败;
  • 合理的 maxUnavailablemaxSurge
  • 优雅终止和足够的 terminationGracePeriodSeconds

流水线必须检查 kubectl rollout status 的退出码。只有所有新副本可用、旧副本退出,Deployment 才算完成。

回滚不只有 kubectl rollout undo

应用镜像回滚可以使用:

kubectl -n production rollout history \
  deployment/order-service

kubectl -n production rollout undo \
  deployment/order-service

但这不能自动回滚数据库、消息格式和外部副作用。数据库迁移应优先满足 expand/contract:

  1. 先增加兼容结构;
  2. 新旧应用都能工作;
  3. 发布新应用并迁移数据;
  4. 确认旧版本不再需要;
  5. 最后删除旧结构。

如果一次发布修改了不可逆数据,回滚方案可能是“前滚修复”,必须在发布前写清楚。

发布后的验证属于流水线

rollout 完成只证明 Pod ready,不证明业务正确。生产发布后还应运行最小 smoke test:

  • 关键 API 能否完成;
  • 指标中的错误率与延迟是否异常;
  • 新旧版本日志是否出现高频错误;
  • 消息积压和数据库连接是否恶化;
  • 版本信息是否与目标 commit/digest 一致。

高风险服务可以先灰度少量流量,再扩大范围。小团队不一定需要复杂的服务网格,但至少要给“观察多久、什么指标触发回滚”写出规则。

推荐的落地顺序

  1. 先做到提交触发测试;
  2. 再生成 commit SHA 镜像并推送仓库;
  3. 为测试环境自动部署并等待 rollout;
  4. 补 smoke test 和回滚命令;
  5. 为生产增加环境审批和最小权限;
  6. 用 OIDC 替换长期云密钥;
  7. 环境和团队复杂度上升后再评估 GitOps;
  8. 最后再考虑弹性 runner、金丝雀和自动回滚。

先让一条路径可靠,再扩展组件。CI/CD 的质量最终体现在失败是否可见、发布是否可追踪、回滚是否真的可用。

参考资料