一、CI 与 CD 的职责切分
传统流水线把编译、测试、打镜像、kubectl apply 全塞进一个 workflow,问题在于谁都能从 CI 直接改生产集群,集群的最终状态没有单一事实来源。
CI 侧(GitHub Actions)
触发:push / pull_request
职责:lint、测试、构建镜像、推送 registry、产出 SBOM
产出:一个不可变的镜像 digest
不接触:集群 kubeconfig 与任何集群写权限
CD 侧(ArgoCD / Flux 控制器)
触发:部署仓库的 manifest 变更
职责:把期望状态同步到集群,检测并纠正漂移
产出:集群实际状态等于 Git 声明状态
不接触:源码与构建过程
1.1 边界在哪里
属于 CI
- 单元与集成测试、镜像构建与扫描
- 更新部署仓库里的镜像 tag(见第六章)
属于 CD
- 把 manifest 应用到集群
- 渐进式发布、漂移检测、自动同步、回滚
不该做
- CI 里 kubectl apply,等于绕过 Git 成为事实来源
- CD 控制器里跑测试或构建,违反单一职责
切分后有两个收益:CI 的凭证里不再需要集群管理员 kubeconfig,攻击面收窄;集群状态永远可从 Git 历史回溯,回滚等于 git revert。
二、镜像仓库与标签策略
2.1 短哈希与语义化标签
- name: Compute image tag
id: meta
run: echo "tag=sha-$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: registry.example.com/app:${{ steps.meta.outputs.tag }}
标签类型 可变性 用途
sha-abc1234 不可变 生产部署的唯一引用,推荐
v1.4.2 不可变 发布里程碑,人工识别
1.4 可变 指向 1.4.x 最新,慎用于生产
latest 可变 仅本地开发
sha256:xxxx... 不可变 真正的内容寻址,最严格
短哈希与 commit 一一对应、便于追溯、天然去重,缺点是人工读起来无意义。多架构镜像推送后生成 manifest list,digest 指向 list 而非单架构镜像,Kubernetes 按节点架构自动选层。
2.2 用 digest 锁定
image: registry.example.com/app@sha256:9f2c1a...e3b
最稳的做法是引用 digest 而非 tag。嫌 digest 难读时,折中是 tag + digest 同时写入,tag 供人看、digest 供机器校验。
三、GitOps 仓库结构与覆盖方式
3.1 应用仓库与部署仓库分离
app-repo(源码)
.github/workflows/ci.yml 构建、测试、推镜像
src/ ... Dockerfile
deploy-repo(部署清单)
apps/web/
base/{deployment.yaml,service.yaml,kustomization.yaml}
overlays/{dev,staging,prod}/kustomization.yaml
clusters/prod-ap-northeast-1/apps.yaml
分离的好处:部署仓库的提交历史就是一部发布史,且可以给它设置比源码仓库更严的 CODEOWNERS 与审批规则。
3.2 Kustomize 覆盖
# apps/web/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: web-prod
resources:
- ../../base
images:
- name: registry.example.com/app
newTag: sha-abc1234
patches:
- path: replicas-patch.yaml
images 字段是 CI 更新 manifest 最省事的入口:只改一行 newTag 即可完成一次发布。patches 指向的补丁文件只写差异字段,例如只声明 spec.replicas: 6。
3.3 Helm values 覆盖
# deploy-repo/charts/web/values-prod.yaml
image:
repository: registry.example.com/app
tag: sha-abc1234
replicaCount: 6
选择建议
Kustomize 无模板、纯覆盖、被 ArgoCD 原生渲染,适合自有服务
Helm 有模板与依赖管理,适合引入第三方 chart
混合 用 Helm 生成 base,再用 Kustomize 打补丁
四、ArgoCD Application 与 ApplicationSet
4.1 一个 Application 清单
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/deploy-repo.git
targetRevision: main
path: apps/web/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: web-prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
selfHeal: true 让控制器在被手工改动后自动拉回 Git 状态,是漂移检测的第一道防线;prune: true 会删除 Git 中已移除的资源,生产首次启用前务必确认无误删风险。
4.2 ApplicationSet 批量生成
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: web-all-envs
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- matrix:
generators:
- list:
elements:
- { env: dev, cluster: https://kubernetes.default.svc }
- { env: prod, cluster: https://prod.example.com }
- git:
repoURL: https://github.com/my-org/deploy-repo.git
revision: main
files:
- path: "apps/*/config.json"
template:
metadata:
name: "{{.path.basename}}-{{.env}}"
spec:
source:
repoURL: https://github.com/my-org/deploy-repo.git
targetRevision: main
path: "apps/{{.path.basename}}/overlays/{{.env}}"
destination:
server: "{{.cluster}}"
goTemplateOptions: ["missingkey=error"] 能避免模板变量拼错时静默生成一个名为 <no value> 的应用。权限上再用 AppProject 限定 sourceRepos 与 destinations,把「谁能 sync prod」收进 RBAC。
五、Flux 的 GitRepository 与 Kustomization
5.1 源与同步单元
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: deploy-repo
namespace: flux-system
spec:
interval: 1m
url: https://github.com/my-org/deploy-repo.git
ref:
branch: main
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: web-prod
namespace: flux-system
spec:
interval: 5m
path: ./apps/web/overlays/prod
prune: true
wait: true
targetNamespace: web-prod
sourceRef:
kind: GitRepository
name: deploy-repo
dependsOn:
- name: infra-crds
dependsOn 是 Flux 相对 ArgoCD 的显式能力:CRD 必须先就绪,业务 Kustomization 才能应用,否则会因找不到 CRD 而反复重试。
5.2 镜像自动化
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: app
namespace: flux-system
spec:
imageRepositoryRef:
name: app
policy:
semver:
range: ">=1.4.0"
配合 ImageUpdateAutomation 与 manifest 里的标记注释,控制器才知道该改哪一行:
image: registry.example.com/app:1.4.2 # {"$imagepolicy": "flux-system:app"}
5.3 ArgoCD 与 Flux 的取舍
维度 ArgoCD Flux
界面 Web UI 丰富、可视化强 以 CLI 与 CRD 为主
多集群 单实例管多集群,集中式 每集群一套控制器,分布式
同步粒度 Application 为单位 Kustomization 为单位
镜像更新 argocd-image-updater(外挂) ImageUpdateAutomation(内置)
适用 团队要可视化、集中治理 平台团队偏好声明式
两者可以共存:Flux 管集群基础组件,ArgoCD 管业务应用,互不干扰。
六、Actions 更新 manifest 的三种做法
6.1 直接提交
- name: Update image tag
run: |
git clone https://x-access-token:${{ secrets.DEPLOY_REPO_TOKEN }}@github.com/my-org/deploy-repo.git /tmp/deploy
cd /tmp/deploy
sed -i "s|newTag: .*|newTag: sha-${{ github.sha }}|" apps/web/overlays/prod/kustomization.yaml
git commit -am "chore: bump web to sha-${{ github.sha }}" && git push
链路最短、无额外组件,但没有评审、没有审计,main 分支保护形同虚设,只适合个人项目与内部工具。
6.2 argocd-image-updater
让 ArgoCD 侧自己发现新镜像,Actions 完全不需要写部署仓库:
metadata:
annotations:
argocd-image-updater.argoproj.io/image-list: app=registry.example.com/app
argocd-image-updater.argoproj.io/app.update-strategy: newest-build
argocd-image-updater.argoproj.io/app.allow-tags: regexp:^sha-[0-9a-f]{7}$
argocd-image-updater.argoproj.io/write-back-method: git:secret:argocd/git-creds
allow-tags 必须写死成哈希正则,否则任何被推送的 tag(包括误推的 latest)都会触发更新。这种做法 CI 零改动、职责彻底分离,代价是更新时机不由 CI 决定,无法与测试门禁串联。
6.3 PR 化更新
生产环境最推荐的做法:CI 开一个 PR,由人评审合并。
git clone https://x-access-token:$TOKEN@github.com/my-org/deploy-repo.git /tmp/deploy
cd /tmp/deploy
git checkout -b "release/web-sha-${GITHUB_SHA:0:7}"
sed -i "s|newTag: .*|newTag: sha-${GITHUB_SHA:0:7}|" apps/web/overlays/prod/kustomization.yaml
git commit -am "chore: bump web to sha-${GITHUB_SHA:0:7}" && git push -u origin HEAD
gh pr create --repo my-org/deploy-repo \
--title "chore: bump web to sha-${GITHUB_SHA:0:7}" \
--body "Automated image bump from ${GITHUB_REPOSITORY}@${GITHUB_SHA}"
维度 直接提交 image-updater PR 化
CI 改动 需要 不需要 需要
人工评审 无 无 有
审计追溯 弱 中 强
生产适用度 不推荐 中 推荐
七、Argo Rollouts 金丝雀与渐进式交付
把 Deployment 换成 Rollout 即可获得金丝雀能力:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: web
namespace: web-prod
spec:
replicas: 10
strategy:
canary:
canaryService: web-canary
stableService: web-stable
steps:
- setWeight: 10
- pause: { duration: 5m }
- analysis:
templates:
- templateName: success-rate
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
selector:
matchLabels:
app: web
template:
spec:
containers:
- name: web
image: registry.example.com/app:sha-abc1234
分析模板决定何时自动中止:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: web-prod
spec:
metrics:
- name: success-rate
interval: 1m
count: 5
successCondition: result[0] >= 0.99
failureLimit: 1
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{code!~"5.."}[1m]))
/ sum(rate(http_requests_total[1m]))
指标不达标时 Rollout 自动中止并回退到 stable 版本,这是 GitOps 里「发布失败自动止损」的关键一环。日常操作只需三条命令:
kubectl argo rollouts get rollout web -n web-prod # 查看进度
kubectl argo rollouts promote web -n web-prod # 手动晋级
kubectl argo rollouts abort web -n web-prod # 中止并回退
八、推送式部署的取舍
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: azure/k8s-deploy@v5
with:
namespace: web-prod
manifests: |
manifests/deployment.yaml
images: |
registry.example.com/app:sha-${{ github.sha }}
strategy: canary
percentage: 20
维度 推送式(CI kubectl) 拉取式(GitOps 控制器)
事实来源 集群(Git 只是输入) Git
漂移纠正 无 自动
凭证位置 CI secret 持有 kubeconfig 控制器在集群内
集群暴露面 CI 需要可达集群 API 集群主动出站拉取
回滚 重跑旧 workflow git revert
多集群 每个集群各配一份凭证 一份 Git 管全部
适用 一次性运维脚本、集群引导 长期运行的集群
推送式并非一无是处:集群引导、GitOps 控制器升级前的手工介入都适合直接推送。真正的坑是两者长期并存——CI 用 kubectl set image 改了 Deployment,五分钟后 selfHeal: true 把它当成漂移改回旧 tag,服务出现「发布成功又自动回退」的诡异现象。要么彻底走 GitOps 删掉 CI 里的 kubectl 步骤,要么临时禁用 selfHeal 并标注原因。
九、漂移检测与回滚
9.1 漂移检测
# ArgoCD 查询所有 OutOfSync 的应用
argocd app list -o json | jq -r '.[] | select(.status.sync.status=="OutOfSync") | .metadata.name'
argocd app diff web-prod
# Flux 检查 Kustomization 状态
flux get kustomizations --status-selector ready=false
flux reconcile kustomization web-prod --with-source
常见漂移来源
1) 有人 kubectl edit 手工改了副本数或镜像
2) HPA 动态改了 replicas
3) MutatingWebhook 注入了 sidecar
4) 其他控制器改了 Service 的 clusterIP
排除 HPA 与 webhook 造成的「假漂移」:
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers: [/spec/replicas]
- group: ""
kind: Service
jsonPointers: [/spec/clusterIP]
9.2 回滚
argocd app history web-prod
argocd app rollback web-prod 12
# Flux 没有内置 history,回滚即 git revert
git revert <bad-commit-sha> && git push
回滚策略选择
代码问题 git revert 部署仓库,控制器自动同步
镜像问题 改 newTag 或 digest 回上一个版本
配置漂移 删除手工改动,让控制器拉回
CRD/数据迁移 不能靠回滚解决,需要前滚修复
生产发布前后各确认一次状态:发布前确保镜像用 digest 锁定、部署仓库 PR 通过 CODEOWNERS、selfHeal 与 prune 语义已确认;发布后确认状态为 Synced 且 Healthy、无 OutOfSync 残留,并记录本次的 commit 与镜像 digest。
总结
GitHub Actions 与 Kubernetes 的 GitOps 协同,本质是把流水线从「一串命令」变成「两个职责清晰的系统」。CI 侧只负责把源码变成不可变镜像,产出短哈希标签或 digest,再通过直接提交、argocd-image-updater 或 PR 化三种方式之一更新部署仓库;CD 侧交给 ArgoCD 或 Flux 控制器,用 Application、ApplicationSet、GitRepository、Kustomization 这些声明式资源把 Git 状态同步到集群。生产发布再叠加 Argo Rollouts 的金丝雀与分析指标,让失败自动止损。推送式部署在引导和一次性脚本里仍有位置,但绝不该与拉取式长期并存,否则 selfHeal 会与 kubectl 互相打架。最后用漂移检测与 git revert 兜住回滚,集群的每一个状态就都能追溯到一次提交。
延伸阅读:
- GitHub Actions 容器化 CI/CD — 镜像构建与推送细节
- GitHub Actions 与 Terraform 基础设施即代码 — 基础设施层 GitOps
- GitHub Actions 企业级治理 — 跨仓库权限与凭证治理
- GitHub Actions OIDC 云认证 — 免密推送镜像到云 registry
- GitHub Actions 安全加固 — 第三方 action 与权限最小化
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。