我们说一下Kubernetes声明式API的工作原理,如何利用其本身的API机制,在Kubernetes中添加自定义的API对象

当我们将一个YAML文件提交给Kubernetes之后,如何创建出一个API对象呢?这就是声明式API的设计了

在Kubernetes项目中,一个API对象在Etcd中的完整资源路径是由 Group Version Resource三个部分组成的

这个结构构成了Kubernetes中的所有API对象,实际上可以形容出如下的树状结构

图片

Kubernetes中的API对象的组织形式,是层层递进的

比如,创建一个CronJob对象,那么可以声明为

apiVersion: batch.v2alpha1

kind: CronJob

batch是其组 v2alpha1是版本 cronjob是资源类型

那么,Kubernetes如何对Resouce Group Version进行解析,然后找到对应的定义的呢?

首先Kuberentes会匹配API对象的组

但是,需要注意的是,对于Kubernetes中的核心对象Pod Node这种,是不需要Group的,所以会直接在/api这个层级下进行下一步的匹配

但是对于其他的对象,需要现在/apis这个层级下找到对应的组

/apis/batch

这个Group的分类是根据对象功能进行区分的,Job和CronJob都属于batch这个Group,然后Kubernetes会进一步的匹配到API对象的版本号,在Kubernetes中,同一种API对象可能还有多个版本,可以是版本管理的重要手段

匹配到了版本后,Kuberentes根据资源认出了对应的API对象这样就可以继续创建这个CronJob对象了

图片

然后,当我们发起了创建CronJob的Post请求后,YAML信息就交给了APIServer

ApiServer会过滤这个请求,完成一些前置性的任务,授权 超时处理 审计等,然后进行诸如MUX和Routes的过程,用于完成APIServer的URL和Handler的绑定的场所

然后APIServer的主要工作来了,根据这个CronJob的类型定义,使用用户提交的YAML文件的字段,生成一个CronJob的对象

在这个过程中,APIServer会进行一个Convert的工作,将用户提交的YAML文件,转换为一个叫做Super Version的对象

这个Super version的创建需要两个函数 Admission()和Validation(),在之前说的Admission Controller和Initializer操作,都属于Admission的内容

然后是Validation函数,验证对象中各个字段是否合法,被验证过的API对象,被保存在了APIServer中的一个叫做Registry的数据结构中

这样,APIServer就会将验证过的API对象转换为用户最初提交后的版本,并且利用ETCD的API进行了保存

所以,声明式api是整个项目的重中之重,这是Kuberenets项目中的精髓

而为了兼顾性能,API完备性等,Kubernetes团队在项目中使用了大量GO的代码生成工作,自动化诸如Convert,DeepCopy等API资源相关的操作,这些自动生成的代码,一度占据Kubernetes总代吗的20%~30%

这也是为何一开始的时候,在这个APIServer中添加一个API资源类型,是一个困难的事情

不过在之后,就相对而言简单些了,因为一个全新的API插件机制CRD,Custom Reource Defintion,允许用户在K8S中添加定义一个全新的API资源类型,自定义API资源

接下来,我们先尝试添加一个名为Network的API资源类型

这个自定义的资源类型,主要功能是为用户调用真正的网络插件,比如Neutron项目,创建一个真正的网络,方便用户声明使用

这个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”

上面给出的组是samplecrd.k8s.io,version是v1,API资源类型是Network

那么Kubernetes如何知道这个API的存在的呢?

上面的YAML文件给出的定义,实际上就是一个具体的自定义API资源实例,也叫做CR Custom Reoucre 为了能够让这个Kubernetes认识这个CR,需要让Kubernetes能够明白这个CR的宏观定义,就是CRD

接下来,我们定义一个CRD

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,复数形式是networks

还有就是这个Networkd是一个属于Namespace的对象,类似Pod

这就定义好了Network API资源中关于API部分的宏观定义,就好比是告诉了K8S,兔子是什么动物,但是兔子长什么样,还没告诉K8S,所以还需要让K8S认识这种Yaml文件中描述的网络部分,cidr,gateway这些字段的含义,告诉计算机,兔子有红眼睛和三瓣嘴

这就需要用到GO语言

我们先在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的文件,用来放置后面的全局变量,基本上的定义就是Java的常量,文件内容如下

package samplecrd

const (

GroupName = “samplecrd.k8s.io”

Version   = “v1”

)

接下来,在pkg/apis/samplecrd目录添加一个doc.go文件,这个文件的内容如下

// +k8s:deepcopy-gen=package

// +groupName=samplecrd.k8s.io

package v1

在这个文件中,上面机上了 +<tag_name>[=value]格式的注释,这就是Kubernetes进行代码生成的注释\

其中+k8s:deepcopy-gen=package的意思是,为整个v1包里面所有类型定义生成DeepCopy方法;而+groupName=samplecrd.l8s.io,则定义了这个包对应的API组的名字

这些注释,起的是全局的代码生成的作用,所以也是Global Tags

然后需要添加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”`

}

上面分别定义了TypeMeta API元数据 ObjectMeta对象元数据字段

其中的spec字段,是需要我们自己定义的部分,在networkspec里,定义了cidr和gateway两个字段,每个字段后面的部分诸如json:”cidr”指的就是这个字段被转换为JSON格式后的名字,也就是YAML文件中的字段的名字

除了定义Network类型,还需要一个NetworkList类型,来描述一组Network对象应该包含哪些字段,之所以需要这样一个类型,是因为Kubernetes中获取到所有X对象的list()方法,获取的是list类型,而不是数组,不一样的

同样的,在Network和NetworkList上,也都有着相对应的代码生成注释

在Network的定义上,有着+genclient,这个意思是,为下面的API资源类型生成对应的Client,

而+genclient:noStatus意思是,这个API资源定义中,无Status字段,不然,会生成UpdateStatus方法

如果类型中定义了Status字段,就不需要这个注释了

而在Global Tags中定义了所有类型生成一个DeepCopy方法,这里就不显示的加上+k8s:deepcopy-gen=true,但是如果想要为这个类型跳过生成deepcopy=gen的方法生成,可以加上+k8s:deepcopy-gen=false

而在NetworkList和Network两个类型上,还有一句k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.object的注释,这个注释是为了在生成DeepCopy的时候,实现Kubernetes提供的runtime.Object的接口,不写可能出现编译报错

然后,再编写一个pkg/apis/samplecrd/v1/register.go文件

registry的作用是注册一个类型Type给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

}

这样,Kuberenetes就知道Network以及NetworkList类型的定义了

像这样register,go文件中的内容比较固定的,可以直接使用上面的代码作为模板,将其中的资源类型,GroupName和Version替换为自己的定义即可

这样定义就基本完事了

首先定义了自定义资源类型的API描述,包括组Group 版本Version 资源类型Resource,相当于告诉了计算机,兔子是哺乳动物

然后自定义资源类型的对象描述,包含Spec,Status等,告诉了计算机,兔子的具体模样

然后就是Kubernetes提供的代码生成工具,为上面的项目生成对应的clientset informer lister

clientset就是操作Network对象所需要使用的客户端

这个代码生成的工具名为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”

那么生成完成,就会在根目录下的pkg/client目录下生成对应包和文件

$ tree

.

├── controller.go

├── crd

│   └── network.yaml

├── example

│   └── example-network.yaml

├── main.go

└── pkg

├── apis

│   └── samplecrd

│       ├── constants.go

│       └── v1

│           ├── doc.go

│           ├── register.go

│           ├── types.go

│           └── zz_generated.deepcopy.go

└── client

├── clientset

├── informers

└── listers

其中 pkg/apis.samplecred/v1下面的zz_generatred.deepcopy.go,就是自动生成的DeepCopy代码

整个client目录,以及下面的三个包clientset informers listers,都是Kubernetes为Network类型生成的客户端库

在这个流程完事,就可以在Kubernetes集群中创建一个Network类型的API对象了

首先,我们先创建一个CRD的定义

使用了上面的CRD的yaml文件,在Kubernetes文件中创建了Network的CRD

$ kubectl apply -f crd/network.yaml

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

这样,就说明了这个对象的信息,我们可以通过kubectl get 命令,查看这个CRD

kubectl get crd

然后就可以创建一个Network对象,使用的就是example-network.yaml

利用上面定义的yaml文件

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

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

这样,我们就创建了一个Kubernetes集群中的Network对象,其API资源路径是

samplecrd.k8s.io/v1/networks

这样,就可以利用kubectl get命令,查看到新创建的Network对象

$ kubectl get network

NAME              AGE

example-network   8s

甚至可以使用kubectl describe命令,看到这个Network对象的细节

$ kubectl describe network example-network

Name:         example-network

Namespace:    default

Labels:       <none>

…API Version:  samplecrd.k8s.io/v1

Kind:         Network

Metadata:

Generation:          1

Resource Version:    468239

Spec:

Cidr:     192.168.0.0/16

Gateway:  192.168.0.1

这样,我们就说明了Kubernetes声明式API的工作原理,讲解如何遵循声明式API的设计,为Kubernetes添加一个名为Network的API资源类型,从而达到了通过标准的kubectl create和get操作,管理自定义API对象的目录,创建出这样一个自定义API的对象,只是完成了Kubernetes声明式API的一半工作,接下来,为这个API对象编写一个自定义控制器,这样这个API对象的增删改工作,才可以真正的有效

课后思考

CRD的定义了解了之后,是否考虑使用CRD来描述现实中的某种实体了呢?

扩展api server有两种方式,

1.通过创建CRDS,主API Server可以处理CRDs的REST请求和持久化存储,不需要其他的编程,适用于声明式的API,和kuberentes高度集成统一

2.API Aggregation,一个独立的API server,主API server委托独立的ApiServer去处理自定义的resource,需要进行编程,但是能够更加灵活的控制API的行为以及在不同API版本之间的切换,

自定义的resource可以使得更加方便的存取结构化的resource数据,但是只有和Controller组合在一起才是声明式API,声明式API可以定义一个期望的状态,Controller可以解毒结构化的resource数据,获得期望的状态,不断的协调期望状态和实际状态

综上,今天文档中的types.go应该是给controller来理解CRDs的schema用的,只有掌握了resource的schema,才能解释并得到用户创建的resource API object,用于接收REST请求,改变resource期望状态

发表评论

邮箱地址不会被公开。 必填项已用*标注