GitOps and Kubernetes Continuous Deployment with Argo CD, Jenkins X, and Flux

1、环境管理简介

在软件部署中,环境是部署和执行代码的地方。不同的环境在软件开发的生命周期中有不同的目的。例如,工程师可以在本地开发环境中创建、测试和调试新的代码版本。工程师完成代码开发后,下一步是将更改提交到 Git 并开始部署到不同的环境以进行集成测试和最终的生产发布。此过程称为持续集成/持续交付 (CI/CD),通常由以下环境组成:QA、E2E、Stage 和 Prod。

QA 环境将针对硬件、数据和其他类似生产的依赖项对新代码进行测试,以确保服务的正确性。如果 QA 中的所有测试都通过,新代码将被提升到 E2E 环境,作为其他预发布服务测试/集成的稳定环境。 QA 和 E2E 环境也称为预生产 (preprod) 环境,因为它们不托管生产流量或使用生产数据。

当新版本的代码准备好进行生产发布时,该代码通常会首先在 Stage 环境(可以访问实际的生产依赖项)中部署,以确保在代码在 Prod 环境中上线之前所有生产依赖项都已就位。例如,新代码可能需要新的数据库模式更新,并且阶段环境可用于验证新模式是否就位。配置仅将测试流量定向到 Stage 环境,以便新代码引入的任何问题都不会影响实际客户。但是,Stage 环境通常配置为使用“真实”生产数据库操作。必须仔细审查在 Stage 环境中执行的测试,以确保它们在生产中安全执行。一旦所有测试在 Stage 中通过,新代码最终将部署在 Prod 中以实现实时生产流量。由于 Stage 和 Prod 都可以访问生产数据,因此它们都被视为生产环境。

预生产具有用于集成测试的 QA 环境和用于预发布功能集成的 E2E 环境。生产环境可能有用于生产依赖性测试的暂存环境和用于实时流量的实际生产环境。

2、配置管理

Kubernetes 帮助我们非常容易的为不同用例创建不同的环境,可以在同一个集群甚至多个集群上使用命名空间,可以托管QA、E2E、Stage、Prod……等等不同的环境。但问题是:如何管理所有这些环境?

第一种也是最直接的方法是创建相同manifest的副本,并为每个副本命名。也就是说,把源文件复制粘贴到每个环境上。

对于只需对每个环境做出极少改动的简单项目,上述方法可能很适用。例如,除了镜像外,所有 YAML 清单都完全相同。可以打开每个目录中的 deployment.yaml 文件进行更改,保存后运行 kubectl apply -f .,就大功告成了。

然而,在大多数情况下,环境之间的差异并不那么简单。请看下面的例子:

  • 开发环境通过某些容器命令参数进行调试,而这些参数在 QA 或生产环境中不可用。

  • QA 部署了一些边车,用于运行测试,开发和生产环境不具备这种能力。

  • 出于显而易见的原因,生产环境的 RBAC 比其他两个环境的限制性更强。

还有其他更多的可能性:

  • 应用变得越来越大,需要其他依赖服务。例如,MySQL 后端和 Redis 缓存服务器。每个服务都有自己的清单、配置设置和环境差异。

  • 需要实施 CI/CD 流水线,将应用程序(连同其依赖项)测试、构建和部署到多个环境中。

单独使用 kubectl 会变成一场噩梦,这就是我们开始探索更高级工具(特指 Helm 和 Kustomize)的原因。让我们先来探讨一下它们各自是如何应对上述挑战的。

2.1、Helm

作为 Kubernetes 的包管理器,Helm 提供了一种以"图表(charts)"形式打包、分发和管理应用程序的方法。Helm chart由模板(template)和值(value)文件集合组成,其中模板定义 Kubernetes 资源(如Deployment、Service、ConfigMap),值文件允许自定义模板值。

这样就可以拥有一组模板,为在不同部署(或环境)中发生变化的参数提供占位符。例如,下面是一个 Helm 部署模板,它从值文件中获取副本数量、镜像名称和标签、容器端口和容器启动参数:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-deployment
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          ports:
            - containerPort: {{ .Values.containerPort }}
          args:
            - {{ .Values.startupArguments }}

{{}} 之间的内容都是动态的。也就是说,在chart部署时,它们会被实际值取代。相应的值文件如下所示:

replicaCount: 3

image:
  repository: myapp/image
  tag: v1.0.0

containerPort: 8080

startupArguments: arg1 arg2 arg3

注意: .Release.Name.Chart.Name 变量取自 Chart.yaml,可视为另一个参数来源,用于为集群中的 Kubernetes 组件赋予唯一的名称,这样我们就能在同一个集群中部署同一 chart 的多个版本。

当 Helm 应用于集群时,Kubernetes API 服务器会收到这些信息:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp/image:v1.0.0
          ports:
            - containerPort: 8080
          args:
            - arg1
            - arg2
            - arg3

这样就可以为每种环境/用例设置不同的值文件。

对于整个环境的更改,只需修改一次源模板。而对于特定环境的更改,可以应用每个环境对应的值文件。

2.2、Kustomize

Kustomize 的目标是一样的,但不使用模板。相反,它在一个目录中保留完整版本的 YAML 文件。按照惯例,这个文件被称为 base。然后可以为每个环境/场景/用例创建一个目录(或目录树),每个目录都需要一个名为 kustomization.yaml 的 YAML 文件,该文件的目的是告知 Kustomize 应该考虑哪些 manifest 文件,以及需要对这些文件进行哪些修改。下面通过例子来说明这,看看如何使用 Kustomize 得出与 Helm 相同的结果。

首先创建一个目录结构:

myapp/
├── kustomization.yaml
├── base
│   └── deployment.yaml
└── overlay
    └── deployment.yaml

myapp/kustomization.yaml 的内容如下:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - base/deployment.yaml

patchesStrategicMerge:
  - overlay/deployment.yaml

base/deployment.yaml 看起来像这样:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: myapp
          image: myapp/image:v1.0.0
          ports:
            - containerPort: 8080

请注意,这是一个完全有效的 YAML,如果需要,也可以按原样应用。

要更改该部署以适应环境需求,可以使用 overlay/deployment.yaml 文件:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  template:
    spec:
      containers:
        - name: myapp
          args:
            - arg1
            - arg2
            - arg3

这样,发送到 Kubernetes API 服务器的文件就变成了

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: myapp
          image: myapp/image:v1.0.0
          ports:
            - containerPort: 8080
          args:
            - arg1
            - arg2
            - arg3

把同样的机制应用到三个环境中,目录结构可以是这样的:

myapp/
├── base
│   └── deployment.yaml
├── overlays
│   ├── dev
│   │   └── kustomization.yaml
│   │   └── deployment.yaml
│   ├── qa
│   │   └── kustomization.yaml
│   │   └── deployment.yaml
│   └── prod
│   │   └── kustomization.yaml
│   │   └── deployment.yaml

如果需要对整个环境进行更改,只需在 base/deployment 文件中进行一次更改,就会传播到所有地方。针对特定环境的更改在相应环境的自定义文件中完成。