GitOps 原则的存在是为了解决在使用 Kubernetes 这样复杂系统时可见性和协作方面的实际问题。这些原则强调声明式期望状态和持续调和的重要性。然而,它们在具体实施上留有很大的解释空间。我们经常收到关于如何构建 Git 仓库、使用什么配置管理工具以及如何使用分支等问题。
你使用的配置管理工具不会对实施 GitOps 的能力产生重大影响。Helm 和 Kustomize 都满足了标准化清单集合的需求,并且能够根据特定部署环境修改这些清单。这种抽象对于保持清单的 DRY[1](不重复原则)非常方便。
然而,这些抽象也带来了新的问题。GitOps 工具通常直接引用它们来确定期望状态。对 Helm Chart 或 Kustomization 基础的更改就是对抽象的更改;这种更改对部署到环境中的清单的真实影响并不明确。
Argo CD 在运行时渲染清单的示意图
核心前提
Kubernetes 集群的期望状态不是一个 Helm Chart 或 Kustomization;而是由这些配置管理工具渲染出的清单。遗憾的是,大多数组织在实践 GitOps 时通常会将其抽象出来存储在 Git 中。在 git 和 kubectl 之间的任何抽象都可能产生意外效果;你不应该在应用之前修改你的真实来源。
存储在 Git 中并经过审批的期望状态不应该包含任何与将要应用到集群的内容不同的抽象。它应该像容器镜像一样被对待,是不可变的并按原样应用到集群中。让 GitOps 工具在将清单应用到集群时运行 Helm 或 Kustomize,就像让你的容器在启动时运行 apt-get install 一样。
解决方案就是渲染清单模式。对 GitOps 仓库主干(即 main 分支)的每次更改都会触发 CI 工作流来渲染清单,并将渲染后的清单按原样存储在 Git 中。这个制品代表了集群的期望状态,没有任何混淆。
渲染后的清单应该被分离到特定环境的分支中。当这些分支发生更改时,提交之间的清单差异将完全透明。你可以清楚地看到 main 分支上的更改对每个环境的影响。
Argo CD 应用在 CI 中渲染的清单的示意图
渲染清单模式的优势一目了然:
- 消除配置管理工具(如 Helm、Kustomize)引入的混淆,提高期望状态的可见性。
- 通过真正不可变的期望状态降低内置工具带来的风险。
- 通过一次性渲染清单提高 Argo CD 的性能。
- 基于环境设置部署和保护策略。
值得注意的是,这种模式有两个缺点:
- 将清单渲染转移到 CI 引擎增加了复杂性。
- 它与渲染明文密钥的工具(如 Sealed Secrets)配合不好。
你可能仍然持怀疑态度,让我们详细分析一下。
这不就是 Gitflow 吗?
让我们先解决这个问题。表面上看,渲染清单模式可能听起来像 Gitflow[2],但实际上并不是。你可以继续在 GitOps 仓库中使用短期特性分支或主干开发模式。
特定环境的分支并不用于环境之间的 promotion[3] - 一个环境分支中的更改不会被合并到另一个分支中。相反,这些分支的内容由自动化工作流维护,并基于 main 分支的内容生成。
可以将这些分支的内容视为一个发布包,其中包含了环境期望状态的纯(渲染后的)清单。就像贡献者永远不应该拉取容器镜像、修改内容并推回注册表一样,任何人都不应该直接提交到特定环境的分支。它们是基于 main 分支内容生成的制品。
消除混淆以提高可见性
看看下面这个在 GitOps 仓库中维护的 Helm Umbrella chart[4] 更改的例子:
![显示依赖版本变更的 Helm Umbrella Chart 截图](https://images.akuity.io/blog/th](https://images.akuity.io/blog/the-rendered-manifests-pattern/helm-dep-update-diff.png)
看这个差异,很难直观地知道结果会是什么。你只知道 Helm chart 依赖的版本在改变。它没有提供渲染并应用到集群的清单变更的上下文。
再看一个使用 Kustomize 的例子:
显示 Kustomization 基础更改的截图
要了解会发生什么变化,你需要检查使用这个 Kustomize 基础的每个环境。
如果你想确定最终的清单(实际部署到集群中的资源),你需要运行 helm template 或 kustomize build。在 Argo CD 中,repo server 执行这个功能。每当 Application 的源发生变化或缓存过期时,它就会运行 helm template、kustomize build 或使用 CMP 的任何自定义工具来生成清单。然后这些清单被传递给 application controller 执行 kubectl apply。
清单的更改应该被持续集成并产生不可变的制品,就像代码一样。将清单生成移到 CI 工作流中并存储在特定环境的分支上,将提供一个不可变的期望状态,其中的更改清晰可见。
使用上面的 Helm chart 例子,这就是使用渲染清单模式时差异的样子。chart 版本的一行更改导致生成的清单中有近 1,000 行的变化。
通过不可变期望状态降低工具风险
GitOps 工具的升级可能会导致内置工具链[5](即 Kustomize、Helm)的升级,这可能会影响它管理的每个环境。记住 - git 和 kubectl 之间的任何抽象都可能产生意外效果。
当升级 Argo CD 的差异看起来是这样的...
...你怎么能确定这对这个 Argo CD 实例中每个 Application 渲染的清单有什么影响?(在这个例子中,从 5.42.0 升级到 5.43.0 的 argo-cd Helm chart 将 Argo CD 升级到 v2.8.0,其中包括将 helm 版本升级到 3.12.0。)
持续集成的核心前提是快速反馈,在问题进入生产系统之前发现它们。这个原则同样适用于描述 Kubernetes 集群期望状态的清单。当 GitOps 工具负责生成清单时,问题直到更改被批准、合并到 main 并尝试应用到集群后才会被发现。在这个时候,修复它需要重复整个过程。
通过将清单生成移到 CI 工作流中并将清单存储在特定环境的分支中,你可以为集群的期望状态获得保证的稳定性。原始 YAML 被捕获在一个不可变的版本(提交)中。Argo CD 不再转换 Git 中的内容;它直接将其应用到集群中而不做修改。
提升 Argo CD 的性能
在 Argo CD 中运行 Kustomize 和 Helm 是很消耗资源的。每次 Application 的目标版本发生变化、缓存过期或有人执行强制刷新时,repo server 都需要重新生成清单。
当你查看大型 Argo CD 部署中 repo server 的资源请求时,很容易看出清单生成有多消耗资源。我见过 repo server 被分配 32 个 CPU 和 200GiB 内存的情况!
这在单体仓库中尤其明显,其中一个文件夹中清单的更改可能会不必要地导致 Argo CD 为许多 Application 重新生成清单。
通过利用 CI 引擎生成原始清单并将其存储在特定环境的分支上,这个过程只需要使用按需计算资源执行一次(而不是持续为 repo server 预留资源)。
自动化开发环境并保护生产环境
到目前为止,这种模式的优势主要可以归因于"你应该在 CI 中渲染清单"这一部分。第二部分"并将它们存储在特定环境的分支中"现在发挥作用了。
将渲染后的清单分离到分支中,可以为每个环境设置不同的策略。
再次考虑显示 Kustomize 基础更改的差异。
如果不使用渲染清单模式并使用自动同步策略,这个更改将同时应用到每个环境。
使用渲染清单模式,在主分支上更改 kustomize 基础将导致在 CI 中渲染清单。对于非生产环境,清单可能会被自动部署,所以它们会直接推送到特定环境的分支。
大多数组织对生产环境做同样的事情感到不舒服(很少有组织成熟到可以实践持续部署)。相反,为生产环境渲染的清单可以推送到一个短期分支。然后,创建一个 PR,提议对包含现有期望状态的特定环境分支进行更改。
因为清单已经渲染完成,PR 中显示的差异将代表没有混淆或抽象的更改。那些审查和批准 PR 的人可以轻松理解更改的真实影响。
大多数源代码管理(SCM)提供商都有广泛的分支权限控制[6]。使用这些控制,你可以确保对生产环境期望状态的任何更改都遵循了正确的流程。例如,代码所有者已经给出批准审查,所有需要的检查都已成功运行,并且永远不会直接推送到生产分支。
你可能会想,"好吧,我可以在 Argo CD 中对生产环境的 Application 使用手动同步来获得类似的解决方案。"虽然这是对的,但缺点是当你的 Application 处于未同步状态时,期望状态是模糊的。Argo CD 显示了差异,但在执行同步时生成的清单可能会有所不同。
缺点
增加了 CI 复杂性
不可否认,这种模式增加了额外的 CI 自动化要求。不应低估使用 Argo CD 进行清单生成的简单性。Argo CD 在集成 Helm 和 Kustomize 并提供可靠的清单生成方面做了大量工作。
对于 Argo CD,已经有几次尝试将 Application 渲染清单的差异添加到 pull request 中。我创建了一个名为 argocd-diff-action 的 GitHub Action 来解决这个问题(它目前已经停止维护,因为我选择使用渲染清单模式为我的 Kubernetes 集群的期望状态生成不可变的制品,并提供清晰和信息丰富的差异)。Zapier 最近开源了他们的内部工具 `kubechecks`[7],它执行类似的功能,并添加了策略检查等功能。
在 Akuity,我们创建了一个内部使用的工具来采用渲染清单模式。虽然该项目仍在积极开发中,但目标是将其开源,这样 Argo CD 用户就可以使用他们现有的清单渲染定义(即 Application 清单),并使用该工具在 CI 中而不是在 Argo CD 中渲染它们。
渲染明文密钥
像 Kustomize + SOPS 这样的 Kubernetes Secrets 管理工具与渲染清单模式不兼容。它们允许用户在 Git 中存储加密的密钥,并依赖在集群中运行的工具来渲染清单和解密密钥。
这对于渲染清单模式来说并不理想,因为解密后的密钥会以明文形式出现在特定环境的分支中。
在采用这种模式之前,建议使用像 External Secrets Operator[8] 这样的工具,它使用 ExternalSecret[9] 资源,这些资源包含对 SecretStore 中数据的引用,可以安全地存储在 Git 中。然后它使用集群内控制器基于 ExternalSecrets 生成 Kubernetes Secret。在我们的博客文章 如何使用 GitOps 管理 Kubernetes 密钥?[10] 中,我们解释了为什么这是我们首选的方法,无论你是否使用渲染清单模式。
结论
虽然 GitOps 原则强调声明式期望状态和持续调和,但在构建 Git 仓库、选择配置管理工具和管理分支方面留下了很大的解释空间。渲染清单模式通过确保存储在 Git 中的期望状态保持清晰和不可变,并且在应用到集群时不进行转换或抽象,解决了这些问题。
这种模式通过提高可见性、消除混淆和降低工具风险,实现了更好的安全性和更安全的变更管理。它通过将清单生成过程转移到 CI 工作流中,提高了性能,特别是在大型 Argo CD 部署中。再加上为每个环境设置不同策略的能力,为你的 GitOps 工作流增加了灵活性和控制,同时保护生产环境。
大多数源代码管理(SCM)提供商都有广泛的分支权限控制[11]。使用这些控制,你可以确保对生产环境期望状态的任何更改都遵循了正确的流程。例如,代码所有者已经给出批准审查,所有需要的检查都已成功运行,并且永远不会直接推送到生产分支。
你可能会想,"好吧,我可以在 Argo CD 中对生产环境的 Application 使用手动同步来获得类似的解决方案。"虽然这是对的,但缺点是当你的 Application 处于未同步状态时,期望状态是模糊的。Argo CD 显示了差异,但在执行同步时生成的清单可能会有所不同。
缺点
增加了 CI 复杂性
不可否认,这种模式增加了额外的 CI 自动化要求。不应低估使用 Argo CD 进行清单生成的简单性。Argo CD 在集成 Helm 和 Kustomize 并提供可靠的清单生成方面做了大量工作。
对于 Argo CD,已经有几次尝试将 Application 渲染清单的差异添加到 pull request 中。我创建了一个名为 argocd-diff-action 的 GitHub Action 来解决这个问题(它目前已经停止维护,因为我选择使用渲染清单模式为我的 Kubernetes 集群的期望状态生成不可变的制品,并提供清晰和信息丰富的差异)。Zapier 最近开源了他们的内部工具 `kubechecks`[12],它执行类似的功能,并添加了策略检查等功能。
在 Akuity,我们创建了一个内部使用的工具来采用渲染清单模式。虽然该项目仍在积极开发中,但目标是将其开源,这样 Argo CD 用户就可以使用他们现有的清单渲染定义(即 Application 清单),并使用该工具在 CI 中而不是在 Argo CD 中渲染它们。
渲染明文密钥
像 Kustomize + SOPS 这样的 Kubernetes Secrets 管理工具与渲染清单模式不兼容。它们允许用户在 Git 中存储加密的密钥,并依赖在集群中运行的工具来渲染清单和解密密钥。
这对于渲染清单模式来说并不理想,因为解密后的密钥会以明文形式出现在特定环境的分支中。
在采用这种模式之前,建议使用像 External Secrets Operator[13] 这样的工具,它使用 ExternalSecret[14] 资源,这些资源包含对 SecretStore 中数据的引用,可以安全地存储在 Git 中。然后它使用集群内控制器基于 ExternalSecrets 生成 Kubernetes Secret。在我们的博客文章 如何使用 GitOps 管理 Kubernetes 密钥?[15] 中,我们解释了为什么这是我们首选的方法,无论你是否使用渲染清单模式。
结论
虽然 GitOps 原则强调声明式期望状态和持续调和,但在构建 Git 仓库、选择配置管理工具和管理分支方面留下了很大的解释空间。渲染清单模式通过确保存储在 Git 中的期望状态保持清晰和不可变,并且在应用到集群时不进行转换或抽象,解决了这些问题。
这种模式通过提高可见性、消除混淆和降低工具风险,实现了更好的安全性和更安全的变更管理。它通过将清单生成过程转移到 CI 工作流中,提高了性能,特别是在大型 Argo CD 部署中。再加上为每个环境设置不同策略的能力,为你的 GitOps 工作流增加了灵活性和控制,同时保护生产环境。
本文使用 Cursor 转译,原文地址:https://akuity.io/blog/the-rendered-manifests-pattern
参考资料
[1]DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself。
[2]Gitflow: https://nvie.com/posts/a-successful-git-branching-model/。
[3]环境之间的 promotion: https://developers.redhat.com/articles/2022/07/20/git-workflows-best-practices-gitops-deployments。
[4]Helm Umbrella chart: https://www.youtube.com/watch?v=MlAWr8bVr0I&t=170s。
[5]内置工具链: https://github.com/argoproj/argo-cd/blob/e37ff6f0ae02db0739bc480e987c19bd4573e082/hack/tool-versions.sh。
[6]分支权限控制: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule。
[7]kubechecks
: https://github.com/zapier/kubechecks。
[8]External Secrets Operator: https://external-secrets.io/latest/。
[9]ExternalSecret: https://external-secrets.io/latest/api/externalsecret/。
[10]如何使用 GitOps 管理 Kubernetes 密钥?: https://akuity.io/blog/how-to-manage-kubernetes-secrets-gitops/。
[11]分支权限控制: https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule。
[12]kubechecks: https://github.com/zapier/kubechecks。
[13]External Secrets Operator: https://external-secrets.io/latest/。
[14]ExternalSecret: https://external-secrets.io/latest/api/externalsecret/。
[15]如何使用 GitOps 管理 Kubernetes 密钥?: https://akuity.io/blog/how-to-manage-kubernetes-secrets-gitops/。