3

Kubernetes CRD编写

 2 years ago
source link: https://staight.github.io/2019/10/08/Kubernetes-CRD%E7%BC%96%E5%86%99/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Kubernetes CRD编写

发表于

2019-10-08 更新于 2019-10-15

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

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

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

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:

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

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

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

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

[root@staight crd]# kubebuilder init --domain k8s.io
go get sigs.k8s.io/[email protected]
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命令生成了什么:

[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:

[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文件:

// 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:

// 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接口,表示上述资源。

// 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资源对象则直接返回:

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:

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的数量:

// 获取控制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:

// 构造需要创建的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:

// 创建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:

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资源的配置:

[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配置文件,以访问集群:

[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资源已安装在集群中:

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

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

[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资源:

[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:

[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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK