k8s使用声明式API的设计,那么将一个YAML文件提交给k8s之后,是如何创建出一个API对象的? 首先了解一下声明式API的设计。

声明式API的设计

在k8s中,一个API对象在Etcd中的完整资源路径是由:Group(API组)、Version(API版本)和Resource(API资源类型) 三个部分组成的。 一个k8s中API对象可以用一个树形结构表示:

例如下面的YAML:

apiVersion: batch/v2alpha1
kind: CronJob
...

CronJob是资源类型,batch是组,v2alpha1是版本。

提交之后,k8s就会把这个YAML描述的内容转化为一个CronJob对象。

k8s如何查找对象

1. 匹配API对象的组

首先要明确的是,k8s中的核心对象,例如 Pod、Node等是不需要组的(group为""),这些API对象会直接在/api这个层级往下找

非核心对象必须在/apis层级往下找对应的组。例如组为batch,那么就要找到/apis/batch

2. 匹配API对象版本号

找到了组之后,就继续查找匹配版本号,这里版本号是v2alpha1,那么就找到/apis/batch/v2alpha1。 同一对象可以有多个版本,这也是k8s进行API版本化管理的重要手段。对于会影响到用户的变更就可以通过升级新版本来处理,从而保证了向后兼容。

3. 匹配API对象的资源类型

现在就可以在/apis/batch/v2alpha1下创建指定类型的资源对象了。 流程如下:

  1. 发起创建CrobJob的POST请求后,编写的YAML提交给了APIServer。APIServer首先要做的就是过滤请求,并完成前置性的工作,比如授权、超时处理、审计等。
  2. 进入MUX和Routes流程。按照前面将的匹配过程,找到对应的CronJob类型定义。
  3. 根据CrobJob类型定义和用户提交的YAML文件中的字段,创建一个CronJob对象。而在这个过程中,APIServer 会进行一个 Convert 工作,即:把用户提交的 YAML 文件,转换成一个叫作 Super Version 的对象,这是这个资源类型所有版本的字段合集,通过Super Version可以对所有版本同一操作。
  4. 接下来先后进行Admission()和Validate()操作。在声明式API与k8s编程范式中的Admission Control和initializer就是Admission()中的内容。Validate就是验证字段是否合法,验证过的都保存在一个叫做Registry结构中。
  5. APIServer将验证过的API对象转换成用户最初提交的版本,进行序列化操作并调用Etcd保存。

自定义API资源

APIServer是k8s项目的重中之重,使用了很多Go语言的代码生成功能来自动化部分操作。在1.7版本之后,得益于一个API插件:CRD(Custom Resource Definition),用户可以在k8s中添加一个与Pod、Node类似的新的API资源类型。

编写CRD的YAML文件

要像创建一个自定义资源,那么就需要想让k8s明白自定义的资源是什么。

比如想要创建一个Network的自定义资源,

apiVersion: samplecrd.k8s.io/v1
kind: Network
metadata:
  name: example-network
spec:
  cidr: "192.168.0.0/16"
  gateway: "192.168.0.1"

那么用户就需要知道,Network这个自定义资源的分组是什么,版本是什么等等。。。。所以需要编写一个CRD的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

这里描述了有一个自定义资源,类型为NetWork,复数(plural)是networks,组为samplecrd.k8s.io,版本为v1,scope属于Namespace的对象,类似于Pod。

这样k8s就能够知道Network是什么了,但是作为一个API对象,在其YAML文件中有很多字段,创建对象的时候需要验证字段是否合法,那么k8s如何知道这个自定义资源有哪些字段。

编写自定义资源的具体类型代码

这就需要做些代码工作了。首先,在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中的samplecrd是组名,v1是版本名。 v1下面的types.go中则定义了Network对象的完整描述。samplecrd/register.go中定义全局变量。 在samplecrd/v1下创建doc.go文件,添加如下内容,利用codegen来生成代码:

// +k8s:deepcopy-gen=package
 
// +groupName=samplecrd.k8s.io
package v1

+<tag_name>[=value]格式的注释,这就是 Kubernetes 进行代码生成要用的 Annotation 风格的注释。这里的例子中表示为包下的所有类型生成DeepCopy方法,这个包对应的api组为samplecrd.k8s.io。

type.go文件下就是自定义类型的字段:

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"`
}

其中的Spec字段就是需要我们自己定义的部分。json标签的名字也是在yaml中的名字。还用用来描述一组对象应该具有哪些字段,比如NetworkList

  • +genclient表示为API资源生成对应的Client代码。
  • +genclient:noStatus 的意思是:这个 API 资源类型定义里,没有 Status 字段,否则会自动添加UpdateStatus字段,当然如果有Status字段,那么就不需要添加这个注释了。
  • +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object的注释。它的意思是,请在生成 DeepCopy 的时候,实现 Kubernetes 提供的 runtime.Object 接口。否则,在某些版本的 Kubernetes 里,你的这个类型定义会出现编译错误。这是一个固定的操作。

最后编写samplecrd/v1/register.go文件。就是为了让客户端也能够知道Network资源类型。最主要的功能就是定义了下面的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
}

然后使用k8s提供的代码生成工具k8s.io/code-generator,为上面定义的Network资源类型自动生成clientset, informer和lister。client就是操作Network对象需要使用的客户端。

使用方法如下:

# 代码生成的工作目录,也就是我们的项目路径
$ 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"

这样就生成了如下的代码结构:

$ 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/samplecrd/v1/zz_generated.deepcopy.go就是自动生成的DeepCopy代码。

使用

现在就可以在集群里创建Network类型的API对象了。

首先创建Network的CRD:

$ kubectl apply -f crd/network.yaml
customresourcedefinition.apiextensions.k8s.io/networks.samplecrd.k8s.io created

然后,创建一个Network对象。

$ kubectl apply -f example/example-network.yaml 
network.samplecrd.k8s.io/example-network created

可以通过kubectl get networdkubectl describe network来查看创建的对象的信息。


tags: 容器编排 声明式API