在Rust中编写Kubernetes CRD控制器

2021-01-10 13:17:37

在本文中,我们将定义一个Kubernetes自定义资源定义(CRD),然后编写一个控制器(或运算符)来管理它-全部用60行Rust代码编写。

在过去的几个月中,我一直在Rust中编写越来越多的Kubernetes特定代码。尽管Kubernetes本身是用Go编写的,但我发现我通常可以在Rust中编写更简洁,可读性和稳定性更高的Kubernetes代码。例如,我最近在Rust和Go中编写了功能等效的CRD控制器。 Go版本超过1700行,并装有样板和自动生成的代码。 Rust版本只有127行。它更容易理解和调试……而且绝对更快地编写。在这里,我们仅用60行就可以写一个。

您应该拥有最新的稳定Rust版本。您还需要配置为指向现有Kubernetes集群的kubectl。

控制器通常在Kubernetes集群内部作为守护进程运行。因此,我们将创建一个新的Rust程序(与库相对)。我们的目的是提供用于编写​​控制器的基本模型,因此我们不会花时间将事物分解为模块。我们还将介绍构建Rust Docker映像或创建Deployment来运行控制器的内容。所有这些都在其他地方有据可查。

在开始编写代码之前,让我们创建两个YAML文件。第一个是我们的CRD定义,第二个是该CRD的实例。我们将在k8s-controller /中创建一个名为docs /的目录,并将我们的YAML文件放在此处。

apiVersion:apiextensions.k8s.io/v1beta1种类:CustomResourceDefinition元数据:名称:books.example.technosophos.com规格:组:example.technosophos.com版本:-名称:v1服务:真实存储:真实范围:命名空间名称:复数:单书:书的种类:书

单步执行此文件超出了本教程的范围,但是您可以在官方文档中了解有关此文件格式的所有信息。 (Kubernetes的最新版本在定义中添加了更多字段,但我们将坚持使用基本版本。)CRD只是一个清单,用于声明新的资源类型并表示与此新资源关联的名称。类型。我们的全名是books.example.technosophos.com/v1。

$ kubectl创建-f docs / crd.yamlcustomresourcedefinition.apiextensions.k8s.io" books.example.technosophos.com"创建了$ kubectl create -f docs / book.yamlbook.example.technosophos.com" moby-dick"创建了$ kubectl删除书moby-dickbook.example.technosophos.com" moby-dick"已删除

我们不会像现在那样向我们的Cargo.toml文件中逐步添加依赖关系,而是现在要设置所有依赖关系。随着本文的进展,我们将看到如何使用它们。

[package] name =" k8s-controller"版本=" 0.1.0"版本=" 2018" [依赖关系] kube =" 0.14.0" serde =" 1.0" serde_derive =" 1.0" serde_json =" 1.0"

serde序列化库可能已经为您所熟悉。而kube是用于编写控制器的Kubernetes库。 (另一个库k8s_openapi对于处理现有的Kubernetes资源类型很有用,但是我们在这里不需要它)

我们将编写的第一段代码是代表我们的书CRD的结构。最简单的方法是编写定义主体(规范)的基本结构。在book.yaml中,规范中有两个字段:

由于我们只是在编写一个简单的示例,因此我们将继续在main.rs中创建此结构:

#[macro_use] extern crate serde_derive; //这是我们的新Book struct#[derive(Serialize,Deserialize,Clone,Debug)] pub struct Book {pub title:String,pub authors:Option< Vec<字符串>> ,} //这是Cargo生成的样板:fn main(){println! ("你好,世界!"); }

通过将标题设置为字符串,将作者设置为选项,我们声明标题是必需的,但作者不是必需的。现在我们有了:

我们还使用了宏来生成Serde序列化器和解串器功能以及克隆和调试支持。

如果再次查看book.yaml,我们将看到本书的正文有两个部分:

一些Kubernetes对象的第三部分称为状态。我们不需要其中之一。

kube库知道此元数据/规范/状态模式。因此,它提供了一种称为kube :: api :: Object的泛型类型,可用于创建Kubernetes风格的资源。为了使我们的代码更易于阅读,我们将为此新资源类型创建类型别名:

//描述具有Book规格且无状态类型的Kubernetes对象KubeBook = Object<书本,虚空> ;

cube :: api :: Object已经定义了元数据部分。但这使我们可以选择添加自己的规格和状态字段。我们将Book添加为规格,但是我们不需要状态字段,因此将其设置为Void。

#[macro_use] extern crate serde_derive;使用kube :: api :: {Object,Void}; #[derive(Serialize,Deserialize,Clone,Debug)] pub struct Book {pub title:String,pub authors:Option< Vec<字符串>> ,} //这是一个方便的别名,用于描述我们从Kubernetes获取的对象,类型为KubeBook = Object<书本,虚空> ; fn main(){println! ("你好,世界!"); }

接下来,我们将在main()函数中创建控制器。我们将分几个步骤进行。首先,让我们加载使用Kubernetes所需的所有信息。

#[macro_use] extern crate serde_derive;使用kube :: {api :: {Object,Void,RawApi},client :: APIClient,config,}; #[derive(Serialize,Deserialize,Clone,Debug)] pub struct Book {pub title:String,pub authors:Option< Vec<字符串>> ,} //这是一个方便的别名,用于描述我们从Kubernetes获取的对象,类型为KubeBook = Object<书本,虚空> ; fn main(){//加载kubeconfig文件。让kubeconfig = config :: load_kube_config().expect(" kubeconfig无法加载"); //创建一个新的客户端let client = APIClient :: new(kubeconfig); //设置名称空间。我们现在只是进行硬编码。让命名空间=" default" ; //描述我们正在使用的CRD。 //这基本上是我们CRD定义中的字段。 let resource = RawApi :: customResource(" books").group(" example.technosophos.com").within(& namespace); }

如果我们运行此程序,它将不会做任何可见的事情。但是,这是在main()函数中发生的事情:

首先,我们加载kubeconfig文件(或者在集群中,从卷装载中读取机密信息)。这会将URL加载到Kubernetes API服务器,并加载用于身份验证的凭据。

其次,我们创建一个新的API客户端。这是将与Kubernetes API服务器通信的对象。

第三,我们设置名称空间。 Kubernetes按名称空间分割对象。在普通程序中,我们为用户提供了一种指定特定名称空间的方法。但是为此,我们将仅使用默认的内置名称空间。

第四,我们正在创建描述CRD的资源。我们将用一点时间来告诉告密者它应该注意什么。

因此,现在我们有足够的信息来针对特定的名称空间针对Kubernetes API服务器运行操作,并注意特定的CRD。

用Kubernetes的话来说,告密者是一种特殊的代理,它监视Kubernetes事件流,并在特定种类的资源触发事件时通知程序。这是我们控制者的核心。

还有第二种监视代理程序,用于保留与类型匹配的所有对象的本地缓存。这就是所谓的反射器。

在我们的案例中,我们将编写一个告密者,该告密者会告诉我们任何时候书籍发生任何变化。

这是创建通知程序,然后处理事件进入时的代码:

#[macro_use] extern crate serde_derive;使用kube :: {api :: {Object,RawApi,Informer,W​​atchEvent,Void},client :: APIClient,config,}; #[derive(Serialize,Deserialize,Clone,Debug)] pub struct Book {pub title:String,pub authors:Option< Vec<字符串>> ,} //这是一个方便的别名,用于描述我们从Kubernetes获取的对象,类型为KubeBook = Object<书本,虚空> ; fn main(){//加载kubeconfig文件。让kubeconfig = config :: load_kube_config().expect(" kubeconfig无法加载"); //创建一个新的客户端let client = APIClient :: new(kubeconfig); //设置名称空间。我们现在只是进行硬编码。让命名空间=" default" ; //描述我们正在使用的CRD。 //这基本上是我们CRD定义中的字段。 let resource = RawApi :: customResource(" books").group(" example.technosophos.com").within(& namespace); //创建我们的告密者并开始收听。让notifyer = Informer :: raw(client,resource).init().expect(" informer init failed");循环{notifyer .poll().expect(" informer poll failed"); //现在,每次触发新书事件时,我们都要做一些事情。而让Some(event)= notifyer .pop(){handle(event); }}} fn句柄(事件:WatchEvent< KubeBook>){println! ("书中发生了什么事")}

该行创建了一个原始的告密者。原始告密者是不使用Kubernetes OpenAPI规范对其内容进行解码的告密者。由于我们使用的是自定义CRD,因此我们不需要OpenAPI规范。请注意,我们为通知者提供了两条信息:

基于这些信息,我们的通知者现在将连接到API服务器,并监视与Book CRD有关的任何事件。接下来,我们只需要告诉它继续监听新事件即可:

循环{notifyer .poll().expect(" informer poll failed"); //现在,每次触发新书事件时,我们都要做一些事情。而让Some(event)= notifyer .pop(){handle(event); }}

上面告诉告密者轮询API服务器。每次有新事件排队时,pop()都会将事件从队列中移出并进行处理。现在,我们的handle()方法令人印象深刻:

稍后,我们将向handle()添加一些功能,但是首先让我们看看如果运行此代码会发生什么。

确保您的本地环境指向Kubernetes集群!否则,货物运行和kubectl命令都将无效。并确保您安装了docs / crd.yaml。

在7.28秒内完成dev [unoptimized + debuginfo]目标运行`target / debug / k8s-controller`书中发生了某事书中发生了某事

在最后一部分中,我们将向handle()函数添加更多内容。这是我们修改后的功能:

fn handle(event:WatchEvent< KubeBook>){//每当匹配事件{WatchEvent ::添加的(book)=> {println! ("添加了书名{}的书{},书.metadata.name,书.spec.title)},WatchEvent :: Deleted(book)=&gt ; {println! ("删除了图书{}",图书.metadata.name)} _ => {println! ("另一个事件")}}}

注意,函数签名表示它接受事件:WatchEvent< KubeBook>。通知程序发出WatchEvent对象,该对象描述了它在Kubernetes事件流上看到的事件。创建告密者时,我们告诉它要注意描述我们Book CRD的资源。

因此,每次发出WatchEvent时,它都会包装一个KubeBook对象。该对象将代表我们之前的YAML定义:

因此,我们希望KubeBook将具有诸如book.metadata.name或book.spec.title之类的字段。实际上,我们之前的Book结构的所有属性都可以在book.spec中获得。

在上面的代码中,我们使用match事件匹配其中一个事件。我们显式处理添加和删除,但使用通用_匹配捕获其他对象。

为了更仔细地观察,在第一个匹配项中,我们只需打印出书对象的名称和书名:

如果执行货物运行,然后再次运行kubectl create和kubectl delete命令,这将在货物运行输出中看到:

$ cargo run编译k8s-controller v0.1.0(/ Users / technosophos / Code / Rust / k8s-controller)在5.33s中完成dev [unoptimized + debuginfo]目标运行`target / debug / k8s-controller`已添加标题为Moby Dick的书Moby-Dick,删除了书Moby-Dick

从这里开始,我们可能想对我们的告密者做一些更复杂的事情。或者,我们可能想尝试使用反射器。但是仅用60行代码,我们就编写了带有自定义资源定义的整个Kubernetes控制器!

这就是创建基本控制器的全部。与用Go编写这些代码不同,您不需要特殊的代码生成器或注释,样板代码的小样以及复杂的配置。这是创建新的Kubernetes控制器的快速有效的方法。

从这里开始,您可能需要仔细阅读kube库的文档。有数十个示例,并且API本身已有充分的文档记录。您还将学习如何使用内置的Kubernetes类型(这也是一件容易的事)。