在本章中,我们将看看什么是GitOps,以及这个想法在 Kubernetes集群中如何有意义。我们将介绍特定的组件,例如应用程序编程接口(API)服务器和控制器管理器,它们可以使集群对状态更改做出反应。我们将从命令式API开始,然后浏览声明式API,并将看到如何应用文件和文件夹来应用Git存储库只是一个步骤——当执行它时,GitOps出现了。
我们将在本章中介绍以下主要主题:

  • 什么是GitOps?
  • Kubernetes和GitOps
  • 命令式和声明式API
  • 构建一个简单的GitOps操作符
  • 基础设施作为代码(IaC)和GitOps

1.1 技术要求

在本章节,你需要访问一个Kubernetes的集群和一个如minikube 这样的本地集群(https://minikube.sigs.k8s. io / docs /))或者kind (https://kind.sigs.k8s.)就可以了。我们将与集群交互并向它发送命令,因此你还需要安装kubectl (https://kubernetes.io/docs/tasks/tools/#kubectl))。
我们要写一些代码,所以需要一个代码编辑器。本人使用的是Visual Studio Code ( VS Code )(https://code.visualstudio.com)),我们要使用需要安装:https://golang.org(Go的当前版本是1.16.7;代码应该能和它一起使用)的Go语言。代码能在:https://golang.orghttps://github.com/PacktPublishing/ ArgoCD - in Practice的ch01的文件夹中找到。

1.2 什么是GitOps

GitOps一词是在2017年由Weaveworks的人创造的,他们也是名为Flux的GitOps工具的作者。从那以后,我看到了GitOps是如何成为一个流行词的,直到被命为development-operations(DevOps)之后的下一个重要事物。如果你搜索定义和解释,你会发很多:它被定义为通过 pull requests(PRs)进行操作(https://www.weave.works/blog/gitops-operations-by-pull-request)),或者采用开发实践(版本控制、协作、合规、持续集成/持续部署(CI/CD)),并将其运用于基础设施自动化。(https://about.gitlab.com/topics/gitops/ )
不过,我认为有一个定义很突出。我指的是由GitOps工作组(https://github.com/gitops-working-group/gitops-working- group )创建的工作组,该工作组是Cloud Native Computing Foundation(CNCF)的Application Delivery Technical Advisory Group(Application DeliveryTAG)的一部分。Application DeliveryTAG是专门用于构建、部署、管理和操作云原生应用程序 (https://github.com/cncf/tag-app-delivery)。该工作组是由来自不同公司的人组成的,目的是为GitOps构建一个与供应商无关、以原则为主导的定义,所以我认为这些都是仔细研究他们工作的好理由。
该定义侧重于GitOps的原则,目前已经确定了五个原则(这仍是一个草案),如下:

  • 声明式的配置

  • 版本控制的不可变存储

  • 自动化交付

  • 软件代理

  • 闭环

    它从声明式配置开始,这意味着我们想要表达我们的意图、结束状态,而不是要执行特定的动作。这不是一种命令式的风格,你说”让我们再启动三个容器“,而是声明想为这个应用程序拥有三个容器,代理将负责达到这个数字,这可能意味着如果现有五个容器,它需要停止两个正在运行的容器。Git在这里被称为版本控制和不可变控制,这是公平的,虽然它是目前最常用的源代码控制系统,但是它不是唯一的一个,我们可以用其他源代码控制系统实现GitOps。
    自动交付意味着一旦更改到达版本控制系统 (VCS),我们就不应该有任何手动操作。配置更新后,软件代理将确保采取必要的操作来达到新的声明式配置。因为我们在表达想要的状态,所以需要计算达到它的动作。它们产生于系统的实际状态和版本控制的期望状态之间的差异——这就是闭环部分试图说明的。
    虽然GitOps起源于Kubernetes世界,但是这个定义试图将它从图片中拿出来,并将前面的原则引入整个软件世界。在我们的案例中,看看是什么使GitOps成为可能,并深入了解Kubernetes中的软件代理是什么,或者闭环在这里是如何运行的,仍然很有趣。

1.3 Kubernetes 和 GitOps

现在很难不听说Kubernetes—它可能是目前最知名的开源项目之一。它起源于2014年左右,当时谷歌的一群工程师开始根据他们与谷歌自己的名为Borg的内部协调器合作积累的经验来构建一个容器协调器。该项目于2014开源,并在2015年达到1.0.0版本,这是一个里程碑,鼓励许多公司仔细研究它。
另一个导致它被社区迅速且被狂热的采用的原因是CNCF的治理(https://www.cncf.io))。在使该项目开源之后,谷歌与Linux基金会 (https://www.linuxfoundation.org)讨论创建一个新的非营利组织,该组织将领导开源云原生技术的采用。当Kubernetes成为它的种子项目且KubeCon是它的主要开发者大会时,CNCF就是这样被创建的。当我说到CNCF的治理时,我主要指的是这样一个事实,即CNCF内部的每个项目或组织都有一个完善的维护者结构,并详细的说明了他们是如何被提名的,这些团队是如何做决定的,没有一家公司能拥有一个简单的多数。这确保了在没有社区参与的情况下不会做出任何决定,并确保整个社区在项目生命周期中扮演着重要的角色。

1.3.1 体系结构

Kubernetes变得如此的庞大和可扩展以至于如果不用抽象的概念(比如构建平台的平台)就真的很难去定义它。这是因为它仅仅只是一个起点——你会得到很多部分,但你必须以一种适合你的方式去把它们组合在一起(GitOps就是其中一部分)。如果我们说它只是一个容器编排平台,这并不完全正确,因为你也可以用它来运行虚拟机(VMs),而不仅仅是容器(更多详细信息,请查看[https://ubuntu.com/blog/what-is-kata-containers](https://ubuntu.com/blog/what-is-kata-containers)),不过,编排部分仍然是正确的。

它的组件分为两个主要部分——第一个是控制平面,它由一个 REpresentational State Transfer(REST)API服务器和一个用于存储的数据库(通常是etcd)组成、一个用于运行多个控制环路的控制器管理器,一个调度器负责为我们的Pod分配节点(Pod是一个容器的逻辑分组,有助于在同一节点上运行它们。更多信息请访问https://kubernetes.io/docs/concepts/workloads/pods/),一个云控制器管理器,用于处理任何云特定的工作。第二部分是数据平面,控制平面是关于管理集群的,而这一部分是关于运行用户工作负载的节点上发生的事情。作为Kubernetes集群的一部分节点将具有容器运行时(可以是Docke、CRI-O或 containerd ,和其他一些),Kubelet,负责 REST API服务器和节点的容器运行时之间的连接,以及Kube-proxy负责在节点级别抽象网络。有关所有组件如何协同工作以及API服务器所扮演的中心角色的详细信息,请参阅下一个表。
我们不会详细介绍所有这些组成部分;相反,对于我们来说,使声明性部分成为可能的REST API 服务器和使系统收敛到所需状态的控制器管理器很重要,所以我们想对它们进行一点剖析。
下图显示了一个典型的Kubernetes体系结构的概述:
微信图片_20230514212210.png

请注意:
当查看架构图时,你需要知道它只能捕捉到整个画面的一部分。例如,在这里,带有API的云提供商似乎时一个外部系统,但实际上,所有节点和控制平面都是在该云提供商中创建的。

1.3.2 HTTP REST API服务器

从超文本传输协议(HTTP) REST API服务器的角度来看Kubernetes,它就像任何具有REST端点和用于存储状态的数据库的经典应用程序一样,在我们的例子中,通常是etcd和web服务器的多个副本以实现高可用性(HA)。需要强调的是,我们想用Kubernetes做的任何事都需要通过API来完成;我们不能直接连接到其他软件,对于内部组织也是一样:它们之间不能直接对话,它们需要通过API。
从我们的客户端机器上,我们不直接查询API(例如使用curl),而是使用这个kubectl客户端应用程序,它隐藏了一些复杂性,例如身份验证标头、准备请求内容、解析响应正文等。
每当我们执行kubectl get pods之类的命令时,都会有对API服务器的HTTP Secure(HTTPS)进行调用。然后,服务器转到数据库以获取有关Pods的详细信息,然后创建一个响应并将其推送回到客户端。kubectl客户端应用程序接收并解析它,然后能够显示适合人类阅读者的输出。为了了解具体发生了什么,我们可以使用kubectl(–v)的全局标志,我们为其设置的值越高,我们获得的详细信息就越多。
对于一个练习,请尝试kubectl get pods–v=6,当它只显示执行了GET请求时,并将–v不断增加到7、8、9等,以便你可以看到HTTP请求标头、响应标头、部分或全部JavaScript对象表示法(JSON)响应以及许多其他详细信息。
API服务器本身并不负责实际更改集群的状态——它使用新值更新数据库,并根据这些更新,还会发生其他事情。实际的状态更改是由控制器和如调度器或kubelet等组件完成的。我们将深入了解控制器,因为它们对我们理解GitOps很重要。

1.3.3 控制器管理器

当阅读到有关Kubernetes的文章(或者听播客)时,你会经常听到controller这个词。它背后的想法来自工业自动化或机器人,它是关于转换控制回路的。
假设我们有一个机械臂,我们给它一个简单的命令,让它在90度的位置上移动。它要做的第一件事就是分析它的当前状态;或许它已经在90度了,没有什么可做的。如果它不在正确的位置上的话,接下来的事情就是计算到达那个位置而采取的行动,然后,它将尝试应用这些行动来到达它的相对位置。
我们从观察阶段开始,在该阶段,我们将所需状态与当前状态进行比较,然后是差异阶段,我们计算要应用的操作,在操作阶段,我们执行这些操作。同样,在我们执行操作之后,它将开始观察阶段,看看它是否位于正确的位置;如果没有(可能有什么东西阻止了它到达那里),就会计算操作,然后我们开始应用操作,以此类推,直到它到达某个位置或可能耗完电池或者其他什么的。这个控制循环一直持续下去,直到在观察阶段,当前状态与所需状态匹配,因此无需任何操作计算和应用。你可以在下图中看到该流程的表示:
微信图片_20230516223335.png
图1.2-控制回路
在Kubernetes中,有许多控制器。我们有以下内容:

  • ReplicaSet :https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/

  • HorizontalPodAutoscaler (HPA):https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/

  • 其他的一些可以在这里找到,但这不是一个完整的列表:https://kubernetes.io/docs/concepts/workloads/controllers/

    ReplicaSet控制器负责运行固定数量的Pods。你通过kubectl创建了它,并要求运行三个实例,这就是所期望的状态。因此,它从检查当前状态开始:当前有多少Pods在运行;它计算要执行的操作:要启动或终止多少Pods才能拥有三个实例;然后执行这些操作。还有HPA控制器,根据一些指标,它能够增加或减少部署的Pods数量(部署是建立在Pods和ReplicaSets 之上的结构,允许我们定义更新Pods的方法https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)),而 Deployment 依赖于它内部构建的ReplicaSet控制器来更新Pods的数量。修改数量后,仍然是ReplicaSet控制器运行控制循环以达到所需Pods的数量。
    控制器的工作是确保实际状态与期望状态相匹配,并且它们永远不会停止尝试达到最终状态。而且,不仅如此,它们还专门处理资源类型——每个资源都负责集群中的一小个部分。
    在前面的示例中,我们讨论了内部Kubernetes控制器,但我们也可以编写自己的控制器,这就是Argo CD的真正含义——一个控制器,它的控制循环负责确保Git存储库中声明的状态与集群中的状态相匹配。实际上,正确地说,它不是一个控制器,而是一个操作符,区别在于控制器处理内部Kubernetes对象,而操作符处理两个域:Kubernetes和其他域。在我们的案例中,Git存储库是由操作员处理的外部部分,它使用称为自定义资源的东西来完成此操作,这是一种扩展Kubernetes功能的方式(https://kubernetes.io/docs/concepts/ extend-kubernetes/api-extension/custom-resources/)。
    到目前为止,我们已经了解了Kubernetes体系结构与API服务器连接所有组件,以及控制器如何始终在控制循环中工作,以使集群达到所需状态。接下来,我们将详细介绍如何定义所需状态:我们将从命令式方式开始,继续使用更重要的声明式方式,并展示所有这些如何使我们离GitOps更进一步。

1.4 命令式和声明式API

我们讨论了一点命令式风格和声明式风格之间的区别,命令式风格明确指定要采取的操作——比如启动三个以上的pods——而声明式风格则指定你的意图——比如部署中应该有三个正在运行的pods——并且需要计算操作(如果已经运行了三个pods,你可能会增加或减少pods,或者什么都不做)。命令式和声明式方法都会在Kubectl客户端中实现。

1.4.1 命令式——直接命令

无论我们何时创建、更新或删除Kubernetes对象时,我们都可以使用命令式的方式来完成。要创建命名空间,请运行以下命令:

kubectl create namespace test-imperative

然后,为了看到创建的命名空间,使用以下命令:

kubectl get namespace test-imperative

在该命名空间中创建一个部署,如下所示:

kubectl create deployment nginx-imperative --image=nginx -n 
test-imperative 

然后,你可以使用以下命令查看创建的部署:

kubectl get deployment -n test-imperative nginx-imperative

要更新我们创建的任何资源,我们可以使用特定的命令,例如kubectl label来修改资源标签,kubectl scale来修改Deployment、ReplicaSet、StatefulSet或kubectl set中的pods数量,或者Kubectl set用于更改环境变量(kubect1 set env)、容器映像(kubectl set image)、容器资源(kubect1 set resources)等。
如果你想给命名空间添加一个标签,你可以运用以下命令:

kubectl label namespace test-imperative namespace=imperative-apps

最后,你可以用以下命令删除之前创建的对象:

bectl delete deployment -n test-imperative nginx-imperative
kubectl delete namespace test-imperative

命令式命令很清楚它们的作用,当你将它们用于命名空间这样的小对象时,它是有意义的。但是对于更复杂的,比如部署,我们最终可以给它传递很多标志,比如指定一个容器镜像、镜像标签、拉取策略,如果一个秘密被链接到一个拉取(对于私有映像注册表),对于init容器和许多其他选项也是如此。接下来,让我们看看是否有更好的方法来处理这么多可能的标志。

1.4.2 命令式——配置文件

命令式命令还可以使用配置文件,这使事情变得更容易,因为它们大大减少了我们需要传递给命令式命令的标志数量。我们可以使用一个文件来说明我们想要创建什么。
这就是命名空间配置文件的样子——尽可能最简单的版本(没有任何标签或注释)。以下文件也可以在https://github.com/PacktPublishing/ArgoCD-in-Practice/tree/main/ch01/imperative-config中找到。
将以下内容复制到一个名为namespace.yaml的文件中。

apiVersion: v1
kind: Namespace
metadata:
 name: imperative-config-test

然后,执行以下命令:

 kubectl create -f namespace.yaml  

复制以下内容并保存到一个名为deployment.yaml的文件中:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx-deployment
 namespace: imperative-config-test
spec:
 selector:
 matchLabels:
 app: nginx
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx
 image: nginx

然后,执行以下命令:

kubectl create -f deployment.yaml

通过运行前面的命令,我们创建了一个命名空间和一个Deployment,类似于我们使用命令式直接命令所做的事情。你可以看到,这比将所有标志传递给kubectl create deployment要容易得多。更重要的是,并非所有字段都可作为标志使用,因此在许多情况下,使用配置文件可能会成为强制性的。
我们也可以通过配置文件修改对象。下面是一个如何向命名空间添加标签的示例。用以下内容更新我们之前使用的命名空间(注意以标签开头的额外两行)。更新后的命名空间可以在官方的https://github.com/PacktPublishing/ArgoCD-in-Practice/tree/main/ch01/imperative-config中看到,配置命名空间中的存储库-with-labels.yaml文件:

apiVersion: v1
kind: Namespace
metadata:
 name: imperative-config-test
 labels:
 name: imperative-config-test

然后,我们可以执行以下命令:

kubectl replace -f namespace.yaml

然后,要查看是否添加了标签,请执行以下命令:

kubectl get namespace imperative-config-test -o yaml

与将所有标志传递给命令相比,这是一个很好的改进,并且可以将这些文件存储在版本控制中,以供将来参考。但是,如果资源是新的,你需要指定你的意图,因此你使用kubectl create,而如果它存在,你使用kubectl replace也有一些限制:kubectl replace命令执行一个完整的对象更新,因此如果有人在两者之间修改了其他东西(例如在命名空间中添加注释),这些更改将丢失。

1.4.3 声明式——配置文件

我们刚刚看到了使用配置文件创建内容是多么容易,如果我们可以修改配置文件并在其中调用某个updat e/sync命令,那就太好了。我们可以修改文件中的标签,而不是使用kubectl标签,也可以对其他更改执行同样的操作,例如缩放部署的Pod、设置容器资源、容器镜像等。还有这样一个命令,你可以向它传递任何文件,无论是新的还是修改的,它将能够对API服务器做出正确的调整:kubectl apply。
请创建名为description-files的新文件夹,并将命名空间.yaml文件,内容如下(这些文件也可以在https://github.com/上找到。打包发布/ArgoCD-实践/tree/main/ch01/description-files):

apiVersion: v1 
kind: Namespace  
 metadata: 
    name: declarative-files 

然后,请执行以下命令:

kubectl apply -f declarative-files/namespace.yaml

控制台的输出应该是这样的:

namespace/declarative-files created

接下来,我们可以修改命名空间。yalm文件,并直接在文件中为其添加标签,如下所示:

apiVersion: v1
kind: Namespace
metadata:
 name: declarative-files
 labels:
 namespace: declarative-files

然后,再次执行以下命名:

kubectl apply -f declarative-files/namespace.yaml

控制台输出应该如下所示:

namespace/declarative-files configured

** **在上述两个案例中发生了什么?在运行任何命令之前,我们的客户端(或我们的服务器-本章将进一步说明何时使用客户端应用或服务器端应用)将集群中的现有状态与文件中的所需状态进行比较,从而能够计算为达到所需状态而需要应用的操作。在第一个应用示例中,它发现名称空间不存在,因此需要创建名称空间;而在第二个应用示例中,它发现名称空间存在,但没有标签,因此添加了一个标签。
接下来,让我们在它自己名为部署的文件中添加Deployment.yaml的文件在相同的声明式文件夹中,如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx
 namespace: declarative-files
spec:
 selector:
 matchLabels:
 app: nginx
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx
 image: nginx

我们将运行以下命令,在命名空间中创建一个部署:

 kubectl apply -f declarative-files/deployment.yaml  

如果你需要,你可以对部署进行更改。Yaml文件(标签、容器资源、映像、环境变量等),然后运行kubectl apply命令(完整的是前面的那个),你所做的更改将应用到集群上。

1.4.4 声明式—配置文件夹

在本节中,我们将创建一个名为declarative - folder的新文件夹,并在其中创建两个文件。
这是命名空间的内容。Yaml文件(代码也可以在这里找到:https://github.com/PacktPublishing/ArgoCD-in-Practice/tree/main/ch01/declarative-folder):

apiVersion: v1
kind: Namespace
metadata:
 name: declarative-folder

这是 deployment.yaml 文件夹的内容:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: nginx
 namespace: declarative-folder
spec:
 selector:
 matchLabels:
 app: nginx
 template:
 metadata:
 labels:
 app: nginx
 spec:
 containers:
 - name: nginx
 image: nginx

然后,我们将执行以下命令:

kubectl apply -f declarative-folder

最有可能的是,你会看到以下错误,这是意料之中的,所以不用担心:

namespace/declarative-folder created
Error from server (NotFound): error when creating "declarativefolder/deployment.yaml": namespaces "declarative-folder" not 
found

** **这是因为这两个资源是同时创建的,但是部署依赖于名称空间,所以当需要创建部署时,需要准备好名称空间。我们看到消息说创建了一个名称空间,但是API调用是同时在服务器上完成的,因此当部署启动其创建流时,名称空间不可用。我们可以通过再次运行以下命令来修复这个问题:

 kubectl apply -f declarative-folder  

在控制台中,我们应该看到以下输出:

deployment.apps/nginx created
namespace/declarative-folder unchanged

由于名称空间已经存在,因此可以在不更改名称空间的情况下在其中创建部署。
kubectl apply命令获取声明式文件夹的全部内容,对在这些文件中找到的每个资源进行计算,然后使用API服务器进行更改。我们可以应用整个文件夹,而不仅仅是文件,尽管如果资源相互依赖的话,我们可以修改这些文件并调用文件夹的apply命令,更改将得到应用。现在,如果这就是我们在集群中构建应用程序的方式,那么我们最好将所有这些文件保存在源代码管理中,以备将来参考,这样在一段时间后应用更改就会变得更容易。
但是,如果我们可以直接应用Git存储库,而不仅仅是文件夹和文件呢?毕竟,本地Git存储库就是一个文件夹,而最终,GitOps操作符就是这样的:一个知道如何使用Git存储库。
请注意
apply命令最初完全在客户端上实现。这意味着查找更改的逻辑是在客户端上运行,然后在服务器上调用特定的命令APIs。但最近,应用逻辑转移到了服务器端;所有对象都有一个apply方法(从REST API的角度来看,它是一个PATCH方法,带有一个application/apply-patch+yaml内容类型头),并且从版本1.16开始默认启用该功能(更多相关信息请访问:https://kubernetes.io/docs/reference/using-api/server-side-apply/)。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐