Kubebuilder构建operator

1. 为什么需要crd

  • kubernetes默认支持deployment,configmap等资源,供我们以资源的形式管理云平台上的服务。
  • 但当我们需要添加新的资源时,为了不对kubernetes原有代码进行更改,则需要kubernetes支持动态扩展新资源类型。
  • 因此提供了自定义资源crd作为动态扩展的入口,通过crd可以声明新资源类型,声明后后就能创建该自定义资源的实例,自定义资源对用的具体行为则由用户创建的operator完成。

2. 整体结构

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

  • 在etcd中用户声明了自定义crd的结构,声明的自定义资源与deploymenet等默认资源使用方式一致
  • 默认资源大部分由kubernetes中的controller-manager等组件管理,而自定义资源则由部署的operator服务通过api-server管理。CRD资源只是一个抽象资源数据,资源对象对应的实际服务是由operator提供。

譬如,声明了访问固定网页的资源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的框架性组件

  • Cache

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

  • Controller

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

  • Clients

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

  • Index

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

  • Finalizer

在一般情况下,如果资源被删除之后,我们虽然能够被触发删除事件,但是这个时候从 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 置为空即可。

  • OwnerReference

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

kube.png

3.2 构建operator

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

  • kubebuilder 搭建与使用

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

  • 安装kubebuilder

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
  • 初始化operator

首先通过以下命令创建 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"`
}

  • 完善 controller 逻辑

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

  • 部署 crd 和 controller

在项目主目录下执行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 部署。

  • 资源创建实例
    新建文件demo.yaml, 并使用命令部署 kubectl create -f ./demo.yaml
apiVersion: batch.xiyanxiyan10/v1alpha1
kind: ImoocPod
metadata:       
  name: demo 
spec:        
  replicas: 2 
  • 确认结果

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


demo.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容