大型组织应用 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”。
我看到很多团队不理解不同配置参数之间的区别,而只使用一个配置文件(或机制)来设置不同域的值(即运行时和应用业务配置)。
有了配置列表以及所属区域之后,就可以创建环境结构并优化需要经常变更并且需要在不同环境之间迁移的文件复制操作。
5 个 GitOps 环境及其变更示例
我们来看一个实际的例子。
我们将对之前提到的环境进行建模,该公司有 5 个不同的环境:
- 负载测试
- 集成测试
- QA
- 预发
- 生产
我们假设最后两个环境也部署在欧洲、美国和亚洲,而前两个环境也有 GPU 和非 GPU 变体,这意味着该公司共有 11 个环境。
可以在 https://github.com/kostis-codefresh/gitops-environment-promotion 找到建议的文件夹结构,所有环境都是同一分支中的不同文件夹,对于不同的环境没有分支。如果想知道在一个环境中部署了什么,只需查看 repo 的主分支中的 envs/。
在解释结构之前,有一些免责声明:
免责声明 1: 写这篇文章花了我很长时间,因为不确定应该讨论 Kustomize[12]、Helm[13]还是普通的 manifests。我选择了 Kustomize,因为它更简单(在文章的最后我也提到了 Helm)。但是请注意,示例 repo 中的 Kustomize 模板只是为了演示目的。本文不是 Kustomize 教程。在实际应用中,你可能有 Configmap 生成器[14]、定制补丁[15],并采用和这里展示的完全不同的“组件”结构。如果你不熟悉 Kustomize,请先花些时间理解它的功能,然后再回来。
免责声明 2: 我用于部署的应用[16]完全只是为了演示,它的配置由于简洁和简单的原因而忽略了几个最佳实践。例如,某些部署缺少运行状况检查[17],所有部署都缺少资源限制[18]。同样,本文不会讨论如何创建 Kubernetes 部署,你应该已经知道正确的部署 manifests 是什么样子的。如果想了解更多关于生产级最佳实践的信息,请参阅另一篇文章 https://codefresh.io/kubernetes-tutorial/kubernetes-antipatterns-1/
抛开免责声明不说,下面是存储库的结构:
GitOps 目录结构
base 目录保存对所有环境通用的配置,不会经常改变。如果同时对多个环境进行更改,最好使用“variants”文件夹。
variants 文件夹(或者叫 mixins、components)保存不同环境之间的共同特征。在研究上一节讨论的应用程序之后,可以自行定义你认为的环境之间的“共同之处”。
在示例应用中,我们为所有 prod 和非 prod 环境以及地区提供了 variants。下面是一个适用于所有生产环境的 prod variant[19]示例。
--- apiVersion: apps/v1 kind: Deployment metadata: name: simple-deployment spec: template: spec: containers: - name: webserver-simple env: - name: ENV_TYPE value: "production" - name: PAYPAL_URL value: "production.paypal.com" - name: DB_USER value: "prod_username" - name: DB_PASSWORD value: "prod_password" livenessProbe: httpGet: path: /health port: 8080
在上面的示例中,我们确保所有生产环境都使用了生产 DB 凭证、生产支付网关和活动探针(这是一个精心设计的示例,请参阅本节开头的免责声明 2)。这些设置属于我们不希望在不同环境之间迁移的配置集,我们假设在整个应用生命周期中这些都是静态的。
准备好 base 和 variants 之后,可以用这些属性的组合来定义每个最终环境。
下面是一个 ASIA 环境的示例[20]:
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: staging namePrefix: staging-asia- resources: - ../../base components: - ../../variants/non-prod - ../../variants/asia patchesStrategicMerge: - deployment.yml - version.yml - replicas.yml - settings.yml
首先定义一些公共属性,从 base 环境、非 prod 环境和 asia 的所有环境中继承所有配置。
这里的关键点是我们应用的补丁。version.yml[21]和 replicas.yml[22]是自解释的,只定义自己的镜像和副本,其他什么都没有。
version.yml 文件(这是环境间最重要的东西)只定义了应用的镜像,其他什么都没有。
--- apiVersion: apps/v1 kind: Deployment metadata: name: simple-deployment spec: template: spec: containers: - name: webserver-simple image: docker.io/kostiscodefresh/simple-env-app:2.0
我们希望在不同环境之间部署的每个版本的相关设置也在 settings.yml[23]中定义。
--- apiVersion: apps/v1 kind: Deployment metadata: name: simple-deployment spec: template: spec: containers: - name: webserver-simple env: - name: UI_THEME value: "dark" - name: CACHE_SIZE value: "1024kb" - name: PAGE_LIMIT value: "25" - name: SORTING value: "ascending" - name: N_BUCKETS value: "42"
请随意查看整个存储库[16],以理解所有 kustomizations 的构造方式。
通过 GitOps 执行初始部署
要将应用程序部署到相关的环境中,只需将 GitOps 控制器指向相应的“env”文件夹,kustomize 将创建完整的 settings 和 values 层次结构。
下面是在 Staging/Asia 中运行的示例应用程序[16]。
GitOps 应用示例
可以在命令行上使用 Kustomize 预览将为每个环境部署的内容,例如:
kustomize build envs/staging-asia kustomize build envs/qa kustomize build envs/integration-gpu
当然,也可以将上述命令的输出通过管道输出到 kubectl 来部署每个环境,但在 GitOps 的上下文中,应该始终让 GitOps 控制器部署环境,避免手动 kubectl 操作。
比较两个环境的配置
对于软件团队来说,一个非常普遍的需求是理解两个环境之间的不同之处。我看到一些团队有这样的误解,他们认为只有使用分支才能很容易的发现不同环境之间的差异。
这与事实相去甚远。通过比较文件和文件夹,可以很容易的使用成熟的文件 diff 工具来查找环境之间的不同之处。
最简单的方法是只区分对应用程序至关重要的设置。
vimdiff envs/integration-gpu/settings.yml envs/integration-non-gpu/settings.yml
GitOps settings diff
在 kustomize 的帮助下,还可以比较任意数量的环境,获取整体的概念:
kustomize build envs/qa/> /tmp/qa.yml kustomize build envs/staging-us/ > /tmp/staging-us.yml kustomize build envs/prod-us/ > /tmp/prod-us.yml vimdiff /tmp/staging-us.yml /tmp/qa.yml /tmp/prod-us.yml
GitOps 环境 diff
个人认为这种方式和在环境分支之间执行“git diff”没有什么差别。
如何在 GitOps 环境中进行部署升级
现在文件结构已经很清楚了,终于可以回答这个古老的问题:“我如何用 GitOps 部署发布”?
让我们看看下面一些部署场景。如果你有关注文件结构,应该已经了解到所有部署升级都可以解析为简单的文件复制操作。
场景: 在美国将应用版本从 QA 升级到预发环境:
- cp envs/qa/version.yml envs/staging-us/version.yml
- commit/push 变更
场景: 从 GPU 集成测试到 GPU 负载测试,再到 QA 的应用版本升级。这是一个两步的过程:
- cp envs/integration-gpu/version.yml envs/load-gpu/version.yml
- commit/push 变更
- cp envs/load-gpu/version.yml envs/qa/version.yml
- commit/push 变更
场景: 通过额外配置,将应用程序从 prod-eu 升级到 prod-us。这里我们还复制了 settings 文件。
- cp envs/prod-eu/version.yml envs/prod-us/version.yml
- cp envs/prod-eu/settings.yml envs/prod-us/settings.yml
- commit/push 变更
场景: 确保 QA 拥有与 staging-asia 相同的副本数量
- cp envs/staging-asia/replicas.yml envs/qa/replicas.yml
- commit/push 变更
场景: 从 QA 到移植所有配置到集成测试(非 gpu 版本)
- cp envs/qa/settings.yml envs/integration-non-gpu/settings.yml
- commit/push 变更
场景: 一次性对所有非 prod 环境进行全局更改(但请参阅下一节,以了解关于此操作的一些讨论)
- 在 variants/non-prod/non-prod.yml 中做出变更
- commit/push 变更
场景: 向所有美国环境(包括生产环境和预发环境)添加新的配置文件。
- 在 variants/us 文件夹中添加新的 manifest
- 修改 variants/us/kustomization.yml 引入新的 manifest
- commit/push 变更
一般来说,所有的部署升级只是复制操作。与 branch-per-environment 方法不同,现在可以自由的将任何东西从任何环境推广到其他环境,不必担心进行错误的变更。特别是当涉及到反向移植配置时,environment-per-folder 确实很出色,因为可以简单地“向上”或“向后”移动配置,甚至可以在不相关的环境之间移动配置。
注意,我使用 cp 操作只是为了演示。在实际的应用程序中,此操作将由 CI 系统或其他编排工具自动执行。根据环境的不同,你可能想先创建一个 Pull Request,而不是直接在主分支中编辑文件夹。
一次对多个环境进行更改
首先,我们需要定义“多重”环境的确切含义,假设以下两种情况。
- 同时更改同一“级别”上的多个环境。例如,想要同时变更“prod-us”、“prod-eu”和“prod-asia”。
- 同时更改不在同一级别上的多个环境。例如,想同时更改“integration”和“staging-eu”。
第一种情况是有效场景,我们将在下面讨论。但是,我认为第二个场景是反模式,拥有不同环境的关键在于能够以一种渐进的方式发布内容,并推动从一个环境到下一个环境的变化。因此,如果你发现自己在不同的环境中部署了相同的变化,问问自己是否真的需要这样做以及为什么。
对于部署单个更改到多个“类似”环境的有效场景,有两种策略:
- 如果你确定更改是绝对“安全的”,并且希望立即应用到所有环境,那么可以在适当的 variant(或各自的文件夹)中进行更改。例如,如果你在 variants/non-prod 文件夹中提交/推送一个更改,那么所有非生产环境都会同时应用这个更改。我个人反对这种方法,因为有些更改在理论上看起来是“安全的”,但在实践中可能会有问题。
- 更可取的方法是将更改应用于每个单独的文件夹,然后将其移动到“父”variant(当它在所有环境中都存在时)。
让我们举个例子。我们想做一个影响所有 EU 环境的改变(例如 GDPR 功能[24])。简单的方法是将配置更改直接提交/推送到 variants/eu 文件夹。这确实会影响到所有的 EU 环境(prod-eu 和 staging-eu)。但是,这有一点风险,因为如果部署失败,就会导致生产环境崩溃。
建议采用如下方法:
- 首先在 envs/staging-eu 中做出变更
- 然后对 envs/prod-eu 做同样的修改
- 最后,从两个环境中删除更改,并将其添加到 variants/eu 中(通过一个 commit/push 操作)。
GitOps 渐进升级
你可以从渐进的数据库重构[25]中认识到这种模式。最后的提交是“过渡性的”,不会以任何方式影响任何环境。Kustomize 将在这两种情况下创建完全相同的定义,GitOps 控制器应该不会发现任何差异。
这种方法的优点是,当我们在环境中移动更改时,可以轻松回滚/恢复更改。缺点是需要增加工作(和提交)将更改推广到所有环境,但是我相信这些工作的好处大于风险。
如果采用这种方法,意味着永远不会直接对 base 文件夹应用新的更改。如果希望对所有环境进行更改,则首先将更改应用于单个环境和/或 variants,然后将其反向移植到 base 文件夹,同时将其从所有下游文件夹中删除。
“environment-per-folder”方法的优点
既然我们已经分析了“environment-per-folder”方法的所有内部工作原理,现在就该解释为什么它比“branch-per-environment”方法更好了。如果你已经看过前面的部分,那么应该已经理解了“environment-per-folder”方法是如何避免之前分析的所有问题的。
环境分支最突出的问题是提交的顺序,以及从一个环境合并到另一个环境时带来不必要更改的风险。使用文件夹方法,这个问题就完全消除了:
- 提交顺序现在已经无关紧要了。当你将一个文件从一个文件夹复制到下一个文件夹时,不需要关心它的提交历史,只需要关心它的内容。
- 通过只复制周围的文件,只拿需要的东西,而不拿其他东西。当你复制 envs/qa/version.yml 到 env/staging-asia/version.yml 中,可以确定只升级了容器镜像,没有其他东西。如果其他人在 QA 环境中改变了副本,并不会影响升级流程。
- 不需要使用 git cherry-picks 或任何其他高级的 git 方法来升级版本,只需要复制文件,并且可以访问用于文件处理的实用程序的成熟生态系统。
- 可以自由的从任何环境对上游或下游环境进行任何更改,而不受环境正确“顺序”的任何限制。例如,如果想将设置从 prod-us 反向移植到 staging-us,可以简单的将 env/prod-us/settings.yml 拷贝到 env/staging-us/settings.yml,而不用担心可能会无意中部署了不相关的只应在生产环境中应用的修补程序。
- 可以容易的使用文件 diff 操作来了解各个环境之间的不同之处(源环境和目标环境,反之亦然)
我认为这些优势对于任何重要的应用程序都是非常重要的,我敢打赌大型组织中总会有几个“失败的部署”可以直接或间接归因于有问题的 environment-per-branch 模型。
之前我们提到的第二个问题是,将一个分支合并到下一个环境时,会出现配置漂移。这样做的原因是,当你执行“git merge”时,git 只会通知你它将带来的更改,而不会告诉你目标分支中已经发生了什么更改。
同样,文件夹方案完全消除了这个问题。正如前面说的,文件 diff 操作没有“方向”的概念,可以从任何环境向上或向下复制任何设置,如果对文件执行 diff 操作,可以看到环境之间的所有更改,而不管它们的上游/下游位置如何。
关于环境分支的最后一点是随着环境数量的增长,分支复杂性将会线性增加。对于 5 个环境,需要在 5 个分支之间切换更改,而对于 20 个环境,需要处理 20 个分支。在大量的分支之间正确迁移发布版本是一个繁琐的过程,在生产环境中,这是一场灾难。
使用文件夹方法,分支的数量不仅是静态的,而且只有一个。如果有 5 个环境,可以用“主”分支来管理,如果需要更多的环境,你只需要添加额外的文件夹。如果 20 个环境,仍然只需要一个 Git 分支。当只有一个分支时,获得部署的集中视图是很简单的。
在 GitOps 环境中使用 Helm
如果你不使用 Kustomize 而是更喜欢 Helm,也可以创建一个文件夹层次结构,其中包含所有环境的“通用”设置,特定的特性/mixins/组件,以及特定于每个环境的最终文件夹。
下面是文件夹结构的样子:
chart/ [...chart files here..] common/ values-common.yml variants/ prod/ values-prod.yml non-prod/ Values-non-prod.yml [...other variants…] envs/ prod-eu/ values-env-default.yaml values-replicas.yaml values-version.yaml values-settings.yaml [..other environments…]
同样,你需要花一些时间来检查应用属性,并决定如何将它们分割成不同的 values 文件,以获得最佳的升级速度。
除此之外,在环境升级方面,大多数过程都是一样的。
场景: 在 US 将应用版本从 QA 提升到预发环境:
- cp envs/qa/values-version.yml envs/staging-us/values-version.yml
- commit/push 变更
场景: 从 GPU 集成测试到 GPU 负载测试,再到 QA 的应用版本升级。这是一个两步的过程:
- cp envs/integration-gpu/values-version.yml envs/load-gpu/values-version.yml
- commit/push 变更
- cp envs/load-gpu/values-version.yml envs/qa/values-version.yml
- commit/push 变更
场景: 通过额外配置,将应用从 prod-eu 提升到 prod-us。这里我们还复制了 settings 文件。
- cp envs/prod-eu/values-version.yml envs/prod-us/values-version.yml
- cp envs/prod-eu/values-settings.yml envs/prod-us/values-settings.yml
- commit/push 变更
理解 Helm(或者你的 GitOps 代理处理 Helm)如何处理多个 values 文件以及它们相互覆盖的顺序也是非常重要的。
如果希望预览某个环境,可以使用以下命令,而不是“kustomize build”:
helm template chart/ --values common/values-common.yaml --values variants/prod/values-prod.yaml –values envs/prod-eu/values-env-default.yml –values envs/prod-eu/values-replicas.yml –values envs/prod-eu/values-version.yml –values envs/prod-eu/values-settings.yml
可以看到,如果在每个环境文件夹中都有大量的 variants 或文件,那么 Helm 比 Kustomize 更麻烦一些。
environment-per-git-repo 方法
当我与大型组织讨论文件夹方法时,听到的第一个反对意见是,人们(尤其是安全团队)不喜欢看到单个 Git 存储库中的单个分支同时包含产品化和非产品化环境。
这是一个可以理解的反对意见,可以说是文件夹方法相对于“environment-per-branch”范式的唯一弱点。毕竟,在 Git 存储库中保护各个分支比在单个分支中保护文件夹要容易得多。
这个问题可以很容易的通过自动化、验证检查甚至手工批准(如果这对你的组织至关重要的话)来解决。我想再次强调,在文件操作中使用“cp”来升级发布版本,只是为了演示的目的,并不意味着当升级发生时,需要在交互式终端中手动运行 cp。
理想情况下,应该有一个自动化系统来复制文件并 commit/push 它们,可以是持续集成(CI)系统或处理软件生命周期的其他平台。如果仍然有人自己做出改变,不应该直接 commit “main”目录,而是应该发起一个 Pull Request,然后通过适当的流程,在合并之前检查 Pull Request。
然而,我意识到有些组织对安全问题特别敏感,当涉及到 Git 保护时,他们更喜欢完全隔离的方法。对于这些组织,可以使用 2 个 Git 存储库,一个保存 base 配置、所有生产 variants 和所有生产环境(以及所有与生产相关的东西),而第二个 Git 存储库保存所有非生产的东西。
这种方法让升级变得有点困难,因为现在需要在做任何升级之前签出 2 个 git 仓库。另一方面,它允许安全团队向“生产”Git 存储库放置额外的安全约束,并且无论部署到多少环境中,仍然拥有静态数量的 Git 存储库(只有 2 个)。
个人认为这种方法有些过分,至少在我看来,它显示出开发和运维缺乏信任。关于人们是否应该直接访问生产环境的讨论是一个复杂的问题,可能需要单独讨论。
拥抱文件夹,忘记分支
希望通过这篇文章,可以解决在多环境中部署的问题,现在你已经很好的理解了文件夹方法的好处以及应该使用它的原因。
GitOps 部署快乐!
References:
[1] Stop Using Branches for Deploying to Different GitOps Environments: https://medium.com/containers-101/stop-using-branches-for-deploying-to-different-gitops-environments-7111d0632402
[2] How to Model Your Gitops Environments and Promote Releases between Them: https://codefresh.io/about-gitops/how-to-model-your-gitops-environments-and-promote-releases-between-them/
[3] Multiple environments(dev, stage, ..,. prod ) example: https://github.com/argoproj/argocd-example-apps/issues/57
[4] A successful git branching model: https://nvie.com/posts/a-successful-git-branching-model/
[5] Trunk based development: https://trunkbaseddevelopment.com/
[6] Feature flags: https://trunkbaseddevelopment.com/feature-flags/
[7] git diff: https://git-scm.com/docs/git-diff
[8] git cherry-pick: https://git-scm.com/docs/git-cherry-pick
[9] Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/
[10] Manage resources containers: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
[11] Helm deployment evnironments: https://codefresh.io/helm-tutorial/helm-deployment-environments/
[12] Kustomize: https://kustomize.io/
[13] Helm: https://helm.sh/
[14] Configmap generator: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/configmapgenerator/
[15] Patches strategic merge: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patchesstrategicmerge/
[16] GitOps promotion source code: https://github.com/kostis-codefresh/gitops-promotion-source-code
[17] Configure liveness readiness startup probes: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
[18] Manage resources containers: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
[19] Sample prod: https://raw.githubusercontent.com/kostis-codefresh/gitops-environment-promotion/main/variants/prod/prod.yml
[20] Sample staging-asia: https://github.com/kostis-codefresh/gitops-environment-promotion/tree/main/envs/staging-asia
[21] Sample version.yml: https://github.com/kostis-codefresh/gitops-environment-promotion/blob/main/envs/staging-asia/version.yml
[22] Sample replicas.yml: https://github.com/kostis-codefresh/gitops-environment-promotion/blob/main/envs/staging-asia/replicas.yml
[23] Sample settings.yml: https://github.com/kostis-codefresh/gitops-environment-promotion/blob/main/envs/staging-asia/settings.yml
[24] GDPR: https://gdpr-info.eu/
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/303634.html