在本章中,我们将看看什么是GitOps,以及这个想法在 Kubernetes集群中如何有意义。我们将介绍特定的组件,例如应用程序编程接口(API)服务器和控制器管理器,它们可以使集群对状态更改做出反应。我们将从命令式API开始,然后浏览声明式API,并将看到如何应用文件和文件夹来应用Git存储库只是一个步骤——当执行它时,GitOps出现了。 我们将在本章中介绍以下主要主题:
在本章节,你需要访问一个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的文件夹中找到。
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的原则,目前已经确定了五个原则(这仍是一个草案),如下:
现在很难不听说Kubernetes—它可能是目前最知名的开源项目之一。它起源于2014年左右,当时谷歌的一群工程师开始根据他们与谷歌自己的名为Borg的内部协调器合作积累的经验来构建一个容器协调器。该项目于2014开源,并在2015年达到1.0.0版本,这是一个里程碑,鼓励许多公司仔细研究它。 另一个导致它被社区迅速且被狂热的采用的原因是CNCF的治理(https://www.cncf.io))。在使该项目开源之后,谷歌与Linux基金会 (https://www.linuxfoundation.org)讨论创建一个新的非营利组织,该组织将领导开源云原生技术的采用。当Kubernetes成为它的种子项目且KubeCon是它的主要开发者大会时,CNCF就是这样被创建的。当我说到CNCF的治理时,我主要指的是这样一个事实,即CNCF内部的每个项目或组织都有一个完善的维护者结构,并详细的说明了他们是如何被提名的,这些团队是如何做决定的,没有一家公司能拥有一个简单的多数。这确保了在没有社区参与的情况下不会做出任何决定,并确保整个社区在项目生命周期中扮演着重要的角色。
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体系结构的概述:
请注意: 当查看架构图时,你需要知道它只能捕捉到整个画面的一部分。例如,在这里,带有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很重要。
当阅读到有关Kubernetes的文章(或者听播客)时,你会经常听到controller这个词。它背后的想法来自工业自动化或机器人,它是关于转换控制回路的。 假设我们有一个机械臂,我们给它一个简单的命令,让它在90度的位置上移动。它要做的第一件事就是分析它的当前状态;或许它已经在90度了,没有什么可做的。如果它不在正确的位置上的话,接下来的事情就是计算到达那个位置而采取的行动,然后,它将尝试应用这些行动来到达它的相对位置。 我们从观察阶段开始,在该阶段,我们将所需状态与当前状态进行比较,然后是差异阶段,我们计算要应用的操作,在操作阶段,我们执行这些操作。同样,在我们执行操作之后,它将开始观察阶段,看看它是否位于正确的位置;如果没有(可能有什么东西阻止了它到达那里),就会计算操作,然后我们开始应用操作,以此类推,直到它到达某个位置或可能耗完电池或者其他什么的。这个控制循环一直持续下去,直到在观察阶段,当前状态与所需状态匹配,因此无需任何操作计算和应用。你可以在下图中看到该流程的表示:
图1.2-控制回路 在Kubernetes中,有许多控制器。我们有以下内容:
我们讨论了一点命令式风格和声明式风格之间的区别,命令式风格明确指定要采取的操作——比如启动三个以上的pods——而声明式风格则指定你的意图——比如部署中应该有三个正在运行的pods——并且需要计算操作(如果已经运行了三个pods,你可能会增加或减少pods,或者什么都不做)。命令式和声明式方法都会在Kubectl客户端中实现。
无论我们何时创建、更新或删除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容器和许多其他选项也是如此。接下来,让我们看看是否有更好的方法来处理这么多可能的标志。
命令式命令还可以使用配置文件,这使事情变得更容易,因为它们大大减少了我们需要传递给命令式命令的标志数量。我们可以使用一个文件来说明我们想要创建什么。 这就是命名空间配置文件的样子——尽可能最简单的版本(没有任何标签或注释)。以下文件也可以在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命令执行一个完整的对象更新,因此如果有人在两者之间修改了其他东西(例如在命名空间中添加注释),这些更改将丢失。
我们刚刚看到了使用配置文件创建内容是多么容易,如果我们可以修改配置文件并在其中调用某个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命令(完整的是前面的那个),你所做的更改将应用到集群上。
在本节中,我们将创建一个名为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/)。