大型组织应用 GitOps 难免会遇到在多环境中部署的问题,本文分析了应用环境分支策略会遇到到问题,介绍了应用文件夹策略解决这些问题的方案。原文:Stop Using Branches for Deploying to Different GitOps Environments[1], How to Model Your Gitops Environments and Promote Releases between Them[2]
在关于GitOps问题的指南中,我们简要解释了(参见第 3 和第 4 点)当前 GitOps 工具在支持不同环境部署以及多集群配置建模时的问题。
“如何将发布部署到下一个环境?”的问题在希望采用 GitOps 的组织中越来越受到重视[4],并且有几种可能的答案。但在这篇文章中,我们将重点讨论在这一过程中不应该做什么。
我们不应该使用 Git 分支来建模不同的环境。如果保存配置的 Git 存储库(在 Kubernetes 的例子中是 manifests/templates)有名为“预发”、“QA”、“生产”等分支,那就掉进了陷阱。
重要的事情说三遍:
使用 Git 分支来建模不同的环境是一种反模式,不要这样做!
使用 Git 分支来建模不同的环境是一种反模式,不要这样做!
使用 Git 分支来建模不同的环境是一种反模式,不要这样做!
我们将从以下几点探讨为什么这个实践是反模式:
- 在部署环境中使用不同的 Git 分支是过去的遗留问题。
- 不同分支之间的 pull request 和合并是有问题的。
- 人们倾向于包含特定于环境的代码并创建不同的配置。
- 一旦环境数量增多,环境的维护就会变得难以控制。
- 每个环境的分支模型违背了现有的 Kubernetes 生态系统。
在不同环境中采用分支应该只应用于遗留应用程序。
当问到为什么选择 Git 分支来建模不同的环境时,回答几乎总是“我们一直都是这样做的”,“感觉很自然”,“这是开发人员知道的”等等。
这没有错,大多数人都熟悉在不同环境中使用分支。这一实践是由古老的 Git-Flow 模型[3]大力推广的。但自从引入这种模式以来,情况发生了很大的变化,甚至最初的作者也从宏观角度发出了严重警告,建议人们不要在不了解后果的情况下采用这种模式。
事实上,Git-flow 模型……
- 专注于应用程序源代码,而不是环境配置(更不用说 Kubernetes manifest 了)。
- 如果需要在生产环境中支持多个应用版本,这一模型很合适,通常没有这种场景,但也时有发生。
因为本文是关于 GitOps 环境而不是应用程序源代码的,因此不打算在这里过多讨论 Git-flow 及其缺点,总而言之,如果需要为不同的环境支持不同的特性,那么应该遵循基于主干的开发[5]并使用特性标志[6]。
在 GitOps 上下文中,应用程序源代码和配置也应该在不同的 Git 存储库中(一个存储库只有应用程序代码,一个存储库有 Kubernetes manifests/templates)。这意味着应用程序源代码分支不应该影响环境存储库中的分支。
当我们在项目中采用 GitOps 时,应用程序开发人员可以为源代码选择想要的任何分支策略(甚至使用 Git-flow),但是环境配置 Git 存储库(包含所有 Kubernetes manifests/templates)不应该遵循每个环境一个分支的模型。
部署升级绝不是简单的 Git 合并
既然我们已经了解了在部署中使用按环境区分分支的方法的历史,就可以讨论其缺点了。
这种方法的主要优点是“部署升级是一个简单的 git 合并”。理论上,如果想要将一个版本从 QA 环境升级部署到预发环境,只需将 QA 分支合并到预发分支即可。当我们准备好生产环境时,再次将预发分支合并到生产分支,就可以确定来自预发的所有变更已经部署到了生产环境中。
想知道生产环境和预发环境之间有什么不同吗?只需要在两个分支之间做一个标准的 git diff[7]就可以了。想要将配置变更从预发环境反向移植到 QA 环境?从预发分支到 QA 分支的一个简单的 Git 合并就可以做到这一点。
如果想对部署升级施加额外的限制,可以使用 Pull Requests。一方面任何人都可以触发从 QA 到预发的合并,另一方面如果想在生产分支中合入一些东西,可以触发 Pull Request 并要求所有利益相关者手动批准。
这在理论上听起来很棒,一些琐碎的场景实际上可以像这样工作。但在实践中,情况并非如此。通过 Git 合并来升级一个版本可能会遇到合并冲突、引入不想要的变更,甚至触发错误的变更顺序。
下面我们以 Kubernetes 部署为例看一个简单的例子,当前部署位于预发分支中:
apiVersion: apps/v1 kind: Deployment metadata: name: example-deployment spec: replicas: 15 template: metadata: labels: app: my-app spec: containers: - name: backend image: my-app:2.2 ports: - containerPort: 80
QA 团队已经通知我们说版本 2.3(位于 QA 分支中)看起来已经准备好了,可以转移到交付阶段。我们将 QA 分支合并到预发分支,部署应用程序,并认为一切都很好。
但我们不知道,由于某些资源限制,有人将 QA 分支中的副本数量更改为 2。使用 Git 合并,不仅将 2.3 部署到了预发环境,而且还将副本改成了 2 个(而不是 15 个),这可能并不是我们想要的。
你可能会说,在合并之前查看副本个数很容易,但请记住,在实际场景中,有大量的应用程序,其中有大量的 manifests 被模板化(通过 Helm 或 Kustomize)。因此,理解想要带来什么变化,留下什么变化并不是一件小事情。
即使我们确实发现了不应该被合并的变更,也需要使用 git cherry-pick[8]或其他非标准方法手动选择“好的”部分,这与最初的“简单的”git 合并相去甚远。
但是,即使我们知道了所有可以合并的变更,也会出现合并的顺序与提交的顺序不同的情况。例如,QA 环境上有以下 4 个更改。
- 更新了应用 ingress[9]的主机名。
- 版本 2.5 被部署到 QA 环境,所有 QA 人员开始测试。
- 在 2.5 版本中发现了一个问题,并修复了 Kubernetes 的 configmap。
- 资源限制[10]进行了微调,并提交到 QA 分支。
然后我们决定 ingress 设置和资源限制应该部署到下一个环境(预发),但是 QA 团队还没有完成 2.5 版本的测试。
如果我们盲目的将 QA 分支合并到预发分支,就将同时合并所有 4 个变更,包括 2.5 的升级。
为了解决这个问题,需要再次使用 git cherry-pick 或其他手动方法。
在更复杂的情况下,提交之间存在依赖关系,因此即使是 cherry-pick 也帮不上忙。
在上面的示例中,版本 1.24 必须部署到生产环境。问题是其中一个提交(hotfix)包含了大量的变更,而其中某些变更又依赖于另一个提交(ingress 配置变更),而后者本身无法部署到生产环境(因为只适用于预发环境)。因此,即使是精心挑选,也不可能只将所需的变更从准备阶段引入到生产阶段。
最终的结果是,部署升级绝不是简单的 Git 合并。大多数组织还拥有大量应用,这些应用位于大量集群中,由大量 manifests 组成,手动选择变更将是一场失败的战斗。
特定于环境的变更更容易造成配置漂移
理论上,配置漂移不应该成为 Git 合并的问题。如果在预发环境中进行了变更,然后将该分支合并到生产环境,那么所有变更都应该迁移到新环境中。
然而在实践中,事情是不一样的,因为大多数组织只向一个方向合并,团队成员很容易改变上游环境,而从不将这些改变迁移到下游环境。
在 QA、预发和生产三个环境的经典例子中,Git 合并的方向只有一个。人们将 QA 分支合并到预发,将预发分支合并到生产,这意味着变化只会向上流动。
QA -> 预发(Staging) -> 生产(Production).
典型场景是,在生产环境中需要对配置进行快速变更(一个 hotfix),然后有人部署了该修复程序。在 Kubernetes 的情况下,这个修补程序可以是任何东西,比如对现有 manifest 的更改,甚至是一个全新的 manifest。
现在生产环境有了一个与预发完全不同的配置。下次一个版本从临时版本升级到生产版本时,Git 只会通知我们将从临时版本升级到生产版本。生产上的临时变更永远不会出现在 Pull Request 中的任何地方。
因为现在生产中有一个没有文档化的变更,这意味着所有后续部署都可能失败,而这个变更永远不会被任何后续升级检测到。
理论上,我们可以反向迁移这些变更,并周期性的将所有提交从生产阶段合并到交付阶段(以及交付阶段合并到 QA 阶段)。实际上,由于前面提到的原因,这种情况从未发生过。
可以想象,如果有很多环境,就会进一步放大这个问题。
总而言之,通过 Git 合并来部署发布版本并不能解决配置漂移问题,而且实际上团队会试图做出一些不按顺序合并的特殊变更,因此会使问题更加严重。
在大量环境中管理不同的 Git 分支是一场注定失败的战斗
在前面的所有示例中,我只使用了 3 个环境(QA 环境->预发环境->生产环境)来说明基于分支的环境部署的缺点。
根据组织的大小,也许有更多的环境,如果考虑地理位置等其他因素,那么环境的数量就会迅速增加。
我们以某个公司为例,它有 5 个工作环境:
- 负载测试
- 集成测试
- QA
- 预发
- 生产
我们假设最后 3 个环境也部署在欧洲、美国和亚洲,而前 2 个环境也有 GPU 和非 GPU 变体,这意味着该公司共有 13 个环境,而这只是针对单个应用的。
如果使用基于分支的方法:
- 在任何时候都需要有 13 个长期 Git 分支。
- 需要 13 个 pull requests 才能跨所有环境部署一个变更。
- 有一个二维的部署升级矩阵,纵向 5 步,横向 2-3 步。
- 错误合并、配置漂移和特别变更的可能性在所有环境组合中都有可能出现。
在这个示例组织的上下文中,所有以前的问题现在都更加普遍了。
branch-per-environment 模型与 Helm/Kustomize 背道而驰
描述应用程序的两个最流行的 Kubernetes 工具是 Helm 和 Kustomize,我们看看这两种工具如何对不同环境进行建模。
对于 Helm,需要创建一个通用 chart,该 chart 本身接受 values.yaml 形式的参数,如果希望拥有不同的环境,则需要多个 values 文件[11]。
对于 Kustomize,需要创建一个“base”配置,然后每个环境被建模为一个 overlay,有自己的文件夹:
在这两种情况下,不同的环境使用不同的文件夹/文件进行建模。Helm 和 Kustomize 对 Git 分支、Git merge 或 Pull Requests 一无所知,只使用普通文件。
再重复一遍:Helm 和 Kustomize 在不同的环境下使用普通文件,而不是 Git 分支。这是一个很好的提示,说明如何使用这两种工具建模不同的 Kubernetes 配置。
如果引入 Git 分支,不仅会引入额外的复杂性,还会违背自己的工具。
在 GitOps 环境中部署发布的推荐方法
建模不同的 Kubernetes 环境,并在环境之间部署发布,对于所有采用 GitOps 的团队来说都是非常普遍的问题。尽管非常流行的方法是在每个环境中使用 Git 分支,并假设每次部署都是一个“简单的”Git 合并,但在本文中已经看到,这是一个反模式。
下面我们将介绍一种更好的方法来为不同的环境建模,从而在不同的 Kubernetes 集群上部署发布,之前的介绍(关于 Helm/Kustomize)应该已经给了你一点关于这种方案的提示。
下面我会解释如何在同一个 Git 分支上使用不同的文件夹对 GitOps 环境进行建模,以及如何通过简单的文件复制操作来处理环境升级(简单的和复杂的)。
GitOps 环境部署升级
首先了解应用程序
在创建文件夹结构之前,需要先做一些研究,了解应用程序的“设置”。尽管一些人以通用的方式讨论应用程序配置,但实际上并不是所有的配置设置都同样重要。
在 Kubernetes 应用的上下文中,我们有以下几类“环境配置”:
- 容器 tag 形式的应用版本。这可能是 Kubernetes manifest 中最重要的设置(就环境升级而言)。根据不同的用例,只需更改容器镜像版本即可。不过有可能源代码中的新变更也需要更改部署环境。
- 应用相关的 Kubernetes 特定配置。包括应用的副本和其他 Kubernetes 相关信息,如资源限制、运行状况检查、持久卷、亲和性规则等。
- 基本静态业务配置。这是一组与 Kubernetes 无关的设置,但与应用业务有关。可能是外部 url、内部队列大小、UI 默认值、身份验证配置文件等。所谓“基本静态”,我指的是为每个环境定义一次的设置,然后永远不会更改。例如,我们总是希望生产环境使用 production.paypal.com,而非生产环境使用 staging.paypal.com。在不同的环境中,这是一个我们永远不希望迁移的设置。
- 非静态业务配置。和上一点一样,但包含了希望在不同环境之间迁移的设置,可以是全球 VAT 设置、推荐引擎参数、可用的比特率编码,以及任何其他特定于业务的配置。
必须了解所有不同的设置是什么,更重要的是,哪些属于第 4 类,因为这些是我们希望随应用程序版本一起推广的设置。
这样就可以覆盖所有可能的部署场景:
- 应用在 QA 中从版本 1.34 升级到 1.35,这是一个简单的源代码变更,因此只需要在 QA 环境中更改容器镜像属性。
- 应用在预发环境中从版本 3.23 升级到 3.24,这不是一个简单的源代码变更,不但需要更新容器镜像属性,而且从 QA 环境带来了新的设置“recommender.batch_size”。
我看到很多团队不理解不同配置参数之间的区别,而只使用一个配置文件(或机制)来设置不同域的值(即运行时和应用业务配置)。
有了配置列表以及所属区域之后,就可以创建环境结构并优化需要经常变更并且需要在不同环境之间迁移的文件复制操作。