深入解析声明式API(一):API对象的奥秘
在这篇文章中,我们来学习一下 Kubernetes 声明式 API 的工作原理,以及如何利用这套 API 机制,在 Kubernetes 里添加自定义的 API 对象。
在 Kubernetes 项目中,一个 API 对象在 Etcd 里的完整资源路径,是由:Group(APi 组)、Version(API 版本)和 Resource(API 资源类型)三个部分组成的。
通过这样的结构,整个 Kubernetes 里的所有 API 对象,实际上就可以用如下的树形结构表现出来:
可以看到,Kubernetes 里 API 对象的组织方式,其实是层层递进的。
比如,现在我要声明创建一个 CronJob 对象,那么我的 YAML 文件的开始部分会这么写:
1 | apiVersion: batch/v2alpha1 |
在这个 YAML 文件中,“CronJob”就是这个 API 对象的 Resource,“batch”就是它的 Group,“v2alpha1”就是它的 Version。
当我们提交了这个 YAML 文件后,Kubernetes 就会把这个 YAML 文件里描述的内容,转化成 Kubernetes 里的一个 CronJob 对象。
那么,Kubernetes 是如何对 Resource、Group 和 Version 进行解析,从而在 Kubernetes 项目里找到 CronJob 对象的定义的呢?
首先,Kubernetes 会匹配 API 对象的组。
需要明确的是,对于 Kubernetes 里的核心对象,比如:Pod、Node 等,是不需要 Group 的(即:它们的 Group 是“”)。所以,对于这些 API 对象来说,Kubernetes 会直接在 /api 这个层级进行下一步的匹配过程。
而对于 CronJob 等非核心的 API 对象来说,Kubernetes 就必须在 /apis 这个层级里查找它对应的 Group,进而根据“batch”这个 Group 的名字,找到 /apis/batch。
不难发现,这些 API Group 的分类是以对象功能为依据的,比如 Job 和 CronJob 就都属于“batch”(离线业务)这个 Group。
然后,Kubernetes 会进一步匹配到 API 对象的版本号。
对于这个 CronJob 对象来说,Kubernetes 在 batch 这个 Group 下,匹配到的版本号就是 v2alpha1。
在 Kubernetes 中,同一种 API 对象可以有多个版本,这正是 Kubernetes 进行 API 版本化管理的重要手段。这样,比如在 CronJob 的开发过程中,对于会影响到用户的变更就可以通过升级新版本来处理,从而保证了向后兼容。
最后,Kubernetes 会匹配 API 对象的资源类型。
在前面匹配到正确的版本之后,Kubernetes 就知道,我要创建的原来是一个 /apis/batch/v2alpha1 下的 CronJob 对象。
这时候,APIServer 就可以继续创建这个 CronJob 对象了。可以通过以下这个流程图来表示整个创建过程:
首先,当我们发起了创建 CronJob 的 POST 请求之后,我们编写的 YAML 的信息就被提交给了 APIServer。而 APIServer 会对这个请求进行过滤,并完成一些前置性的工作,比如授权、超时处理、审计等。
然后,请求会进入 MUX 和 Routes 流程。这里就是按照上文中的过程进行匹配,找到对应的 CronJob 的类型定义。
接着,APIServer 最重要的职责就来了:根据这个 CronJob 类型定义,使用用户提交的 YAML 文件里的字段,创建一个 CronJob 对象。
在这个过程中,APIServer 会进行一个 Convert 操作,即:把用户提交的 YAML 文件,转换成一个叫做 Super Version 的对象,它正是该 API 资源类型所有版本的字段全集。这样用户提交的不同版本的 YAML 文件,就都可以用这个 Super Version 对象来进行处理了。
接下来,APIServer 会先后进行 Admission() 和 Validation() 操作。比如我们上一篇文章中提到的 Admission Controller 和 Initializer,就都属于 Admission 的内容。
而 Validation,则负责验证这个对象里的各个字段是否合法。这个被验证过的 API 对象,都保存在了 APIServer 里一个叫做 Registry 的数据结构中。也就是说,只要一个 API 对象的定义能在 Registry 里查到,它就是一个有效的 Kubernetes API 对象。
最后,APIServer 会把验证过的 API 对象转换成用户最初提交的版本,进行序列化操作,并调用 Etcd 的 API 把它保存起来。
由此可见,声明式 API 对于 Kubernetes 来说非常重要。所以,APIServer 这样一个在其他项目里“平平无奇”的组件,却成了 Kubernetes 项目的重中之重。
由于要同时兼顾性能、API 完整性、版本化、向后兼容等很多工程化指标,所以在过去很长一段时间里,在这样一个极其“复杂”的 APIServer 中,添加一个 Kubernetes 风格的 API 资源类型,是一个非常困难的工作。
不过,在 Kubernetes v1.7 之后,这个工作就变得轻松多了。这得益于一个全新的 API 插件机制:CRD(Custom Resource Definition)。它允许用户在 Kubernetes 中添加一个跟 Pod、Node 类似的、新的 API 资源类型,即:自定义 API 资源。
举个例子,我现在要为 Kubernetes 添加一个名叫 Network 的 API 资源类型。
它的作用是,一旦用户创建一个 Network 对象,那么 Kubernetes 就应该使用这个对象定义的网络参数,调用真实的网络插件,为用户创建一个真正的“网络”。这样,将来用户创建的 Pod,就可以声明使用这个“网络”了。
这个 Network 对象的 YAML 文件,叫做 example-network.yaml,它的内容如下所示:
1 | apiVersion: samplecrd.k8s.io/v1 |
可以看到,我想要描述“网络”的 API 资源类型是 Network;API 组是 samplecrd.k8s.io
;版本是 v1。
那么,Kubernetes 又该如何知道这个 API(samplecrd.k8s.io/v1/network)的存在呢?
其实,上面的这个 YAML 文件,就是一个具体的“自定义 API 资源”实例,也叫 CR(Custom Resource)。而为了能够让 Kubernetes 认识这个 CR,你就需要让 Kubernetes 明白这个 CR 的宏观定义是什么,也就是 CRD(Custom Resource Definition)。
所以接下来,我就先编写一个 CRD 的 YAML 文件,它的名字叫做 network.yaml,内容如下所示:
1 | apiVersion: apiextensions.k8s.io/v1beta1 |
可以看到,在这个 CRD 中,我指定了”group: samplecrd.k8s.io” “version: v1”这样的 API 信息,也指定了这个 CR 的资源类型叫做 Network,复数是 networks。
然后我还声明了它的 scope 是 Namespaced,即:我们定义的这个 Network 是一个属于 Namespace 的对象,类似于 Pod。
接下来,我还需要让 Kubernetes “认识”这种 YAML 文件里描述的“网络”部分,比如“cidr”(网段),“gateway”(网关)这些字段的含义。
这时候,就需要稍微做些代码工作了。
首先,要在 GOPATH 下,创建一个结构如下的项目:
1 | $ tree $GOPATH/src/github.com/<your-name>/k8s-controller-custom-resource |
其中,pkg/apis/samplecrd 就是 API 组的名字,v1 是版本,而 v1 下面的 types.go 文件里,则定义了 Network 对象的完整描述。
然后,在 pkg/apis/samplecrd 目录下创建了一个 register.go 文件,用来放置后面要用到的全局变量。这个文件的内容如下所示:
1 | package samplecrd |
接着,我需要在 pkg/apis/samplecrd 目录下添加一个 doc.go 文件。这个文件里的内容如下所示:
1 | // +k8s:deepcopy-gen=package |
在这个文件中,存在 +<tag_name>[=value]
格式的注释,这是 Kubernetes 进行代码生成要用的 Annotation 风格的注释。
其中,+k8s:deepcopy-gen=package
的意思是,为整个 v1 包里的所有类型定义自动生成 DeepCopy 方法;而 +groupName=samplecrd.k8s.io
,则定义了这个包对应的 API 组的名字。
可以看到,这些定义在 doc.go 文件的注释,起到的是全局的代码生成控制的作用,所以也被称为 Global Tags。
接下来,我需要添加 types.go 文件。它的作用就是定义一个 Network 类型到底有哪些字段(比如,spec 字段里的内容)。这个文件的主要内容如下所示:
1 | package v1 |
在上面这部分代码里,你可以看到 Network 类型定义方法跟标准的 Kubernetes 对象一样,都包括了 TypeMeta(API 元数据)和 ObjectMeta(对象元数据)字段。
而其中的 Spec 字段,就是需要我们自己定义的步伐,所以在 networkspec 里,我定义了 Cidr 和 Gateway 两个字段。
此外,除了定义 Network 类型,你还需要定义一个 NetworkList 类型,用来描述一组 Network 对象应该包括哪些字段。之所以需要这样一个类型,是因为在 Kubernetes 中,获取所有 X 对象的 List() 方法,返回值都是 List 类型,而不是 X 类型的数组。
同样地,在 Network 和 NetworkList 类型上,也有代码生成注释。其中,+genclient
的意思是:请为下面这个 API 资源类型生成对应的 Client 代码。而 +genclient:noStatus
的意思是:这个 API 资源类型定义里,没有 Status 字段。否则,生成的 Client 就会自动被带上 UpdateStatus 方法。
如果你的类型定义包括了 Status 字段的话,就不需要这句 +genclient:noStatus
注释了。比如:
1 | // +genclient |
需要注意的是,+genclient
只需要写在 Network 类型上,而不用写在 NetworkList 上。因为 NetworkList 只是一个返回值类型,Network 才是“主类型”。
而由于我在 Global Tags 中已经定义了为所有类型生成 DeepCopy 方法,所以这里就不需要再显式地加上 +k8s:deepcopy-gen=true
了。当然,这也就意味着你可以用 +k8s:deepcopy-gen=false
来阻止为某些类型生成 DeepCopy。
你可能还注意到,在这两个类型上面还有一句 +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
的注释。它的意思是,请在生成 DeepCopy 的时候,实现 Kubernetes 提供的 runtime.Object
接口。否则,在某些特定版本的 Kubernetes 里,你的这个类型定义会出现编译错误。这是一个固定的操作,记住即可。
最后,我需要再编写一个 pkg/apis/samplecrd/v1/register.go 文件。
在前面对 APIServer 工作原理的讲解中,我已经提到,“register”的作用就是注册一个类型给 APIServer。其中,Network 资源类型在服务器端注册的工作,APIServer 会自动帮我们完成。但与之对应的,我们还需要让客户端也能“知道” Network 资源类型的定义。这就需要我们在项目里添加一个 register.go 文件。它最重要的功能,就是定义了如下所示的 addKnownTypes()
方法:
1 | package v1 |
有了这个方法,Kubernetes 就能够在后面生成客户端的时候,“知道” Network 以及 NetworkList 类型的定义了。
像上面这种 register.go 文件里的内容其实是非常固定的,后面可以直接用这部分代码做模版,替换其中的定义即可。
这样,Network 对象的定义工作就全部完成了。可以看到,它其实定义了两部分内容:
- 第一部分是,自定义资源类型的 API 描述,包括:组(Group)、版本(Version)、资源类型(Resource)等。
- 第二部分是,自定义资源类型的对象描述,包括:Spec、Status 等。
接下来,就可以使用 Kubernetes 提供的代码生成工具,为上面定义的 Network 资源类型自动生成 clientset、informer 和 lister。其中,clientset 就是操作 Network 对象所需要使用的客户端,而 informer 和 lister 这两个包的主要功能,会在后面的文章中讲解。
这个代码生成工具名叫 k8s.io/code-generator
,使用方法如下所示:
1 | # 代码生成的工作目录,也就是我们的项目路径 |
代码生成工作完成之后,我们再查看一下这个项目的目录结构:
1 | $ tree |
其中,pkg/apis/v1 下面的 zz_generated.deepcopy.go 文件,就是自动生成的 DeepCopy 代码文件。
而整个 client 目录,以及下面的三个包(clientset、informers、listers)都是 Kubernetes 为 Network 类型生成的客户端库,这些库会在后面编写自定义控制器的时候用到。
有了这些内容,就可以在 Kubernetes 集群里创建一个 Network 类型的 API 对象了。
首先,使用 network.yaml 文件,在 Kubernetes 中创建 Network 对象的 CRD(Custom Resource Definition):
1 | $ kubectl apply -f crd/network.yaml |
然后,我们就可以创建一个 Network 对象了:
1 | $ kubectl apply -f example/example-network.yaml |
通过这个操作,你就在 Kubernetes 集群里创建了一个 Network 对象。它的 API 资源路径是 samplecrd.k8s.io/v1/networks
。
总结
在这篇文章中,解析了 Kubernetes 声明式 API 的工作原理,讲解了如何遵循声明式 API 的设计,为 Kubernetes 添加一个名叫 Network 的 API 资源类型。
不过,创建出这样一个自定义 API 对象,我们只是完成了 Kubernetes 声明式 API 的一半工作。
接下来的另一半工作是:为这个 API 对象编写一个自定义控制器。 这样,Kubernetes 才能根据 Network API 对象的“增、删、改”操作,在真实环境中做出相应的响应。
而这,正是 Network 这个 API 对象所关注的“业务逻辑”。