暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Kubernetes and GitOps之环境管理

Halugin 2021-09-14
281

环境管理

本文包括以下内容:

  • 了解环境
  • 使用命名空间设计正确的环境
  • 组织Git repo/branching策略以支持你的环境
  • 为你的环境实现配置管理

本文将涉及关于不同运行时环境的知识,以及Kubernetes命名空间如何定义环境边界。我们还将了解几个配置管理工具(Helm、Kustomize和Jsonnet),以及它们如何帮助在多个环境中一致地管理应用程序的配置。

环境管理

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

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

当一个新版本的代码准备好发布时,代码通常会首先部署在Stage环境中(该环境可以访问实际的生产依赖项),以确保在代码在Prod环境中运行之前,所有的生产依赖项都已经就绪。例如,新代码可能需要更新新的DB模式,而Stage环境可用于验证新模式是否到位。配置的目的只是将测试流量引导到Stage环境,这样新代码引入的任何问题都不会影响实际的客户。但是,Stage环境通常配置为使用“真实的”生产数据库操作。必须仔细审查在Stage环境中执行的测试,以确保它们可以安全地在生产中执行。在Stage中通过所有测试之后,最终将在Prod中部署新代码以实现实时生产流量。由于Stage和Prod都可以访问生产数据,因此它们都被认为是生产环境。如图1所示:

图1

环境组成

环境由三个同等重要的部分组成:

  • Code(代码)
  • Run-time prerequisites(运行时的先决条件)
  • Configuration(配置)

代码是应用程序执行特定任务的机器指令。要执行代码,可能还需要运行时依赖项。例如,Node.js代码需要Node.js二进制文件和其他npm包才能成功执行。在Kubernetes的情况下,所有运行时依赖项和代码都被打包为一个可部署单元(也就是Docker镜像),并通过Docker守护进程进行编排。应用程序的Docker镜像可以放心地在任何环境中运行,从开发人员的笔记本电脑到运行在云中的生产集群,因为镜像封装了代码和所有依赖,消除了环境之间潜在的不兼容。

图2

图2左边是一个非基于容器的部署,在部署代码之前需要操作系统和运行时依赖。右边表示基于容器的部署,它包含代码和运行时依赖项。

特定于环境的应用程序属性的配置通常与代码和运行时依赖项一起部署,因此应用程序实例可以按照每个环境行为并连接到正确的依赖项。每个环境都可以包含DB存储、分布式缓存或消息传递(如数据)以实现隔离。环境也有自己的入口和出口网络策略,用于流量隔离和自定义访问控制。例如,可以将入口和出口配置为阻止preprod和prod环境之间的通信,以确保安全。可以将访问控制配置为将对生产环境的访问限制为只有一小部分工程师,而整个开发团队都可以访问preprod环境。

图3

图3中一个环境由应用实例、用于联网的入口/出口和保护其资源的访问控制组成。环境还包括应用程序依赖项,如缓存、DB或消息传递。

最终,我们的目标是将所有新代码部署到生产环境中,以便客户和最终用户能够在它通过质量测试后立即开始使用它。将代码部署到生产环境中的延迟导致开发团队延迟实现新代码的业务价值。选择正确的环境粒度对于及时部署代码至关重要。需要考虑的因素有:

  • Release independence-如果代码需要与要部署的其他团队的代码捆绑在一起,那么一个团队的部署周期取决于其他团队生成的代码的就绪情况。正确的粒度应该能够使你的代码在部署时不依赖于其他团队/代码。
  • Test boundary-与发布独立性一样,新代码的测试应该独立于其他代码发布。如果新代码测试依赖于其他团队/代码,那么发布周期将取决于其他团队的就绪情况。
  • Access control-除了对preprod和prod进行单独的访问控制之外,每个环境还可以将访问控制限制为只对积极从事代码库工作的团队进行。
  • Isolation-每个环境都是一个逻辑工作单元,应该与其他环境隔离,以避免相互影响,并出于安全原因限制来自不同环境的访问。

命名空间管理

名称空间是Kubernetes中支持环境的自然构造。它们允许在多个团队或项目之间划分集群资源。命名空间为唯一的资源命名、资源配额、RBAC、硬件隔离和网络配置提供了作用域:

Kubernetes Namespace ~= Environment

在每个Namespace中,应用程序实例(即Pod)是一个或多个Docker容器,在部署期间注入了特定于环境的应用程序属性。这些应用程序属性定义了环境应该如何运行(比如特性标志)以及应该使用哪些外部依赖项(比如数据库连接字符串)。

除了应用程序pod之外,命名空间还可能包含其他pod,它们提供环境所需的附加功能。

图4

图4中命名空间相当于Kubernetes中的环境。名称空间可能包括Pods(应用程序实例)、网络策略(进入/出口)和RBAC(访问控制),以及运行在独立Pods中的应用程序依赖项。

RBAC是一种基于企业中个人用户的角色来规范对计算机或网络资源访问的方法。在Kubernetes中,角色包含代表一组权限的规则。权限纯粹是附加的(没有拒绝规则)。角色可以用角色在命名空间内定义,也可以用ClusterRole在集群范围内定义。

名称空间还可以具有专用的硬件和网络策略,以根据应用程序需求优化其配置。例如,cpu密集型应用程序可以部署在具有专用多核硬件的命名空间中。另一种需要大量磁盘I/O的业务可以部署在一个独立的Namespace中,并使用高速SSD。每个命名空间还可以定义其网络策略(入口/出口),以限制跨命名空间流量或使用非限定DNS名称访问集群中的其他命名空间。

在本文中,将学习如何使用名称空间在两个不同的环境中(一个名为guestbook-qa的测试环境和一个名为guestbook-e2e的预prod端到端环境)部署相同的应用程序。

图5

图5中,前端架构将有一个服务,以暴露留言簿web前端的实时流量。后端架构由Redis Master和Redis Slave组成。

示例概述:

  1. 创建环境名称空间(guestbook-qa和guestbook-e2e)。
  2. 将guestbook应用程序部署到guestbook-qa环境中。
  3. 测试guestbook-qa环境。
  4. 将guestbook应用程序提升到guestbook-e2e环境。
  5. 测试guestbook-e2e环境。

首先,为每个guestbook环境创建guestbook-qa和guestbook-e2e命名空间:

root@gwz:~# kubectl create namespace guestbook-qanamespace/guestbook-qa created
root@gwz:~# kubectl create namespace guestbook-e2enamespace/guestbook-e2e created
root@gwz:~# kubectl get namespacesNAME              STATUS   AGE
default           Active   5h15m
guestbook-e2e     Active   7s
guestbook-qa      Active   17s
kube-node-lease   Active   5h15m
kube-public       Active   5h15m
kube-system       Active   5h15m

现在可以使用以下命令将guestbook应用程序部署到guestbook-qa环境中:

$ export K8S_GUESTBOOK_URL=https://github.com/kubernetes/website/blob/main/content/zh/examples/application/guestbook
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-master-deployment.yaml
deployment.apps/redis-master created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-master-service.yaml
service/redis-master created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-slave-deployment.yaml
deployment.apps/redis-slave created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-slave-service.yaml
service/redis-slave created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/frontend-deployment.yaml
deployment.apps/frontend created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/frontend-service.yaml
service/frontend created

测试guestbook-qa环境是否按照预期工作。使用下面的minikube命令来找到guestbook-qa服务的URL,然后在你的web浏览器中打开URL:

$ minikube -n guestbook-qa service frontend --url
http://192.168.43.193:31671$ open http://192.168.43.193:31671

在guestbook应用程序的Messages文本编辑中,键入类似于This is the guestbook-qa environment
的内容,然后按下Submit按钮。显示如图6所示:

图6

现在我们已经让Guestbook应用程序在Guestbook -qa环境中运行,并且已经测试了它是否正常工作,现在让我们将Guestbook -qa提升到Guestbook -e2e环境中。在本例中,我们将使用与guestbook-qa环境中使用的完全相同的YAML。这类似于自动CD流水线的工作方式:

$ export K8S_GUESTBOOK_URL=https://k8s.io/examples/application/guestbook
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-master-deployment.yaml
deployment.apps/redis-master created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-master-service.yaml
service/redis-master created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-slave-deployment.yaml
deployment.apps/redis-slave created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-slave-service.yaml
service/redis-slave created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/frontend-deployment.yaml
deployment.apps/frontend created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/frontend-service.yaml
service/frontend created

Guestbook应用程序现在已经部署到Guestbook -e2e环境中。现在让我们测试guestbook-e2e环境是否正常工作:

$ minikube -n guestbook-e2e service frontend --url
http://192.168.99.100:31090$ open http://192.168.99.100:31090

类似于在guestbook-qa环境中所做的,输入类似于This is the guestbook-e2e environment, NOT the guestbook-qa environment!
在Messages文本中编辑并按下Submit按钮。如图7所示。

图7

这里重要的是要认识到在Kubernetes Namespaces定义的两个不同环境中运行相同的应用程序。每个应用程序都维护其数据的独立副本,如果你在QA guestbook中输入一条消息,它不会在E2E guestbook中显示,这是两种不同的环境。

现在已经创建了两个preprod环境,guestbook-qa和guestbook-e2e,在新的生产集群中创建另外两个生产环境,guestbook-stage和guestbook-prod。

可以使用minikube start -p production
命令创建一个新的minikube集群,并使用kubectl config use-context
在它们之间进行切换。

网络隔离

定义用于部署应用程序的环境的一个关键方面是确保只有预期的客户机可以访问特定的环境。默认情况下,所有命名空间都可以连接到运行在所有其他命名空间中的服务。但是在两个不同的环境中,例如QA和Prod,你不希望在这些环境之间进行交叉对话。幸运的是,可以应用Namespace网络策略来限制Namespaces之间的网络通信。让我们看看如何将应用程序部署到两个不同的名称空间并使用网络策略控制访问。

将介绍在两个不同的命名空间中部署服务的步骤。你还可以修改网络策略并观察效果。

  1. 创建环境名称空间(qa和prod)。
  2. 将curl部署到qa和prod名称空间。
  3. 部署NGINX到prod命名空间。
  4. Curl NGINX从qa和prod命名空间(都工作)。
  5. 阻止从qa命名空间到prod命名空间的传入流量。
  6. 从qa命名空间Curl NGINX (blocked)。

出口流量(EGRESS)是指从网络内部开始,通过路由器到达网络外部某个目的地的网络流量。

入口流量(INGRESS)是由来自外部网络的所有数据通信和网络流量组成的。

图8

图8中为了让Curl Pod在QA中到达Web Pod在Prod中,Curl Pod需要通过QA出口到达Prod入口。然后,Prod入口将把流量路由到Prod中的Web Pod。

首先,为每个环境创建命名空间:

root@gwz:~# kubectl create namespace qanamespace/qa created
root@gwz:~# kubectl create namespace prodnamespace/prod created
root@gwz:~# kubectl get namespacesNAME              STATUS   AGE
default           Active   6h2m
guestbook-e2e     Active   47m
guestbook-qa      Active   47m
kube-node-lease   Active   6h2m
kube-public       Active   6h2m
kube-system       Active   6h2m
prod              Active   7s
qa                Active   17s

现在,我们将在这两个命名空间中创建一个Pod,从这里我们可以运行Linux命令curl:

root@gwz:/data/manifest# cat curlpod.yaml apiVersion: v1
kind: Pod
metadata:
  name: curl-pod
spec:
  containers:
  - name: curlpod
    image: radial/busyboxplus:curl
    command:
    - sh
    - -c
    - while true; do sleep 1; done     root@gwz:/data/manifest# kubectl -n qa apply -f curlpod.yaml pod/curl-pod created
root@gwz:/data/manifest# kubectl -n prod apply -f curlpod.yaml pod/curl-pod created

在prod命名空间中,我们将运行一个NGINX服务器来接收curl
HTTP请求:

root@gwz:/data/manifest# cat web.yaml apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
  - image: nginx
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP
root@gwz:/data/manifest# kubectl -n prod apply -f web.yaml pod/web created

默认情况下,在Namespace中运行的Pods可以将网络流量发送到在不同Namespace中运行的其他Pods。让我们通过从qa命名空间中的Pod执行curl
命令到prod命名空间中的NGINX Pod来证明这一点:

root@gwz:/data/manifest# kubectl describe pod web -n prod | grep IP  ❶IP:           172.17.0.6root@gwz:/data/manifest# kubectl -n qa exec curl-pod -- curl -I 172.17.0.6      ❷HTTP/1.1 200 OK
Server: nginx/1.21.3Date: Mon, 13 Sep 2021 07:14:49 GMT
Content-Type: text/html
Content-Length: 615Last-Modified: Tue, 07 Sep 2021 15:21:03 GMT
Connection: keep-alive
ETag: "6137835f-267"Accept-Ranges: bytes
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed  0   615    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0root@gwz:/data/manifest# kubectl -n prod exec curl-pod -- curl -I 172.17.0.6      ❸HTTP/1.1 200 OK
Server: nginx/1.21.3Date: Mon, 13 Sep 2021 07:15:27 GMT
Content-Type: text/html
Content-Length: 615Last-Modified: Tue, 07 Sep 2021 15:21:03 GMT
Connection: keep-alive
ETag: "6137835f-267"Accept-Ranges: bytes
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed  0   615    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0

❶ 获取web Pod的IP地址

❷返回HTTP 200

❸ 返回HTTP 200

通常,你不希望qa和prod环境之间存在依赖关系。如果应用程序的两个实例都配置正确,那么qa和prod之间可能就不存在依赖关系了,但是如果在qa的配置中有一个错误,它意外地将流量发送到prod呢?你可能会破坏生产数据。甚至在生产环境中,如果一个环境托管你的营销站点,而另一个环境托管具有敏感数据的HR应用程序,该怎么办?在这些情况下,可能需要阻止Namespaces之间的网络通信,或者只允许特定Namespaces之间的网络通信。这可以通过将NetworkPolicy
添加到名称空间来实现。

让我们在每个名称空间的pod中添加一个NetworkPolicy
:

容器网口只有配置了CNI (Container network interface)(minikube和Docker desktop均不支持)时,才支持网络策略。

root@gwz:/data/manifest# cat block-other-namespace.yamlapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  namespace: prod               ❶
  name: block-other-namespace
spec:
  podSelector: {}               ❷
  ingress:
  - from:
    - podSelector: {}           ❸
root@gwz:/data/manifest# kubectl apply -f block-other-namespace.yaml networkpolicy.networking.k8s.io/block-other-namespace created

❶应用于命名空间prod

❷在Namespace prod中选择所有pod

❸指定入口,只允许来自prod Namespace的请求。来自其他命名空间的请求将被阻止

这个NetworkPolicy
被应用到prod Namespace,并且只允许从prod Namespace进入(传入的网络流量)。正确使用NetworkPolicy
约束是定义环境边界的一个关键方面。

应用了NetworkPolicy
后,我们可以重新运行curl
命令来验证每个命名空间现在都是独立的:

$ kubectl -n qa exec curl-pod -- curl -I 172.17.0.6    ❶
$ kubectl -n prod exec curl-pod -- curl -I 172.17.0.6

❶命名空间qa的Curl被阻塞了

❷返回Http 200

Preprod和prod集群

既然你已经知道如何使用Namespaces创建多个环境,那么使用一个集群并在该集群上创建所需的所有环境似乎是一件很简单的事情。例如,你的应用程序可能需要QA、E2E、Stage和Prod环境。然而,根据你的具体用例,这可能不是最好的方法。我们的建议是使用两个集群来承载你的环境,一个用于生产前环境的预prod集群,另一个用于生产环境的prod集群。

使用两个独立的集群来承载你的环境的主要原因是保护你的生产环境免受意外中断或与生产前环境的工作相关的其他影响。

AWS中的集群隔离中,可以为preprod和prod创建单独的VPC,作为逻辑边界,实现流量和数据的隔离。为了实现更强的隔离和对生产凭据和访问的更多控制,独立的生产虚拟私有云应该托管在不同的生产AWS帐户中。

有人可能会问,为什么我们要有这么多的环境以及preprod和prod集群的分离。简单的答案是,在将代码发布到生产集群之前,需要一个预prod集群来测试代码。我们使用QA环境进行集成测试,使用E2E环境作为其他服务测试预发布特性的稳定环境。如果你正在进行多分支并发开发,你还可以为每个分支配置额外的预prod测试环境。

使用Kubernetes进行配置管理的一个关键优势是,由于它使用Docker容器(不可变的可移植镜像),所以在不同环境之间部署的唯一区别是命名空间配置、特定于环境的属性和应用程序依赖关系,比如缓存或数据库。预prod测试可以验证服务代码的正确性,而生产集群中的阶段环境可以用来验证应用程序依赖项的正确性。

Preprod和prod集群应该遵循相同的安全最佳实践和操作严格性。安全问题可以在开发周期的早期检测到,如果preprod集群使用与生产相同的标准操作,那么开发人员的生产力不会中断。

Git strategies

使用一个单独的Git存储库来保存Kubernetes清单(也就是配置),将配置与应用程序源代码分开,强烈建议这样做,原因如下:

  • 它提供了应用程序代码和应用程序配置的清晰分离。有时你希望在不触发整个CI构建的情况下修改清单。例如,如果你只是想增加部署规范中的副本数量,则可能不希望触发构建。
  • 审计日志更干净。出于审计目的,一个只保存配置的repo将有一个更清晰的Git历史记录,记录所做的更改,而不会因为常规的开发活动而产生签入的噪音。
  • 你的应用程序可能包含由多个Git存储库构建但作为单个单元部署的服务。微服务应用通常由不同版本方案和发布周期的服务组成(如ELK、Kafka和Zookeeper)。将清单存储在单个组件的源代码存储库中可能没有意义。
  • 开发应用程序的开发人员不一定是能够/应该推送到生产环境的同一个人,无论有意还是无意。拥有独立的存储库允许将提交权限授予源代码repo,而不是应用程序配置repo,后者可以保留给经过更多选择的团队成员组。
  • 如果你正在自动化CI管道,那么将清单更改推到相同的Git存储库可能会触发构建作业和Git提交触发器的无限循环。有一个单独的repo推动配置更改,以防止这种情况发生。

对于代码存储库,可以使用任何你喜欢的分支策略(例如GitFlow),因为它只用于你的CI。对于你的配置存储库(将用于你的CD),你需要根据你的组织规模和工具考虑以下策略。

单分支(多目录)

使用单分支策略,主分支将始终包含在每个环境中使用的精确配置。所有环境都有一个默认配置,在单独的环境目录中定义一个环境特定的覆盖层。单分支策略可以很容易地被Kustomize等工具支持。

图9

图9中单分支策略将为每个环境拥有一个主分支和一个子目录。每个子目录将包含特定于环境的覆盖。

在我们的CI/CD示例中,我们将为qa、e2e、stage和prod提供特定于环境的覆盖目录,每个目录将包含特定于环境的设置,如副本计数、CPU和内存请求/限制。

图10

图10中qal、e2e、stage和production子目录的示例每个子目录将包含副本计数、CPU和内存请求/限制等覆盖。

多分支

使用多分支策略时,每个分支等同于一个环境。这样做的好处是,每个分支都可以在不使用Kustomize这样的工具的情况下,为环境提供精确的清单。每个分支还将有单独的提交历史,用于审计跟踪和必要时的回滚。缺点是,由于Kustomize之类的工具不能与Git分支一起使用,因此在环境之间无法共享公共配置。

图11

图11中在多分支策略中,每个分支等同于一个环境。每个分支将包含确切的清单,而不是覆盖。

可以合并多个分支之间的公共基础设施更改。假设需要向所有环境添加一个新资源。在这种情况下,可以先将该资源添加到QA分支并进行测试,然后在完成适当的测试后将其合并到每个后续分支中。

Multirepo vs. monorepo

如果你处在一个只有一个scrum团队的启动环境中,你可能不希望(或需要)多个存储库的复杂性。所有代码可以放在一个代码存储库中,所有部署配置可以放在一个部署存储库中。

然而,如果你在一个拥有数十(或数百)开发人员的企业环境中,你可能会希望有多个repo协议,以便团队可以彼此分离,每个团队都以自己的速度运行。例如,组织中的不同团队对他们的代码有不同的节奏和发布过程。如果使用了mono配置repo,一些特性可能需要几个星期才能完成,但需要等待预定的发布。这可能意味着延迟将特性交付给最终用户,并发现潜在的代码问题。回滚也是有问题的,因为一个代码缺陷将需要回滚每个团队的所有更改。

图12

图12中 Monorepo是一个带有多个项目的Git repo。在multirepo中,每个项目都有一个专用的Git repo。

使用多个repo协议的另一个考虑事项是根据功能组织应用程序。如果repos集中于离散的可部署功能,那么在团队之间转移这些功能的责任将会更容易(比如在重组之后)。

配置管理

环境配置管理可以非常简单,只需为每个环境创建一个目录,其中包含应该部署的所有资源的YAML清单。这些YAML清单中的所有值都可以硬编码为该环境所需的特定值。要进行部署,请运行kubectl apply -f <directory>

然而,实际情况是,以这种方式管理多个配置会很快变得笨拙且容易出错。如果你需要添加一个新资源呢?你需要确保将该资源添加到每个环境中的YAML中。如果该资源需要特定的属性(比如副本)来为不同的环境提供不同的值,该怎么办?你需要小心地在所有正确的文件中进行所有正确的定制。

已经开发了一些工具来解决配置管理的这种需求,我们讨论一下在选择使用哪种工具时应该考虑哪些因素。

好的Kubernetes配置工具应该具有以下属性:

  • Declarative-该配置是明确的、确定的,并且不依赖于系统。
  • Readable-配置是以易于理解的方式编写的。
  • Flexible-这个工具有助于促进,而不会妨碍你完成你想要做的事情。
  • Maintainable-该工具应该促进重用和可组合性。

Kubernetes配置管理如此具有挑战性的原因有以下几个:部署应用程序的操作听起来很简单,但却有非常不同甚至相反的需求,单一工具很难满足所有这些需求。想象以下用例:

  • 集群运营商将第三方的、现成的应用程序(如WordPress)部署到他们的集群中,几乎不需要定制这些应用程序。这个用例最重要的标准是轻松地接收来自上游源的更新,并尽可能快速、无缝地升级他们的应用程序(新版本、安全补丁,等等)。
  • 软件即服务(SaaS)应用程序开发人员将他们定制的应用程序部署到一个或多个环境(Dev、Staging、Prod-West、Prod-East)。这些环境可能分布在不同的帐户、集群和名称空间之间,它们之间有细微的差异,因此配置重用是至关重要的。对于这个用例,重要的是从代码库中的Git提交,以完全自动化的方式部署到每个环境,并以简单和可维护的方式管理环境。这些开发人员对发布版本的语义版本控制毫无兴趣,因为他们可能一天要部署多次。主要版本、次要版本和补丁版本的概念最终对它们的应用程序没有意义。

正如你所看到的,这些是完全不同的用例,而且通常情况下,擅长其中一个用例的工具不能很好地处理另一个用例。

Helm

Helm,作为第一个配置工具,是Kubernetes生态系统中不可缺少的一部分,你很有可能通过运行Helm install
安装了一些服务。

关于Helm需要注意的重要一点是,它是Kubernetes的一个自我描述的包管理器,并没有声称是一个配置管理工具。然而,由于许多人正是出于这个目的使用Helm模板,因此它属于本文的讨论范围。这些用户最终总是维护几个值。Yaml,每个环境一个(例如基于值。yaml values-prod。Yaml和values-dev. Yaml),然后以一种特定于环境的值可以在图表中使用的方式对它们的图表进行参数化。这个方法或多或少可以工作,但它使模板变得笨拙,因为Go模板是扁平的,需要支持每个环境的每个可以想到的参数,这最终会用{{-if else}}
开关丢弃整个模板。

作为示例,我们使用前面部署的guestbook应用程序,并使用Helm来管理它在不同环境下的配置。

Helm使用以下目录结构来构造charts。

├──  Chart.yaml            ❶
├──  templates             ❷
│   └──    guestbook.yaml
├──  values-prod.yaml      ❸
└──  values-qa.yaml        ❸

❶Chart.yaml是对chart的描述符

❷模板目录,当与值组合时,将生成有效的Kubernetes清单文件

❸Chart的各种配置值,可以用于特定于环境的配置

Helm模板文件使用文本模板语言生成Kubernetes YAML。Helm模板文件看起来像Kubernetes YAML,但是模板变量散布在整个文件中。因此,即使是基本的Helm模板文件最终也会是这样的。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "sample-app.fullname" . }}
  labels:
    {{- include "sample-app.labels" . | nindent 4 }}
spec:
  selector:
    matchLabels:
      {{- include "sample-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
    {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      labels:
        {{- include "sample-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        {{- with .Values.environmentVars }}
          env:
            {{- toYaml . | nindent 12 }}
        {{- end }}

Helm模板不是很易读。但是它们非常灵活,因为最终生成的YAML可以按照用户希望的任何方式进行定制。

最后,当使用Helm定制特定环境时,将创建一个特定于环境的值文件,其中包含用于该环境的值。例如,对于该应用程序的生产版本,值文件可能如下所示。

# Default values for sample-app.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
image:
  repository: gitopsbook/sample-app
  tag: "v0.2"                        ❶
nameOverride: "sample-app"
fullnameOverride: "sample-app"
podAnnotations: {}
environmentVars: [                   ❷
  {
    name: "DEBUG",
    value: "true"
  }
]

❶覆盖默认为chart appVersion的镜像标记

❷将DEBUG环境变量设置为true

最后的qa清单可以通过以下命令安装到qa-heml命名空间中的minikube中:

$ kubectl create namespace qa-helm
$ helm template . --values values.yaml | kubectl apply -n qa-helm -f -
deployment.apps/sample-app created
$ kubectl get all -n qa-helm
NAME                               READY   STATUS       RESTARTS    AGE
pod/sample-app-7595985689-46fbj    1/1     Running      0           11s
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample-app         1/1     1            1           11s
NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-app-7595985689   1         1         1       11s

我们使用Helm参数化了QA和Prod环境的guestbook镜像标签。为每个guestbook部署所需的副本数量添加额外的参数。设置QA的副本数量为1,Prod的副本数量为3。

Kustomize

Kustomize是根据Brian Grant关于声明式应用程序管理的论文中描述的设计原则创建的,Kustomize的受欢迎程度迅速上升,在它启动的8个月里,它已经被并入kubectl。不管你是否同意Kustomize的合并方式,Kustomize应用程序现在将成为Kubernetes生态系统的永久支持,并将成为配置管理用户的默认选择。

使用上面的示例应用程序,并用Kustomize部署它:

├──  base                          ❶
│   ├── deployment.yaml
│   └── kustomization.yaml
└──  envs
    ├── prod                       ❷
    │   └── kustomization.yaml
    └── qa                         ❸
        ├── debug.yaml
        └── kustomization.yaml

❶基本目录包含将由不同环境共享的公共配置。

❷envs/prod目录包含了prod环境的配置。

❸envs/qa目录包含qa环境的配置。

基目录中的清单包含所有环境都通用的所有资源。在这个简单的示例中,我们只有一个Deployment资源。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - command:
        - /app/sample-app
        image: gitopsbook/sample-app:REPLACEME     ❶
        name: sample-app
        ports:
        - containerPort: 8080

❶在基本配置中定义的镜像是不相关的。这个版本的镜像将永远不会被部署,因为本例中的子覆盖环境将覆盖这个值。

要使用基目录作为其他环境的基目录,需要进行库化。Yaml必须出现在目录中。下面是最简单的kustomization.yaml。它只是列出了Yaml作为组成guestbook应用程序的单个资源。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml

现在我们已经建立了kustomize基本目录,我们可以开始定制环境了。为了定制和修改特定环境的资源,我们定义了一个覆盖目录,其中包含我们想要应用在基础资源之上的所有补丁和定制。我们的第一个覆盖是envs/qa目录。在这个目录中是另一个kustomization.yaml,它指定应该应用在基础之上的补丁。下面的两个清单提供了一个qa
覆盖的示例

  • 设置要部署到新标记的不同guestbook镜像(v0.2)
  • 向guestbook容器添加一个环境变量DEBUG=true
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:                        ❶
- ../../base
patchesStrategicMerge:
- debug.yaml                  ❷
images:                       ❸
- name: gitopsbook/sample-app
  newTag: v0.2

❶Bases引用包含共享配置的“base”目录。

❷debug.yaml是对Kustomize补丁的引用,该补丁将修改样例应用程序部署对象并设置DEBUG环境变量。

❸使用不同的标记或镜像存储库,镜像覆盖基中定义的任何容器镜像。这个示例使用v0.2覆盖镜像标记REPLACEME。

Kustomize补丁看起来与实际的Kubernetes资源非常相似,这是因为它们实际上是它们的不完整版本。

apiVersion: apps/v1          ❶
kind: Deployment
metadata:
  name: sample-app
spec:
  template:
    spec:
      containers:
      - name: sample-app     ❷
        env:                 ❸
        - name: DEBUG
          value: "true"

❶apiVersion组(apps)、类型(Deployment)和名称(sample-app)是通知Kustomize这个补丁应该应用到基地中的哪个资源的关键信息。

❷name字段用于标识哪个容器将具有新的环境变量。

❸最后,定义了我们希望在QA环境中使用的新DEBUG环境变量。

在完成所有这些之后,我们运行kustomize build env /qa
。这将为QA环境生成最终呈现的清单。

$ kustomize build envs/qa
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - command:
        - /app/sample-app
        env:
        - name: DEBUG                       ❶
          value: "true"
        image: gitopsbook/sample-app:v0.2   ❷
        name: sample-app
        ports:
        - containerPort: 8080

❶添加了DEBUG环境变量。

❷镜像标签被设置为v0.2。

最后的qa清单可以通过以下命令安装到qa命名空间中的minikube中:

$ kubectl create namespace qa
$ kustomize build envs/qa | kubectl apply -n qa -f -# kubectl get all -n qaNAME                              READY   STATUS    RESTARTS   AGE
pod/sample-app-7595985689-46fbj   1/1     Running   0          11s
NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample-app   1/1     1            1           11s
NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-app-7595985689   1         1         1       11s

Jsonnet

Jsonnet是一种语言,而不是真正的工具。此外,它的使用并不是Kubernetes特有的(尽管Kubernetes已经普及了它)。最好将Jsonnet看作是一个超级强大的JSON,并结合了一种合理的模板方法。Jsonnet结合了你希望使用JSON可以实现的所有功能(注释、文本块、参数、变量、条件、文件导入),而不需要go/Jinja2模板中的任何东西,并添加了你甚至不知道自己需要或想要的功能(函数、面向对象、mixin)。它以声明式和密封(代码作为数据)的方式完成所有这些工作。

当我们查看基本Jsonnet文件时,它看起来非常类似于JSON,这是有意义的,因为Jsonnet是JSON的超集,所有JSON都是有效的Jsonnet。在我们的示例中,我们也可以在文档中添加注释。

{
   // Look! It's JSON with comments!
   "apiVersion": "apps/v1",
   "kind": "Deployment",
   "metadata": {
      "name": "nginx"
   },
   "spec": {
      "selector": {
         "matchLabels": {
            "app": "nginx"
         }
      },
      "replicas": 2,
      "template": {
         "metadata": {
            "labels": {
               "app": "nginx"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "nginx",
                  "image": "nginx:1.14.2",
                  "ports": [
                     {
                        "containerPort": 80
                     }
                  ]
               }
            ]
         }
      }
   }
}

让我们看看如何利用简单的Jsonnet特性,减少重复、更好地组织代码/配置的最简单方法之一就是使用变量。在下一个示例中,我们在Jsonnet文件的顶部声明一些变量(名称、版本和副本),并在整个文档中引用这些变量。这使得我们可以在单个可见的地方进行更改,而无需扫描整个文档以寻找其他需要相同更改的地方,因为这很容易出错,尤其是在大型文档中。

local name = "nginx";
local version = "1.14.2";
local replicas = 2;
{
   "apiVersion": "apps/v1",
   "kind": "Deployment",
   "metadata": {
      "name": name
   },
   "spec": {
      "selector": {
         "matchLabels": {
            "app": name
         }
      },
      "replicas": replicas,
      "template": {
         "metadata": {
            "labels": {
               "app": name
            }
         },
         "spec": {
            "containers": [
               {
                  "name": name,
                  "image": "nginx:" + version,
                  "ports": [
                     {
                        "containerPort": 80
                     }
                  ]
               }
            ]
         }
      }
   }
}

最后,通过我们的示例,开始利用Jsonnet的一些独特而强大的特性:函数、参数、引用和条件。下一个示例开始演示Jsonnet的强大功能。

function(prod=false) {                      ❶
   "apiVersion": "apps/v1",
   "kind": "Deployment",
   "metadata": {
      "name": "nginx"
   },
   "spec": {
      "selector": {
         "matchLabels": {
            "app": $.metadata.name          ❷
         }
      },
      "replicas": if prod then 10 else 1,   ❸
      "template": {
         "metadata": {
            "labels": {
               "app": $.metadata.name
            }
         },
         "spec": {
            "containers": [
               {
                  "name": $.metadata.name,
                  "image": "nginx:1.14.2",
                  "ports": [
                     {
                        "containerPort": 80
                     }
                  ]
               }
            ]
         }
      }
   }
}

❶与前面的示例不同,配置被定义为Jsonnet函数,而不是普通的Jsonnet对象。这允许配置声明输入并从命令行接受参数。prod是函数的一个布尔参数,默认值为false。

❷我们可以在不使用变量的情况下自引用文档的其他部分。

❸副本数量是根据条件设置的。

$ jsonnet advanced.jsonnet
$ jsonnet --tla-code prod=true advanced.jsonnet

Jsonnet中还有更多的语言特性,我们甚至还没有触及其功能的表面。不幸的是,在Kubernetes社区中,Jsonnet并没有被广泛采用,因为在这里描述的所有工具中,Jsonnet无疑是最强大的配置工具,这也是为什么一些分支工具是在它的基础上构建的。

配置管理汇总

使用每种工具都有利弊。下表显示了这些具体工具在配置管理中的比较。


HelmKustomizeJsonnet
DeclarativeFairExcellentExcellent
ReadabilityPoorExcellentFair
FlexibilityExcellentPoorExcellent
MaintainabilityFairExcellentExcellent

持久与短暂的环境

持久的环境是永远可用的环境。例如,生产环境总是需要是可用的,这样服务才不会中断。在持久的环境中,资源(内存、CPU、存储)将被永久提交,以实现始终在线的可用性。通常,E2E是用于内部集成的持久环境,而Prod是用于生产通信的持久环境。

临时环境是不依赖于其他服务的临时环境。临时环境也不需要永久提交资源。例如,Stage用于测试新代码的生产准备情况,并且在测试完成后不需要出现。另一个用例是预览拉请求的正确性,以保证只有好的代码被合并到主代码中。在这种情况下,将使用pull请求更改创建一个临时环境,以便对其进行测试。一旦所有测试完成,PR环境将被删除,只有在所有测试通过时,PR更改才允许合并回master。

如果持久性环境将被其他环境使用,则持久性环境中的缺陷可能会中断其他环境,并可能需要回滚来恢复正确的功能。对于GitOps和Kubernetes,回滚只是通过Git重新应用前面的配置。Kubernetes将检测清单中的更改,并将环境恢复到以前的状态。

Kubernetes使环境中的回滚一致且简单,但是其他资源(如数据库)呢?由于用户数据存储在数据库中,我们不能简单地将数据库回滚到上一个快照,从而导致用户数据的丢失。与Kubernetes中的滚动更新部署一样,新版本和旧版本的代码需要与滚动更新兼容。在数据库的情况下,DB模式需要向后兼容,以避免在回滚期间中断和用户数据丢失。在实践中,这意味着只能添加(而不能删除)列,并且不能更改列定义。模式变更应该用其他变更管理框架来控制,例如Flyway,这样DB变更也可以遵循GitOps流程。

总结

  • 环境是为特定目的部署和执行代码的地方。
  • 每个环境都有自己的访问控制、网络、配置和依赖项。
  • 选择环境粒度的因素有发布独立性、测试边界、访问控制和隔离。
  • Kubernetes命名空间是实现环境的自然构造。
  • 因为名称空间等同于环境,所以部署到特定环境只是指定目标名称空间。
  • 环境间的流量可以通过网络策略进行控制。
  • Preprod和prod应该遵循相同的安全最佳实践和操作活力。
  • 强烈建议将Kubernetes清单的Git repo和代码的Git repo分离开来,以允许环境更改独立于代码更改。
  • 单个分支可以很好地与Kustomize覆盖工具一起工作。
  • Helm是一个软件包管理工具。
  • Kustomize是一个内置的配置管理工具,是kubectl的一部分。
  • Jsonnet是用于JSON模板的语言。
  • 选择正确的配置管理工具应该基于以下标准:声明性、可读性、灵活性和可维护性。
  • 持久的环境总是供其他人使用,而短暂的环境用于短暂的测试和预览。

兴趣的关注如下公众号!

文章转载自Halugin,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论