本文参考《使用 k8s 搭建一个小巧但健全的 CI/CD 系统》的完整链路,但不沿用旧的云厂商命令和组件版本。重点改为:一条小团队真正能维护、能验证、能回滚的发布路径需要哪些不变量。
“用了 Kubernetes”不等于拥有 CI/CD。真正的流水线必须能回答:这次发布对应哪次提交、构建产物是否唯一、谁有生产权限、部署有没有完成、失败后回到哪里。
先定义最小完成条件
一次发布至少满足:
- 同一提交可以重复构建;
- 测试失败不会生成可发布版本;
- 镜像使用不可变标识;
- 生产部署有明确权限边界;
- 流水线等待 rollout 完成,而不是执行
kubectl apply后就报成功; - 可以找到上一稳定版本并回滚;
- 数据库变更与应用回滚兼容;
- 发布记录能关联 commit、镜像 digest、操作者和环境。
缺少任何一项,自动化可能只是“更快地执行一组命令”。
把 CI 和 CD 分开
CI 负责证明产物可以交付
典型步骤:
- 检出固定 commit;
- 安装锁定版本的依赖;
- 运行静态检查、单元测试和必要的集成测试;
- 构建镜像;
- 扫描依赖与镜像;
- 推送到镜像仓库;
- 输出镜像 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 指向具体内容。即使保留 main、stable 等便捷 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 卡住后明确失败;- 合理的
maxUnavailable与maxSurge; - 优雅终止和足够的
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:
- 先增加兼容结构;
- 新旧应用都能工作;
- 发布新应用并迁移数据;
- 确认旧版本不再需要;
- 最后删除旧结构。
如果一次发布修改了不可逆数据,回滚方案可能是“前滚修复”,必须在发布前写清楚。
发布后的验证属于流水线
rollout 完成只证明 Pod ready,不证明业务正确。生产发布后还应运行最小 smoke test:
- 关键 API 能否完成;
- 指标中的错误率与延迟是否异常;
- 新旧版本日志是否出现高频错误;
- 消息积压和数据库连接是否恶化;
- 版本信息是否与目标 commit/digest 一致。
高风险服务可以先灰度少量流量,再扩大范围。小团队不一定需要复杂的服务网格,但至少要给“观察多久、什么指标触发回滚”写出规则。
推荐的落地顺序
- 先做到提交触发测试;
- 再生成 commit SHA 镜像并推送仓库;
- 为测试环境自动部署并等待 rollout;
- 补 smoke test 和回滚命令;
- 为生产增加环境审批和最小权限;
- 用 OIDC 替换长期云密钥;
- 环境和团队复杂度上升后再评估 GitOps;
- 最后再考虑弹性 runner、金丝雀和自动回滚。
先让一条路径可靠,再扩展组件。CI/CD 的质量最终体现在失败是否可见、发布是否可追踪、回滚是否真的可用。