0%

Kubernetes CRD编写

前篇文章有说明了CRD用来创建自定义资源,不过,资源只是一些数据,真正想要让这些数据具备实际意义还需要围绕数据的操作。

在k8s中,数据称之为资源,比如说Namespace, ReplicaSet, Deployment等等,而对这些数据的操作则是通过控制器实现的,比如说namespace-controllerdeployment-controller等等。

这些控制器由一个专门的组件管理,叫做kube-controller-manager,可以加上--help参数查看支持的控制器,目前有如下:

1
attachdetach, bootstrapsigner, cloud-node-lifecycle, clusterrole-aggregation, cronjob, csrapproving, csrcleaner, csrsigning, daemonset, deployment, disruption, endpoint, endpointslice, garbagecollector, horizontalpodautoscaling, job, namespace, nodeipam, nodelifecycle, persistentvolume-binder, persistentvolume-expander, podgc, pv-protection, pvc-protection, replicaset, replicationcontroller, resourcequota, root-ca-cert-publisher, route, service, serviceaccount, serviceaccount-token, statefulset, tokencleaner, ttl, ttl-after-finished

这些控制器的实现方式简单来说,就是每隔一段时间通过访问api-server查询资源的变化,以及进行相应的操作。自定义的资源没有默认的控制器,也能通过自己编写控制器以实现对资源的操作。

那么,接下来就尝试编写一个CRD控制器吧,在这篇文章中,将尝试创建一个名为alpine的crd,该crd用来快速生成一个使用了alpine镜像的pod。

代码已上传至github:https://github.com/staight/crd

准备环境

  • kubernetes:v1.16.1
  • go:1.13.1
  • kubebuilder:2.0.1
  • kustomize:v3.2.3

其中,Kubebuilder是Kubernetes SIGs(Special Interest Group,特别兴趣小组)的一个项目,它是一个基于CRD构建Kubernetes API的框架,使用它可以很方便地构建出CRD和其对应的控制器。下载地址:https://github.com/kubernetes-sigs/kubebuilder

kustomize用于渲染资源配置模板,与helm类似,但主打灵活和小巧,以及。。。官方性=.=,以下是下载地址:https://github.com/kubernetes-sigs/kustomize

注意需要将go,kubebuilder,kustomize放在环境变量中,且在~/.kube/config位置需要有访问kubernetes的kubeconfig配置文件。

创建项目

该项目的位置:/root/code/go/crd

首先使用go mod init命令,表示在该文件夹中应使用go module:

1
2
[root@staight crd]# go mod init crd
go: creating new go.mod: module crd

如果在大陆的话有许多模块下载不了,这时可以使用go的代理,用了都说好,下载速度贼快:

1
go env -w GOPROXY=https://goproxy.io,direct

接下来使用kubebuilder init命令初始化该项目,--domian指定自己想要使用的API Group,本例中使用的是k8s.io

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@staight crd]# kubebuilder init --domain k8s.io
go get sigs.k8s.io/controller-runtime@v0.2.2
go: finding sigs.k8s.io v0.2.2
go mod tidy
go: downloading github.com/onsi/gomega v1.4.2
...
go: extracting gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
go: extracting cloud.google.com/go v0.26.0
Running make...
make
/usr/bin/controller-gen object:headerFile=./hack/boilerplate.go.txt paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go
Next: Define a resource with:
$ kubebuilder create api

可以看到该命令下载了一些依赖包,生成了众多文件,并使用fmtvet做了格式化与静态检查。

注:如果出现which: no controller-gen的错误,说明$GOPATH/bin不在$PATH环境变量中,此时将$GOPATH/bin/controller-gen程序放到/bin/目录即可,root用户的话是/root/go/bin

来看看init命令生成了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[root@staight crd]# tree
.
├── bin
│ └── manager
├── config
│ ├── certmanager
│ │ ├── certificate.yaml
│ │ ├── kustomization.yaml
│ │ └── kustomizeconfig.yaml
│ ├── default
│ │ ├── kustomization.yaml
│ │ ├── manager_auth_proxy_patch.yaml
│ │ ├── manager_prometheus_metrics_patch.yaml
│ │ ├── manager_webhook_patch.yaml
│ │ └── webhookcainjection_patch.yaml
│ ├── manager
│ │ ├── kustomization.yaml
│ │ └── manager.yaml
│ ├── rbac
│ │ ├── auth_proxy_role_binding.yaml
│ │ ├── auth_proxy_role.yaml
│ │ ├── auth_proxy_service.yaml
│ │ ├── kustomization.yaml
│ │ ├── leader_election_role_binding.yaml
│ │ ├── leader_election_role.yaml
│ │ └── role_binding.yaml
│ └── webhook
│ ├── kustomization.yaml
│ ├── kustomizeconfig.yaml
│ └── service.yaml
├── Dockerfile
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
├── main.go
├── Makefile
└── PROJECT

8 directories, 28 files

创建API

使用kubebuilder create api命令创建一个api:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@staight crd]# kubebuilder create api --group staight --version v1 --kind Alpine
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
api/v1/alpine_types.go
controllers/alpine_controller.go
Running make...
/usr/bin/controller-gen object:headerFile=./hack/boilerplate.go.txt paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

如上,创建了一个名为staight的group,version为v1,kind为Alpine。group+domain即为apiGroup,这里为staight.k8s.io

该命令同时创建两个文件:api/v1/alpine_types.gocontrollers/alpine_controller.go。其中alpine_types定义了crd的类型,而alpine_controller则是该crd的控制器,该项目的主要工作就在这两个文件中进行。

编写API

查看api/v1/alpine_types.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// AlpineSpec defines the desired state of Alpine
type AlpineSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
}

// AlpineStatus defines the observed state of Alpine
type AlpineStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Alpine is the Schema for the alpines API
type Alpine struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec AlpineSpec `json:"spec,omitempty"`
Status AlpineStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// AlpineList contains a list of Alpine
type AlpineList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Alpine `json:"items"`
}

该文件定义了一个名为alpine的资源模板,先看Alpine结构:

  • metav1.TypeMeta:描述了apiVersion和kind
  • metav1.ObjectMeta:描述了namespace,name,label等信息
  • Spec:指向了AlpineSpec结构,即资源配置中的Spec字段
  • Status:指向了AlpineStatus结构,即资源配置中的Status字段

AlpineSpec结构定义了Alpine资源的细节,而AlpineStatus结构则定义了其状态,最后还有一个AlpineList结构,表示多个Alpine资源的集合,在同时获取多个Alpine资源时会用到,比如说使用kubectl get pod命令时实际上获取的就是PodList结构。

由于我们只需要生成一个Pod,因此在AlpineSpec结构中定义alpineTemplate字段,表示pod模板,并设置omitempty表示允许为空,如果为空则创建默认的alpine模板(之后定义)。

在AlpineStatus结构中定义active字段,用来存放其引用的pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
// AlpineSpec defines the desired state of Alpine
type AlpineSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
AlpineTemplate corev1.PodTemplate `json:alpineTemplate,omitempty`
}

// AlpineStatus defines the observed state of Alpine
type AlpineStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Active []corev1.ObjectReference `json:"active,omitempty"`
}

v1文件夹还包括两个文件:groupversion_info.go和zz_generated.deepcopy.go。

这两个文件无需改动,不过需要简单说下它们的作用:

groupversion_info.go:包括一些关于group-version的元数据。比如说这里的group为staight.k8s.io,version为v1
zz_generated.deepcopy.go:用于实施runtime.Object接口,表示上述资源。

1
2
3
4
5
6
7
8
9
10
11
// groupversion_info.go
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "staight.k8s.io", Version: "v1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

编写控制器

创建API时同时还创建了controllers/alpine_controller.go文件,可以在该文件中编写代码,以完成控制器的逻辑。

本实例的目的较为简单:如果alpine资源有pod模板,则按照该模板创建pod;否则使用默认模板创建。

首先,获取alpine资源对象,如果没有alpine资源对象则直接返回:

1
2
3
4
5
var alpine staightv1.Alpine
if err := r.Get(ctx, req.NamespacedName, &alpine); err != nil {
log.Error(err, "unable to fetch alpine")
return ctrl.Result{}, ignoreNotFound(err)
}

然后列出该alpine资源对象控制的所有pod:

1
2
3
4
5
var childPods corev1.PodList
if err := r.List(ctx, &childPods, client.InNamespace(req.Namespace), client.MatchingField(podOwnerKey, req.Name)); err != nil {
log.Error(err, "unable to list child pods")
return ctrl.Result{}, err
}

其中,podOwnerKey是.metadata.controller字段,req.Name是alpine资源对象的名称。如果无法获取则返回。

接着获取控制的pod的数量:

1
2
3
4
5
6
7
8
9
// 获取控制pod的数量
size := len(childPods.Items)
log.V(1).Info("pod count", "active pod", size)

// 如果数量不为0,则直接返回
if size != 0 {
log.V(1).Info("has child pod, skip")
return ctrl.Result{}, nil
}

如果pod数量不为0,说明已经有了pod,直接返回。

pod数量为0的话,则需要构建pod资源配置,准备创建pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 构造需要创建的pod:如果有pod模板,则使用pod模板创建;否则使用默认模板
constructPodForAlpine := func(alpine *staightv1.Alpine) (*corev1.Pod, error) {
scheduledTime := time.Now()
name := fmt.Sprintf("%s-%d", alpine.Name, scheduledTime.Unix())
spec := podSpec

// fmt.Printf("get alpine: %+v\n", alpine.Spec.PodTemplate.Spec)
// fmt.Printf("default alpine: %+v\n", corev1.PodSpec{})

// 查看alpine资源是否有pod模板
if !reflect.DeepEqual(alpine.Spec.PodTemplate.Spec, corev1.PodSpec{}) {
log.V(1).Info("podSpec construct", "podSpec", "has podSpec")
spec = *alpine.Spec.PodTemplate.Spec.DeepCopy()
}

// 构造pod
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: alpine.Namespace,
Name: name,
Labels: make(map[string]string),
Annotations: make(map[string]string),
},
Spec: spec,
}

// 将alpine资源的annotation和label复制到对应pod上
for k, v := range alpine.Spec.PodTemplate.Annotations {
pod.Annotations[k] = v
}
pod.Annotations[scheduledTimeAnnotation] = scheduledTime.Format(time.RFC3339)
for k, v := range alpine.Spec.PodTemplate.Labels {
pod.Labels[k] = v
}

// 设置控制关系,实际上是给pod添加了.metadata.ownerReferences字段
if err := ctrl.SetControllerReference(alpine, pod, r.Scheme); err != nil {
return nil, err
}
return pod, nil
}

pod, err := constructPodForAlpine(&alpine)
if err != nil {
log.Error(err, "unable to construct pod from template")
return ctrl.Result{}, nil
}

最后,使用该pod模板创建pod:

1
2
3
4
5
6
7
// 创建pod
if err := r.Create(ctx, pod); err != nil {
log.Error(err, "unable to create pod for alpine", "pod", pod)
return ctrl.Result{}, err
}

log.V(1).Info("create pod for alpine run", "pod", pod)

最后,还需要告诉reconciler只对具有.metadata.ownerReferences字段的pod感兴趣,其余资源的改动不会触发reconciler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (r *AlpineReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := mgr.GetFieldIndexer().IndexField(&corev1.Pod{}, podOwnerKey, func(rawObj runtime.Object) []string {
pod := rawObj.(*corev1.Pod)
owner := metav1.GetControllerOf(pod)
if owner == nil {
return nil
}
if owner.APIVersion != apiGVstr || owner.Kind != "Alpine" {
return nil
}
return []string{owner.Name}
}); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&staightv1.Alpine{}).
Owns(&corev1.Pod{}).
Complete(r)
}

代码完成!接下来赶紧测试下。

运行项目

使用make命令更新对alpine资源的配置:

1
2
3
4
5
[root@staight crd]# make
/usr/bin/controller-gen object:headerFile=./hack/boilerplate.go.txt paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

然后使用make install命令在集群中创建对应的alpine资源,注意需要在~/.kube/config路径下的kubeconfig配置文件,以访问集群:

1
2
3
4
[root@staight crd]# make install
/usr/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/alpines.staight.k8s.io created

此时crd资源已安装在集群中:

1
2
3
[root@node1 tmp]# kubectl get crd
NAME CREATED AT
alpines.staight.k8s.io 2019-10-13T20:09:31Z

最后,使用make run命令运行控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@staight crd]# make run
/usr/bin/controller-gen object:headerFile=./hack/boilerplate.go.txt paths="./..."
go fmt ./...
go vet ./...
/usr/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
go run ./main.go
2019-10-14T04:12:16.682+0800 INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"}
2019-10-14T04:12:16.684+0800 INFO controller-runtime.controller Starting EventSource {"controller": "alpine", "source": "kind source: /, Kind="}
2019-10-14T04:12:16.684+0800 INFO controller-runtime.controller Starting EventSource {"controller": "alpine", "source": "kind source: /, Kind="}
2019-10-14T04:12:16.685+0800 INFO setup starting manager
2019-10-14T04:12:16.685+0800 INFO controller-runtime.manager starting metrics server {"path": "/metrics"}
2019-10-14T04:12:16.786+0800 INFO controller-runtime.controller Starting Controller {"controller": "alpine"}
2019-10-14T04:12:16.886+0800 INFO controller-runtime.controller Starting workers {"controller": "alpine", "worker count": 1}

尝试创建一个没有pod模板的alpine资源:

1
2
3
4
5
6
7
[root@node1 tmp]# cat alpine.yml 
apiVersion: staight.k8s.io/v1
kind: Alpine
metadata:
name: alpine
[root@node1 tmp]# kubectl apply -f alpine.yml
alpine.staight.k8s.io/alpine created

查看pod,可以看到新创建了一个名为alpine-1570997556的pod:

1
2
3
[root@node1 tmp]# kubectl get pod
NAME READY STATUS RESTARTS AGE
alpine-1570997556 1/1 Running 0 3m1s

创建成功,实验成功~

小结

本文尝试使用kubebuilder编写了一个名为alpine的CRD,该CRD用来自动生成一个alpine的pod,或者使用模板生成pod。

本示例功能较为简陋,但具备了CRD的基本功能。考虑到用户可能直接修改pod,更加健壮的做法可以像ReplicaSet一样,给Pod模板生成一个散列值,如果散列值不一致则说明pod被改动,重新生成。

CRD + Controller = Operator,有兴趣的话可以看看OperatorHub:https://operatorhub.io/

如果有报错:"error": "the server could not find the requested resource,说明没有添加一个特别的注释,需参考如下issue:https://github.com/kubernetes-sigs/kubebuilder/issues/751

参考文档

The Kubebuilder Book:https://book.kubebuilder.io/

如何从零开始编写一个Kubernetes CRD:https://www.servicemesher.com/blog/kubernetes-crd-quick-start/