GitOps多环境部署问题及解决方案

593 阅读31分钟

大型组织应用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分支来建模不同的环境是一种反模式,不要这样做!

我们将从以下几点探讨为什么这个实践是反模式:

  1. 在部署环境中使用不同的Git分支是过去的遗留问题。
  2. 不同分支之间的pull request和合并是有问题的。
  3. 人们倾向于包含特定于环境的代码并创建不同的配置。
  4. 一旦环境数量增多,环境的维护就会变得难以控制。
  5. 每个环境的分支模型违背了现有的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个更改。

  1. 更新了应用ingress[9]的主机名。
  2. 版本2.5被部署到QA环境,所有QA人员开始测试。
  3. 在2.5版本中发现了一个问题,并修复了Kubernetes的configmap。
  4. 资源限制[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个工作环境:

  1. 负载测试
  2. 集成测试
  3. QA
  4. 预发
  5. 生产

我们假设最后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应用的上下文中,我们有以下几类“环境配置”:

  1. 容器tag形式的应用版本。这可能是Kubernetes manifest中最重要的设置(就环境升级而言)。根据不同的用例,只需更改容器镜像版本即可。不过有可能源代码中的新变更也需要更改部署环境。
  2. 应用相关的Kubernetes特定配置。包括应用的副本和其他Kubernetes相关信息,如资源限制、运行状况检查、持久卷、亲和性规则等。
  3. 基本静态业务配置。这是一组与Kubernetes无关的设置,但与应用业务有关。可能是外部url、内部队列大小、UI默认值、身份验证配置文件等。所谓“基本静态”,我指的是为每个环境定义一次的设置,然后永远不会更改。例如,我们总是希望生产环境使用production.paypal.com,而非生产环境使用staging.paypal.com。在不同的环境中,这是一个我们永远不希望迁移的设置。
  4. 非静态业务配置。和上一点一样,但包含了希望在不同环境之间迁移的设置,可以是全球VAT设置、推荐引擎参数、可用的比特率编码,以及任何其他特定于业务的配置。

必须了解所有不同的设置是什么,更重要的是,哪些属于第4类,因为这些是我们希望随应用程序版本一起推广的设置。

这样就可以覆盖所有可能的部署场景:

  1. 应用在QA中从版本1.34升级到1.35,这是一个简单的源代码变更,因此只需要在QA环境中更改容器镜像属性。
  2. 应用在预发环境中从版本3.23升级到3.24,这不是一个简单的源代码变更,不但需要更新容器镜像属性,而且从QA环境带来了新的设置“recommender.batch_size”。

我看到很多团队不理解不同配置参数之间的区别,而只使用一个配置文件(或机制)来设置不同域的值(即运行时和应用业务配置)。

有了配置列表以及所属区域之后,就可以创建环境结构并优化需要经常变更并且需要在不同环境之间迁移的文件复制操作。

5个GitOps环境及其变更示例

我们来看一个实际的例子。

我们将对之前提到的环境进行建模,该公司有5个不同的环境:

  1. 负载测试
  2. 集成测试
  3. QA
  4. 预发
  5. 生产

我们假设最后两个环境也部署在欧洲、美国和亚洲,而前两个环境也有GPU和非GPU变体,这意味着该公司共有11个环境。

可以在 github.com/kostis-code… 找到建议的文件夹结构,所有环境都是同一分支中的不同文件夹,对于不同的环境没有分支。如果想知道在一个环境中部署了什么,只需查看repo的主分支中的envs/。

在解释结构之前,有一些免责声明:

免责声明1: 写这篇文章花了我很长时间,因为不确定应该讨论Kustomize[12]、Helm[13]还是普通的manifests。我选择了Kustomize,因为它更简单(在文章的最后我也提到了Helm)。但是请注意,示例repo中的Kustomize模板只是为了演示目的。本文不是Kustomize教程。在实际应用中,你可能有Configmap生成器[14]、定制补丁[15],并采用和这里展示的完全不同的“组件”结构。如果你不熟悉Kustomize,请先花些时间理解它的功能,然后再回来。

免责声明2: 我用于部署的应用[16]完全只是为了演示,它的配置由于简洁和简单的原因而忽略了几个最佳实践。例如,某些部署缺少运行状况检查[17],所有部署都缺少资源限制[18]。同样,本文不会讨论如何创建Kubernetes部署,你应该已经知道正确的部署manifests是什么样子的。如果想了解更多关于生产级最佳实践的信息,请参阅另一篇文章 codefresh.io/kubernetes-…

抛开免责声明不说,下面是存储库的结构:

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升级到预发环境:

  1. cp envs/qa/version.yml envs/staging-us/version.yml
  2. commit/push变更

场景: 从GPU集成测试到GPU负载测试,再到QA的应用版本升级。这是一个两步的过程:

  1. cp envs/integration-gpu/version.yml envs/load-gpu/version.yml
  2. commit/push变更
  3. cp envs/load-gpu/version.yml envs/qa/version.yml
  4. commit/push变更

场景: 通过额外配置,将应用程序从prod-eu升级到prod-us。这里我们还复制了settings文件。

  1. cp envs/prod-eu/version.yml envs/prod-us/version.yml
  2. cp envs/prod-eu/settings.yml envs/prod-us/settings.yml
  3. commit/push变更

场景: 确保QA拥有与staging-asia相同的副本数量

  1. cp envs/staging-asia/replicas.yml envs/qa/replicas.yml
  2. commit/push变更

场景: 从QA到移植所有配置到集成测试(非gpu版本)

  1. cp envs/qa/settings.yml envs/integration-non-gpu/settings.yml
  2. commit/push变更

场景: 一次性对所有非prod环境进行全局更改(但请参阅下一节,以了解关于此操作的一些讨论)

  1. 在variants/non-prod/non-prod.yml中做出变更
  2. commit/push变更

场景: 向所有美国环境(包括生产环境和预发环境)添加新的配置文件。

  1. 在variants/us文件夹中添加新的manifest
  2. 修改variants/us/kustomization.yml引入新的manifest
  3. commit/push变更

一般来说,所有的部署升级只是复制操作。与branch-per-environment方法不同,现在可以自由的将任何东西从任何环境推广到其他环境,不必担心进行错误的变更。特别是当涉及到反向移植配置时,environment-per-folder确实很出色,因为可以简单地“向上”或“向后”移动配置,甚至可以在不相关的环境之间移动配置。

注意,我使用cp操作只是为了演示。在实际的应用程序中,此操作将由CI系统或其他编排工具自动执行。根据环境的不同,你可能想先创建一个Pull Request,而不是直接在主分支中编辑文件夹。

一次对多个环境进行更改

首先,我们需要定义“多重”环境的确切含义,假设以下两种情况。

  1. 同时更改同一“级别”上的多个环境。例如,想要同时变更“prod-us”、“prod-eu”和“prod-asia”。
  2. 同时更改不在同一级别上的多个环境。例如,想同时更改“integration”和“staging-eu”。

第一种情况是有效场景,我们将在下面讨论。但是,我认为第二个场景是反模式,拥有不同环境的关键在于能够以一种渐进的方式发布内容,并推动从一个环境到下一个环境的变化。因此,如果你发现自己在不同的环境中部署了相同的变化,问问自己是否真的需要这样做以及为什么。

对于部署单个更改到多个“类似”环境的有效场景,有两种策略:

  1. 如果你确定更改是绝对“安全的”,并且希望立即应用到所有环境,那么可以在适当的variant(或各自的文件夹)中进行更改。例如,如果你在variants/non-prod文件夹中提交/推送一个更改,那么所有非生产环境都会同时应用这个更改。我个人反对这种方法,因为有些更改在理论上看起来是“安全的”,但在实践中可能会有问题。
  2. 更可取的方法是将更改应用于每个单独的文件夹,然后将其移动到“父”variant(当它在所有环境中都存在时)。

让我们举个例子。我们想做一个影响所有EU环境的改变(例如GDPR功能[24])。简单的方法是将配置更改直接提交/推送到variants/eu文件夹。这确实会影响到所有的EU环境(prod-eu和staging-eu)。但是,这有一点风险,因为如果部署失败,就会导致生产环境崩溃。

建议采用如下方法:

  1. 首先在envs/staging-eu中做出变更
  2. 然后对envs/prod-eu做同样的修改
  3. 最后,从两个环境中删除更改,并将其添加到variants/eu中(通过一个commit/push操作)。

GitOps渐进升级

你可以从渐进的数据库重构[25]中认识到这种模式。最后的提交是“过渡性的”,不会以任何方式影响任何环境。Kustomize将在这两种情况下创建完全相同的定义,GitOps控制器应该不会发现任何差异。

这种方法的优点是,当我们在环境中移动更改时,可以轻松回滚/恢复更改。缺点是需要增加工作(和提交)将更改推广到所有环境,但是我相信这些工作的好处大于风险。

如果采用这种方法,意味着永远不会直接对base文件夹应用新的更改。如果希望对所有环境进行更改,则首先将更改应用于单个环境和/或variants,然后将其反向移植到base文件夹,同时将其从所有下游文件夹中删除。

“environment-per-folder”方法的优点

既然我们已经分析了“environment-per-folder”方法的所有内部工作原理,现在就该解释为什么它比“branch-per-environment”方法更好了。如果你已经看过前面的部分,那么应该已经理解了“environment-per-folder”方法是如何避免之前分析的所有问题的。

环境分支最突出的问题是提交的顺序,以及从一个环境合并到另一个环境时带来不必要更改的风险。使用文件夹方法,这个问题就完全消除了:

  1. 提交顺序现在已经无关紧要了。当你将一个文件从一个文件夹复制到下一个文件夹时,不需要关心它的提交历史,只需要关心它的内容。
  2. 通过只复制周围的文件,只拿需要的东西,而不拿其他东西。当你复制envs/qa/version.yml到env/staging-asia/version.yml中,可以确定只升级了容器镜像,没有其他东西。如果其他人在QA环境中改变了副本,并不会影响升级流程。
  3. 不需要使用git cherry-picks或任何其他高级的git方法来升级版本,只需要复制文件,并且可以访问用于文件处理的实用程序的成熟生态系统。
  4. 可以自由的从任何环境对上游或下游环境进行任何更改,而不受环境正确“顺序”的任何限制。例如,如果想将设置从prod-us反向移植到staging-us,可以简单的将env/prod-us/settings.yml拷贝到env/staging-us/settings.yml,而不用担心可能会无意中部署了不相关的只应在生产环境中应用的修补程序。
  5. 可以容易的使用文件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提升到预发环境:

  1. cp envs/qa/values-version.yml envs/staging-us/values-version.yml
  2. commit/push变更

场景: 从GPU集成测试到GPU负载测试,再到QA的应用版本升级。这是一个两步的过程:

  1. cp envs/integration-gpu/values-version.yml envs/load-gpu/values-version.yml
  2. commit/push变更
  3. cp envs/load-gpu/values-version.yml envs/qa/values-version.yml
  4. commit/push变更

场景: 通过额外配置,将应用从prod-eu提升到prod-us。这里我们还复制了settings文件。

  1. cp envs/prod-eu/values-version.yml envs/prod-us/values-version.yml
  2. cp envs/prod-eu/values-settings.yml envs/prod-us/values-settings.yml
  3. 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: medium.com/containers-…
[2] How to Model Your Gitops Environments and Promote Releases between Them: codefresh.io/about-gitop…
[3] Multiple environments(dev, stage, ..,. prod ) example: github.com/argoproj/ar…
[4] A successful git branching model: nvie.com/posts/a-suc…
[5] Trunk based development: trunkbaseddevelopment.com/
[6] Feature flags: trunkbaseddevelopment.com/feature-fla…
[7] git diff: git-scm.com/docs/git-di…
[8] git cherry-pick: git-scm.com/docs/git-ch…
[9] Ingress: kubernetes.io/docs/concep…
[10] Manage resources containers: kubernetes.io/docs/concep…
[11] Helm deployment evnironments: codefresh.io/helm-tutori…
[12] Kustomize: kustomize.io/
[13] Helm: helm.sh/
[14] Configmap generator: kubectl.docs.kubernetes.io/references/…
[15] Patches strategic merge: kubectl.docs.kubernetes.io/references/…
[16] GitOps promotion source code: github.com/kostis-code…
[17] Configure liveness readiness startup probes: kubernetes.io/docs/tasks/…
[18] Manage resources containers: kubernetes.io/docs/concep…
[19] Sample prod: raw.githubusercontent.com/kostis-code…
[20] Sample staging-asia: github.com/kostis-code…
[21] Sample version.yml: github.com/kostis-code…
[22] Sample replicas.yml: github.com/kostis-code…
[23] Sample settings.yml: github.com/kostis-code…
[24] GDPR: gdpr-info.eu/
[25] Database refactoring: databaserefactoring.com/

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind