云计算

Kubebuilder构建operator

2021-07-15  本文已影响0人  梅_梅

1. 为什么需要crd

2. 整体结构

如图所示,其中黄色的部分是kubernetes默认的部分,而白色的模块则是动态扩展的自定义资源及其组件。

譬如,声明了访问固定网页的资源crd,那么当建立该资源对象时,operator就会获取到该信息,并根据资源对象中提供的地址去执行访问网页的行为。

crdss.png

3. 使用kubebuilder构建operator

为什么使用kubebuilder? 因为当我们需要增加新资源时就需要声明crd资源和构建operator。不同的opertor都需要连接apiserver同步自定义资源,因此会有高度冗余的逻辑。
kubebuilder不仅会为我们生成opertor服务框架,还能自动同步自定义资源的变化。
用户只需要定义CRD结构,和处理资源变动时的回调即可。

3.1 项目结构

以使用kubebuilder创建的operator做结构分析
如下图所示,operator主要包含以下组件, 协同完成了对自定义资源的监控和根据资源处理对应的业务。

用户只需要处理黄色部分的工作

  1. 定义好自定义资源结构crd,由框架注册资源
  2. 定义回调函数controller,资源变动时处理业务

以下部分是kubebuilder的框架性组件

Kubebuilder 的核心组件,负责在 Controller 进程里面根据 Scheme 同步 Api Server 中所有该 Controller 关心 GVKs 的 GVRs,其核心是 GVK -> Informer 的映射,Informer 会负责监听对应 GVK 的 GVRs 的创建/删除/更新操作,以触发 Controller 的 Reconcile 逻辑。

Kubebuidler 为我们生成的脚手架文件,我们只需要实现 Reconcile 方法即可。

在实现 Controller 的时候不可避免地需要对某些资源类型进行创建/删除/更新,就是通过该 Clients 实现的,其中查询功能实际查询是本地的 Cache,写操作直接访问 Api Server。

由于 Controller 经常要对 Cache 进行查询,Kubebuilder 提供 Index utility 给 Cache 加索引提升查询效率。

在一般情况下,如果资源被删除之后,我们虽然能够被触发删除事件,但是这个时候从 Cache 里面无法读取任何被删除对象的信息,这样一来,导致很多垃圾清理工作因为信息不足无法进行,K8s 的 Finalizer 字段用于处理这种情况。在 K8s 中,只要对象 ObjectMeta 里面的 Finalizers 不为空,对该对象的 delete 操作就会转变为 update 操作,具体说就是 update deletionTimestamp 字段,其意义就是告诉 K8s 的 GC“在deletionTimestamp 这个时刻之后,只要 Finalizers 为空,就立马删除掉该对象”。

所以一般的使用姿势就是在创建对象时把 Finalizers 设置好(任意 string),然后处理 DeletionTimestamp 不为空的 update 操作(实际是 delete),根据 Finalizers 的值执行完所有的 pre-delete hook(此时可以在 Cache 里面读取到被删除对象的任何信息)之后将 Finalizers 置为空即可。

K8s GC 在删除一个对象时,任何 ownerReference 是该对象的对象都会被清除,与此同时,Kubebuidler 支持所有对象的变更都会触发 Owner 对象 controller 的 Reconcile 方法。

kube.png

3.2 构建operator

我们将使用kubebuilder扩展一个带有replicas字段的资源 imoocpod。当创建该资源实例后,将会维持数量等于replicas, 名称等于该实例前缀的一组pod,

Kubebuilder由Kubernetes特殊兴趣组(SIG) API Machinery 拥有和维护,能够帮助开发者创建 CRD 并生成 controller 脚手架。

kubebuilder 使用起来比较简单,首先我们需要安装 kubebuilder 和它依赖的 kustomize。

os=$(go env GOOS)
arch=$(go env GOARCH)

# download kubebuilder and extract it to tmp
curl -sL https://go.kubebuilder.io/dl/2.3.0/${os}/${arch} | tar -xz -C /tmp/

# move to a long-term location and put it on your path
# (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else)
mv /tmp/kubebuilder_2.3.0_${os}_${arch} /usr/local/kubebuilder
export PATH=$PATH:/usr/local/kubebuilder/bin

首先通过以下命令创建 CR 的GVK(Group、Version、Kind):

# 定义 crd 所属的 domain,这个指令会帮助你生成一个工程。
$ kubebuilder init --domain xiyanxiyan10
# 创建 crd 在 golang 工程中的结构体,以及其所需的 controller 逻辑
$ kubebuilder create api --group batch --version v1alpha1 --kind ImoocPod

执行结束后,目录结构如下:

.
├── api         ## 这里定义了 sample 的结构体,以及所需的 deepcopy 实现         
│   └── v1alpha1
│       ├── groupversion_info.go
│       ├── imoocpod_types.go # 本例中需要二次开发 crd定义
│       └── zz_generated.deepcopy.go
├── bin
│   └── manager ## controller 编译后的 二进制文件
├── config      ## 包含了我们在使用 crd 是可能需要的 yml 文件,包括rbac、controller的deployment 等
│   ├── certmanager
│   ├── crd
│   ├── default
│   ├── manager
│   ├── prometheus
│   ├── rbac
│   ├── samples
│   └── webhook
├── controllers         ## 我们的controller 逻辑就放在这里,
│   ├── imoocpod_controller.go # 本例中需要二次开发 controller
│   └── suite_test.go
├── Dockerfile          
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
├── main.go
├── Makefile
└── PROJECT

自定义CRD字段

在api/v1/imoocpod_types.go中,包含了 kubebuilder 为我们生成的 ImoocPodSpec 以及相关字段, 我们改造生成的ImoocPodSpec, ImoocPodStatus 两个结构,引入Replicas, PodNames 两个字段完成demo功能。

package v1alpha1
    
import (    
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)    
    
// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
    
// ImoocPodSpec defines the desired state of ImoocPod
type ImoocPodSpec struct {
        // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
        // Important: Run "make" to regenerate code after modifying this file
    
        // Foo is an example field of ImoocPod. Edit ImoocPod_types.go to remove/update
        Replicas int `json:"replicas"`     //  这里二次开发,加入计数字段
}    
    
// ImoocPodStatus defines the observed state of ImoocPod
type ImoocPodStatus struct {
        // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
        // Important: Run "make" to regenerate code after modifying this file
        Replicas int      `json:"replicas"`
        PodNames []string `json:"podNames"`    //这里二次开发加入计数和pod列表
}    
    
// +kubebuilder:object:root=true
    
// ImoocPod is the Schema for the imoocpods API
type ImoocPod struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty"`
            
        Spec   ImoocPodSpec   `json:"spec,omitempty"`
        Status ImoocPodStatus `json:"status,omitempty"`
}
            
// +kubebuilder:object:root=true
            
// ImoocPodList contains a list of ImoocPod
type ImoocPodList struct {
        metav1.TypeMeta `json:",inline"`
        metav1.ListMeta `json:"metadata,omitempty"`
        Items           []ImoocPod `json:"items"`
}

kubebuilder 依赖于 controller-runtime 实现 controller 整个处理流程,在此工程中,controller 对资源的监听依赖于 Informer 机制,细节详见K8s中 controller & infromer机制controller-runtime 在此机制上又封装了一层,其整体流程入下图


其中 Informer 已经由kubebuilder和contorller-runtime 实现,监听到的资源的事件(创建、删除、更新、webhock)都会放在 Informer 中。然后这个事件会经过 predict()方法进行过滤,经过interface enqueue进行处理,最终放入 workqueue中。我们创建的 controller 则会依次从workqueue中拿取事件,并调用我们自己实现的 Recontile() 方法进行业务处理。
在controllers/imoocpod_controller.go中,有函数
// +kubebuilder:rbac:groups=sample.sample.io,resources=samples,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=sample.sample.io,resources=samples/status,verbs=get;update;patch
func (r *PlaybookReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
        _ = context.Background()
        _ = r.Log.WithValues("imoocpod", req.NamespacedName)

        // your logic here

        return ctrl.Result{}, nil
}

我们改造他,实现ImoocPod资源对应的具体逻辑

// +kubebuilder:rbac:groups=batch.xiyanxiyan10,resources=imoocpods,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch.xiyanxiyan10,resources=imoocpods/status,verbs=get;update;patch
        
func (r *ImoocPodReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
        ctx := context.Background()
        logger := r.Log.WithValues("imoocpod", req.NamespacedName)
        logger.Info("start reconcile")
        
        // fetch the ImoocPod instance
        instance := &batchv1alpha1.ImoocPod{}
        if err := r.Client.Get(ctx, req.NamespacedName, instance); err != nil {
                if errors.IsNotFound(err) {
                        return ctrl.Result{}, nil
                }
                return ctrl.Result{}, err
        }
        
        // 1. 获取 name 对应的所有的 pod 的列表
        lbls := labels.Set{"app": instance.Name}
        existingPods := &corev1.PodList{}
        if err := r.Client.List(ctx, existingPods, &client.ListOptions{
                Namespace: req.Namespace, LabelSelector: labels.SelectorFromSet(lbls)}); err != nil {
                logger.Error(err, "fetching existing pods failed")
                return ctrl.Result{}, err
        }
        
        // 2. 获取 pod 列表中的 pod name
        var existingPodNames []string
        for _, pod := range existingPods.Items {
                if pod.GetObjectMeta().GetDeletionTimestamp() != nil {
                        continue
                }
                if pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodPending {
                        existingPodNames = append(existingPodNames, pod.GetObjectMeta().GetName())
                }
        }
        
        
        // 4. pod.Spec.Replicas > 运行中的 len(pod.replicas),比期望值小,需要 scale up create
        if instance.Spec.Replicas > len(existingPodNames) {
                logger.Info(fmt.Sprintf("creating pod, current and expected num: %d %d", len(existingPodNames), instance.Spec.Replicas))
                pod := newPodForCR(instance)
                if err := controllerutil.SetControllerReference(instance, pod, r.Scheme); err != nil {
                        logger.Error(err, "scale up failed: SetControllerReference")
                        return ctrl.Result{}, err
                }
       // 5. pod.Spec.Replicas < 运行中的 len(pod.replicas),比期望值大,需要 scale down delete
        if instance.Spec.Replicas < len(existingPodNames) {
                logger.Info(fmt.Sprintf("deleting pod, current and expected num: %d %d", len(existingPodNames), instance.Spec.Replicas))
                pod := existingPods.Items[0]
                existingPods.Items = existingPods.Items[1:]
                if err := r.Client.Delete(ctx, &pod); err != nil {
                        logger.Error(err, "scale down faled")
                        return ctrl.Result{}, err
                }
        }
       
        return ctrl.Result{Requeue: true}, nil
}      
       
func newPodForCR(cr *batchv1alpha1.ImoocPod) *corev1.Pod {
        labels := map[string]string{"app": cr.Name}
        return &corev1.Pod{
                ObjectMeta: metav1.ObjectMeta{
                        GenerateName: cr.Name + "-pod",
                        Namespace:    cr.Namespace,
                        Labels:       labels,
                },
                Spec: corev1.PodSpec{
                        Containers: []corev1.Container{
                                {
                                        Name:    "busybox",
                                        Image:   "busybox",
                                        Command: []string{"sleep", "3600"},
                                },
                        },
                },
        }
}      

在项目主目录下执行make install,会自动调用 kustomize 创建部署 crd 的yml并部署,我们也可以从 config/crd/bases/下找到对应的 crd yaml文件。

然后执行 make run 则在本地启动 operator 主程序,日志如下,可见已经在监听资源

$ make run
...
go vet ./...
/home/xiyanxiyan10/project/app/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
go run ./main.go
2021-07-16T19:51:29.959+0800    INFO    controller-runtime.metrics  metrics server is starting to listen    {"addr": ":8080"}
2021-07-16T19:51:29.959+0800    INFO    setup   starting manager
2021-07-16T19:51:29.959+0800    INFO    controller-runtime.manager  starting metrics server {"path": "/metrics"}
2021-07-16T19:51:29.959+0800    INFO    controller-runtime.controller   Starting EventSource    {"controller": "imoocpod", "source": "kind source: /, Kind="}
2021-07-16T19:51:30.060+0800    INFO    controller-runtime.controller   Starting Controller {"controller": "imoocpod"}
2021-07-16T19:51:30.060+0800    INFO    controller-runtime.controller   Starting workers    {"controller": "imoocpod", "worker count": 1}
2021-07-16T19:51:30.060+0800    INFO    controllers.ImoocPod    start reconcile {"imoocpod": "default/demo"}

当我们想以deployment 方式部署controller时,可以使用 Dockerfile 构建镜像,使用config/manager/manager.yml 部署。

apiVersion: batch.xiyanxiyan10/v1alpha1
kind: ImoocPod
metadata:       
  name: demo 
spec:        
  replicas: 2 

可以看到,在kubernetes已经根据自定义资源实例创建了对应的pod对象


demo.png
上一篇下一篇

猜你喜欢

热点阅读