深入解析声明式 API 之 API 对象的奥秘

你可能一直就很好奇:当我把一个 YAML 文件提交给 Kubernetes 之后,它究竟是如何创建出一个 API 对象的呢?

这得从声明式 API 的设计谈起了。在 Kubernetes 项目中,一个 API 对象在 Etcd 里的完整资源路径,是由:Group(API 组)、Version(API 版本)和 Resource(API 资源类型)三个部分组成的。

Kubernetes 里 API 对象的组织方式,其实是层层递进的。比如,现在我要声明要创建一个 CronJob 对象,那么我的 YAML 文件的开始部分会这么写:

apiVersion: batch/v2alpha1

    kind: CronJob...

在这个 YAML 文件中,“CronJob”就是这个 API 对象的资源类型(Resource),“batch”就是它的组(Group),v2alpha1 就是它的版本(Version)。

当我们提交了这个 YAML 文件之后,Kubernetes 就会把这个 YAML 文件里描述的内容,转换成 Kubernetes 里的一个 CronJob 对象。

那么,Kubernetes 是如何对 Resource、Group 和 Version 进行解析,从而在 Kubernetes 项目里找到 CronJob 对象的定义呢?

首先,Kubernetes 会匹配 API 对象的组。

需要明确的是,对于 Kubernetes 里的核心 API 对象,比如: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 这个 API 对象来说,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 流程。如果你编写过 Web Server 的话就会知道,MUX 和 Routes 是 APIServer 完成 URL 和 Handler 绑定的场所。而 APIServer 的 Handler 要做的事情,就是按照我刚刚介绍的匹配过程,找到对应的 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 项目的重中之重。

CRD 的全称是 Custom Resource Definition。顾名思义,它指的就是,允许用户在 Kubernetes 中添加一个跟 Pod、Node 类似的、新的 API 资源类型,即:自定义 API 资源。

举个例子,我现在要为 Kubernetes 添加一个名叫 Network 的 API 资源类型。它的作用是,一旦用户创建一个 Network 对象,那么 Kubernetes 就应该使用这个对象定义的网络参数,调用真实的网络插件,比如 Neutron 项目,为用户创建一个真正的“网络”。这样,将来用户创建的 Pod,就可以声明使用这个“网络”了。

这个 Network 对象的 YAML 文件,名叫 example-network.yaml,它的内容如下所示:

apiVersion: samplecrd.k8s.io/v1

kind: Network

metadata:

  name: example-network

spec:

  cidr: "192.168.0.0/16"

  gateway: "192.168.0.1"

可以看到,我想要描述“网络”的 API 资源类型是 Network;API 组是samplecrd.k8s.io;API 版本是 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,内容如下所示:

apiVersion: apiextensions.k8s.io/v1beta1

kind: CustomResourceDefinition

metadata:

  name: networks.samplecrd.k8s.io

spec:

  group: samplecrd.k8s.io

  version: v1

  names:

    kind: Network

    plural: networks

  scope: Namespaced

可以看到,在这个 CRD 中,我指定了“group: samplecrd.k8s.io”“version: v1”这样的 API 信息,也指定了这个 CR 的资源类型叫作 Network,复数(plural)是 networks。然后,我还声明了它的 scope 是 Namespaced,即:我们定义的这个 Network 是一个属于 Namespace 的对象,类似于 Pod。这就是一个 Network API 资源类型的 API 部分的宏观定义了。

接下来,我还需要让 Kubernetes“认识”这种 YAML 文件里描述的“网络”部分,比如“cidr”(网段),“gateway”(网关)这些字段的含义。

首先,我要在 GOPATH 下,创建一个结构如下的项目:

$ tree $GOPATH/src/github.com/<your-name>/k8s-controller-custom-resource

.

├── controller.go

├── crd

│  └── network.yaml

├── example

│  └── example-network.yaml

├── main.go

└── pkg

    └── apis

        └── samplecrd

            ├── register.go

            └── v1

                ├── doc.go

                ├── register.go

                └── types.go

其中,pkg/apis/samplecrd 就是 API 组的名字,v1 是版本,而 v1 下面的 types.go 文件里,则定义了 Network 对象的完整描述。

然后,我在 pkg/apis/samplecrd 目录下创建了一个 register.go 文件,用来放置后面要用到的全局变量。

package samplecrd

const (

GroupName = "samplecrd.k8s.io"

Version  = "v1"

)

接着,我需要在 pkg/apis/samplecrd 目录下添加一个 doc.go 文件(Golang 的文档源文件)。

// +k8s:deepcopy-gen=package

// +groupName=samplecrd.k8s.io

package v1

在这个文件中,你会看到 +<tag_name>[=value]格式的注释,这就是 Kubernetes 进行代码生成要用的 Annotation 风格的注释。其中,+k8s:deepcopy-gen=package 意思是,请为整个 v1 包里的所有类型定义自动生成 DeepCopy 方法;而+groupName=samplecrd.k8s.io,则定义了这个包对应的 API 组的名字。

接下来,我需要添加 types.go 文件。顾名思义,它的作用就是定义一个 Network 类型到底有哪些字段(比如,spec 字段里的内容)。

package v1

...

// +genclient

// +genclient:noStatus

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Network describes a Network resource

type Network struct {

// TypeMeta is the metadata for the resource, like kind and apiversion

metav1.TypeMeta `json:",inline"`

// ObjectMeta contains the metadata for the particular object, including

// things like...

//  - name

//  - namespace

//  - self link

//  - labels

//  - ... etc ...

metav1.ObjectMeta `json:"metadata,omitempty"`

Spec networkspec `json:"spec"`

}

// networkspec is the spec for a Network resource

type networkspec struct {

Cidr    string `json:"cidr"`

Gateway string `json:"gateway"`

}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// NetworkList is a list of Network resources

type NetworkList struct {

metav1.TypeMeta `json:",inline"`

metav1.ListMeta `json:"metadata"`

Items []Network `json:"items"`

}

在上面这部分代码里,你可以看到 Network 类型定义方法跟标准的 Kubernetes 对象一样,都包括了 TypeMeta(API 元数据)和 ObjectMeta(对象元数据)字段。而其中的 Spec 字段,就是需要我们自己定义的部分。所以,在 networkspec 里,我定义了 Cidr 和 Gateway 两个字段。其中,每个字段最后面的部分比如json:"cidr",指的就是这个字段被转换成 JSON 格式之后的名字,也就是 YAML 文件里的字段名字。

此外,除了定义 Network 类型,你还需要定义一个 NetworkList 类型,用来描述一组 Network 对象应该包括哪些字段。之所以需要这样一个类型,是因为在 Kubernetes 中,获取所有 X 对象的 List() 方法,返回值都是List 类型,而不是 X 类型的数组。这是不一样的。

需要注意的是,+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 工作原理的讲解中,我已经提到,“registry”的作用就是注册一个类型(Type)给 APIServer。其中,Network 资源类型在服务器端注册的工作,APIServer 会自动帮我们完成。但与之对应的,我们还需要让客户端也能“知道”Network 资源类型的定义。这就需要我们在项目里添加一个 register.go 文件。

它最主要的功能,就是定义了如下所示的 addKnownTypes() 方法:

package v1

...

// addKnownTypes adds our types to the API scheme by registering

// Network and NetworkList

func addKnownTypes(scheme *runtime.Scheme) error {

scheme.AddKnownTypes(

  SchemeGroupVersion,

  &Network{},

  &NetworkList{},

)

// register the type in the scheme

metav1.AddToGroupVersion(scheme, SchemeGroupVersion)

return nil

}

有了这个方法,Kubernetes 就能够在后面生成客户端的时候,“知道”Network 以及 NetworkList 类型的定义了。像上面这种 register.go 文件里的内容其实是非常固定的,你以后可以直接使用我提供的这部分代码做模板,然后把其中的资源类型、GroupName 和 Version 替换成你自己的定义即可。这样,Network 对象的定义工作就全部完成了。可以看到,它其实定义了两部分内容:

第一部分是,自定义资源类型的 API 描述,包括:组(Group)、版本(Version)、资源类型(Resource)等。这相当于告诉了计算机:兔子是哺乳动物。

第二部分是,自定义资源类型的对象描述,包括:Spec、Status 等。这相当于告诉了计算机:兔子有长耳朵和三瓣嘴。

接下来,我就要使用 Kubernetes 提供的代码生成工具,为上面定义的 Network 资源类型自动生成 clientset、informer 和 lister。其中,clientset 就是操作 Network 对象所需要使用的客户端,而 informer 和 lister 这两个包的主要功能,我会在下一篇文章中重点讲解。这个代码生成工具名叫k8s.io/code-generator,使用方法如下所示:

# 代码生成的工作目录,也就是我们的项目路径

$ ROOT_PACKAGE="github.com/resouer/k8s-controller-custom-resource"

# API Group

$ CUSTOM_RESOURCE_NAME="samplecrd"

# API Version

$ CUSTOM_RESOURCE_VERSION="v1"

# 安装k8s.io/code-generator

$ go get -u k8s.io/code-generator/...

$ cd $GOPATH/src/k8s.io/code-generator

# 执行代码自动生成,其中pkg/client是生成目标目录,pkg/apis是类型定义目录

$ ./generate-groups.sh all "$ROOT_PACKAGE/pkg/client" "$ROOT_PACKAGE/pkg/apis" "$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"

而有了这些内容,现在你就可以在 Kubernetes 集群里创建一个 Network 类型的 API 对象了。

首先,使用 network.yaml 文件,在 Kubernetes 中创建 Network 对象的 CRD(Custom Resource Definition):

$ kubectl apply -f crd/network.yaml

customresourcedefinition.apiextensions.k8s.io/networks.samplecrd.k8s.io created

然后,我们就可以创建一个 Network 对象了,这里用到的是 example-network.yaml:

$ kubectl apply -f example/example-network.yaml 

network.samplecrd.k8s.io/example-network created

通过这个操作,你就在 Kubernetes 集群里创建了一个 Network 对象。它的 API 资源路径是samplecrd.k8s.io/v1/networks。

此文章为3月Day15学习笔记,内容来源于极客时间《深入剖析 Kubernetes》,强烈推荐该课程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容