在传统的流水线中,CI 构建代码后,用 kubectl apply 或 helm upgrade 把结果推送进集群。这样也能工作,但流水线需要每个集群的管理员凭据;不去问集群,谁也说不准到底在运行什么;而被手动修改过的部署,会悄无声息地偏离仓库里的定义。
GitOps 把方向反了过来。Git 保存期望状态,集群内的代理持续拉取它,并让集群与之保持一致。本文讲解 GitOps 的原则、大多数团队使用的双仓库工作流、晋级和回滚的方式,并画出完整的流程。
四条原则
CNCF 的 OpenGitOps 项目 用四条原则定义了 GitOps:
- 声明式: 系统的期望状态以声明式的方式表达。
- 版本化且不可变: 这一状态的存储方式要能保留完整、不可变的版本历史,实践中就是 Git。
- 自动拉取: 软件代理从源头拉取期望状态。
- 持续调和: 代理不断比较实际状态与期望状态,并修正两者的差异。
第三条正是 GitOps 与“从读取 Git 的流水线进行部署”的区别所在。由集群来拉取,外部的任何东西都不会向集群推送。
两个仓库:代码与配置
大多数团队会区分两类变更:
- 应用仓库 存放源代码。CI 构建代码、运行测试、进行扫描,并把一个不可变的镜像标签推送到镜像仓库。
- 配置仓库 存放各个环境的 Kubernetes 清单、Helm values 或 Kustomize overlay。它描述了什么应该在哪里运行,包括镜像标签。
两者之间的连接是一个小而明确的变更:CI 或镜像自动化工具在配置仓库中发起一个 Pull Request,提升预发环境的镜像标签。这个 Pull Request 就是部署申请,自带评审和审计记录。
集群中的控制器
Argo CD 和 Flux 是两个被广泛使用的 GitOps 控制器。它们都运行在集群内部,轮询配置仓库或接收它的 Webhook,渲染清单,与线上对象进行比较,然后应用差异。
它们的调和设置决定了系统有多严格:
- 自动同步: 无需有人点按钮,就自动应用新的提交。
- 清理(prune): 删除已从 Git 中移除的对象。
- 自愈(self-heal): 撤销直接在集群中做出的变更。在 Argo CD 的 自动同步选项 中,自愈和自动同步是两个独立的开关。
正是自愈这一设置,让 Git 成为真正的事实来源。没有它,凌晨 3 点的一次 kubectl edit 就会变成永久而且看不见的变更。
晋级与回滚
环境就是配置仓库中的目录、overlay 或文件。晋级就是把一个版本从一个环境复制到下一个环境:
- Pull Request 提升预发环境的标签;合并后几分钟内,预发环境就会更新。
- 针对预发环境运行测试和检查。
- 第二个 Pull Request 把生产环境提升到同一个标签。合并它,就是生产部署。
回滚就是对那次合并执行 git revert。控制器看到旧标签,便会应用它。由于全部历史都在 Git 中,“上周二 14:00 生产环境运行的是什么?”这个问题有准确的答案。
由谁来发起 Pull Request
把新镜像接入配置仓库,常见的方式有两种:
- 由 CI 发起。 流水线的最后一步克隆配置仓库,修改一行,提交到某个分支并发起 Pull Request。这种方式明确、易于调试,而且流水线只需要这一个仓库的令牌。
- 由镜像自动化工具提交。 Flux 的镜像自动化控制器和 Argo CD Image Updater 项目可以监视镜像仓库,挑选符合策略的新标签,并把它们写回 Git。适合那些应该始终运行最新构建的预发环境。
很多团队会把两者结合起来:用自动化让预发环境保持最新,而生产环境的变更总是以经过评审的 Pull Request 形式到来,由人或发布任务发起。无论选择哪种方式,都要把它写在架构图上,因为“新版本是怎么到这里的?”是任何人看到 GitOps 方案时问的第一个问题。
一次发布的完整过程
跟随一个 shop 服务的 1.8.0 版本,从提交一路走到生产环境:
- 开发者把一个变更合并到应用仓库。CI 运行测试、构建镜像、进行扫描,并把
shop:1.8.0推送到镜像仓库。 - CI 在配置仓库中发起一个 Pull Request,把预发环境的标签从 1.7.3 改为 1.8.0。评审者批准并合并。
- 一两分钟内,控制器发现新的提交,渲染预发环境的清单并更新部署。Kubernetes 滚动发布新的 Pod,新 Pod 从镜像仓库拉取镜像。
- 预发环境的冒烟测试通过。发布负责人发起第二个 Pull Request,把生产环境设置为 1.8.0。
- 合并之后,生产环境以同样的方式发布。如果错误率上升,回滚那次合并提交就会恢复到 1.7.3。
整个过程中,集群外部的任何东西都不需要集群凭据,而且每一步都留下了一个提交。
密钥
明文密钥不能放进 Git。常见做法有:
- 在 Git 中加密存储, 使用 Sealed Secrets 或 SOPS 等工具,只在集群内部解密。
- Git 中只放引用,值放在别处, 借助 External Secrets 这类从密钥管理服务读取的 Operator。
无论哪种方式,配置仓库都保持可评审,密钥的值也永远不会出现在 diff 中。
架构图
图表内容
- Developer
- App repo: code
- CI: build, test, scan
- Container registry
- Config repo: desired state
- Kubernetes cluster
- GitOps controller
- staging
- shop v1.8.0
- production
- shop v1.7.3
- push
- trigger
- image 1.8.0
- PR: bump staging tag
- pull every few minutes
- apply
- nodes pull images
这张图的读法如下:
- 顶部一行,构建: 开发者推送到应用仓库,CI 构建并测试镜像 1.8.0,然后推送到镜像仓库。
- 中间: CI 发起一个 Pull Request,提升配置仓库中预发环境的标签。这是 CI 唯一接触部署的地方。
- 底部,集群: GitOps 控制器拉取配置仓库(虚线),并应用到预发命名空间(现在运行 1.8.0)和生产环境(在它自己的 Pull Request 合并之前仍运行 1.7.3)。节点从镜像仓库拉取镜像(点线)。
- Markdown 块 写明了工作流规则:通过 Pull Request 晋级、通过 revert 回滚、通过自愈撤销漂移。
团队会发生哪些变化
- CI 不再持有集群凭据。 是集群主动向外拉取,流水线只需要对一个仓库有写权限。
- 评审覆盖了部署。 谁在什么时候、出于什么原因部署了什么,都记录在 Pull Request 历史里。
- 漂移变得可见。 控制器会展示未同步的资源,而不是把它们藏起来。
- 调试的位置变了。 “为什么这个没部署上去?”要从控制器的同步状态和事件中找答案,所以要让负责合并的人能看到它们。
GitOps 架构图上应该画什么
- 两个仓库, 以及连接它们的 Pull Request。
- 拉取的方向, 是从控制器到配置仓库,而不是从流水线到集群。
- 每个环境, 连同它运行的版本,让晋级过程一目了然。
- 同步设置: 自动同步、清理、自愈。
部署策略图 展示了发布本身如何采用金丝雀或蓝绿方式,CI/CD 流水线图 详细介绍了构建阶段,Kubernetes 架构图 展示了集群本身,从 Kubernetes YAML 生成架构图一文 则介绍了如何把配置仓库中的清单变成一张图。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加 C4 模型节点包中的一个 人员,DevOps 节点包中的两个 代码仓库 节点和一个 流水线,以及 Docker 节点包中的一个 镜像仓库。
- 添加基础节点包中的一个 框架 表示集群,并在其中放入一个表示 GitOps 控制器的 容器。
- 添加 Kubernetes 节点包中的两个 Namespace 节点,每个里面放一个 Deployment,并在标签中注明它运行的版本。
- 用一条从控制器指向配置仓库的虚线表示拉取,并添加一个基础节点包中的 Markdown 块,写明工作流规则。
这个示例共用 12 个图表节点,在 Free 套餐的限制之内。
常见问题
GitOps 和 CI/CD 有什么区别? CI/CD 负责构建、测试和交付软件。GitOps 是完成其中交付部分的一种方式:期望状态保存在 Git 中,由目标环境中的代理拉取并调和,而不是由流水线把变更推送进去。
应该用 Argo CD 还是 Flux? 两者实现的是同一种模型。Argo CD 有丰富的 Web 界面和以应用为中心的视图;Flux 是一组可组合的控制器,很多团队完全通过 Git 和 CLI 来驱动它。
应用仓库和配置仓库需要分开吗? 这是一种常见的约定,而不是硬性规定。分开的仓库能让部署历史保持清晰,并且无需重新构建代码就能修改配置。
用 GitOps 如何回滚? 回滚配置仓库中修改版本的那个提交。控制器会应用之前的状态。
GitOps 中如何处理密钥? 把它们加密后存进 Git,或者在 Git 中只存引用,由集群内的 Operator 从密钥管理服务中获取实际的值。