当前位置: 首页 > 科技观察

一篇带你实战kubebuilder的文章:CRUD

时间:2023-03-12 06:24:26 科技观察

前两天的文章,我们搭建了本地的K8s开发环境,了解了kubebuilder的基本使用。今天,我们将从我之前遇到的一个真实需求出发,完成它写一个Operator需求分析背景在K8s运行过程中,我们发现总会有一些业务因为各种原因,需要运行在一些独立的节点池上安全性和可用性。这些节点池可能会被分成一些更小的节点池。节点池。虽然我们可以使用Taint和Label来划分节点,使用nodeSelector和tolerations让Pod运行在指定的节点上,但是主要有两个问题:一是不方便管理,在实际使用中我们会发现不匹配和遗漏。v1.16之后虽然可以使用RuntimeClass来简化pod的配置,但是RuntimClass与节点没有关联[^1]另外就是扩展需求不容易实现,比如A节点属于网段或者当节点加入节点池,墙自动打开。1.对于应用,我们可以在创建或更新应用时方便的选择对应的节点池。默认不需要选择2。对于节点池来说,一个节点池可能有多个节点,一个节点也可能同时属于多个节点池。不同节点池的标签和污点信息可能不同等待MVP版本支持标签和污点设计节点池资源如下apiVersion:nodes.lailin.xyz/v1kind:NodePoolmetadata:name:testspec:taints:-key:node-pool.lailin.xyzvalue:testeffect:NoSchedulelabels:node-pool.lailin.xyz/test:""如何建立节点与节点池的映射关系?我们可以使用node-role.kubernetes.io/xxx=""标签与节点池建立映射关系,使用节点池名称对应的xxx这个标签的好处是可以很方便的看到是哪个节点池使用kubectl获取节点所属节点,在创建NodePool对象的时候,我们创建对应的RuntimeClass对象,然后只需要在Pod中添加runtimeClassName:myclass就足够了。注意:对于MVP版本,我们实际上不需要使用自定义资源。我们只需要结合标签和RuntimeClass就可以满足要求了。但这里展示一个完整的流程,我们使用自定义资源开发创建项目#Initializeprojectkubebuilderinit--repogithub.com/mohuishou/blog-code/k8s-operator/03-node-pool-operator--domainlailin.xyz--skip-go-version-check#Createapikubebuildercreateapi--groupnodes--versionv1--kindNodePool定义对象//NodePoolSpec节点池typeNodePoolSpecstruct{//TaintsstainTaints[]v1.Taint`json:"taints,omitempty"`//LabelslabelLabelsmap[string]string`json:"labels,omitempty"`}创建并实现Reconcile函数,req会返回当前改变对象的Namespace和Name信息。有了这两个信息,我们就可以获取对象了,所以我们的操作是1.获取NodePool对象2.通过NodePool对象生成对应的Label,查找对应的Label是否已经存在。如果存在,则在对应的Node上添加对应的Taint和Label。如果不存在,则跳过3.通过NodePool生成对应的RuntimeClass,查看对应的RuntimeClass是否已经存在。If不存在则新建,存在则跳过Getobjectpool:=&nodesv1.NodePool{}iferr:=r.Get(ctx,req.NamespacedName,pool);err!=nil{returnctrl.Result{},err}varnodescorev1.NodeList//检查是否有对应的节点,如果有则向这些节点添加数据err:=r.List(ctx,&nodes,&client.ListOptions{LabelSelector:pool.NodeLabelSelector()})ifclient.IgnoreNotFound(err)!=nil{returnctrl.Result{},err}iflen(nodes.Items)>0{r.Log.Info("findnodes,willmergedata","nodes",len(nodes.Items))for_,n:=rangenodes.Items{n:=nerr:=r.Patch(ctx,pool.Spec.ApplyNode(n),client.Merge)iferr!=nil{returnctrl.Result{},err}}}varruntimeClassv1beta1.RuntimeClasserr=r.Get(ctx,client.ObjectKeyFromObject(pool.RuntimeClass()),&runtimeClass)ifclient.IgnoreNotFound(err)!=nil{returnctrl.Result{},err}//创建一个新的ifruntimeClass.Name=""{err=r.Create(ctx,pool.RuntimeClass())iferr!=nil{returnctrl.Result{},err}}如果不存在returnctrl.Result{},nil}update相信聪明的你已经发现上面的创建逻辑有很多问题1.如果更新了NodePool对象,Node是否更新对应的Taint和Label如果NodePool删除了一个Label或者Node对应的Label或者Taint是否需要删除,如何删除呢?2.如果NodePool对象更新了,RuntimeClass是否更新,如何更新我们的MVP版本。实施可以更简单。我们同意所有属于NodePool的节点Tanit和Label信息都应该由NodePool管理。密钥包含kubernetes标签污点,除了func(r*NodePoolReconciler)Reconcile(ctxcontext.Context,reqctrl.Request)(ctrl.Result,error){//....iflen(nodes.Items)>0{r.Log。Info("findnodes,willmergedata","nodes",len(nodes.Items))for_,n:=rangenodes.Items{n:=n//更新节点的label和taint信息+err:=r.Update(ctx,pool.Spec.ApplyNode(n))-err:=r.Patch(ctx,pool.Spec.ApplyNode(n),client.Merge)iferr!=nil{returnctrl.Result{},err}}}//...//如果存在则更新+err=r.Client.Patch(ctx,pool.RuntimeClass(),client.Merge)+iferr!=nil{+returnctrl.Result{},err+}returnctrl.Result{},err}ApplyNode方法如下,主要是修改节点的label和taint信息//ApplyNode生成一个Node结构体,可以用于Patch数据func(s*NodePoolSpec)ApplyNode(nodecorev1.Node)*corev1.Node{//除了节点池的标签,我们只保留k8s的相关标签//注意:这里的逻辑如果一个节点只能属于一个节点池nodeLabels:=map[string]string{}fork,v:=rangenode.Labels{ifstrings.Contains(k,"kubernetes"){nodeLabels[k]=v}}fork,v:=ranges.Labels{nodeLabels[k]=v}node.Labels=nodeLabels//污点同vartaints[]corev1.Taintfor_,taint:=rangenode.Spec.Taints{ifstrings.Contains(taint.Key,"kubernetes"){taints=append(taints,taint)}}node.Spec.Taints=append(taints,s.Taints...)return&node}我们使用makerun来运行服务并测试它。首先,我们准备一个NodePoolCRD,并使用kubectlapply-fconfig/samples/部署apiVersion:nodes。lailin.xyz/v1kind:NodePoolmetadata:name:masterspec:taints:-key:node-pool.lailin.xyzvalue:mastereffect:NoSchedulelabels:"node-pool.lailin.xyz/master":"8""node-pool.lailin.xyz/test":"2"handler:runc部署后,可以得到节点的标签labels:beta.kubernetes.io/arch:amd64beta.kubernetes.io/os:linuxkubernetes.io/arch:amd64kubernetes。io/hostname:kind-control-planekubernetes.io/os:linuxnode-pool.lailin.xyz/master:"8"node-pool.lailin.xyz/test:"2"node-role.kubernetes.io/control-plane:""node-role.kubernetes.io/master:""和RuntimeClassapiVersion:node.k8s.io/v1handler:runckind:RuntimeClassscheduling:nodeSelector:node-pool.lailin.xyz/master:"8"node-pool.lailin.xyz/test:"2"tolerations:-effect:NoSchedulekey:node-pool.lailin.xyzoperator:Equalvalue:master我们更新查看NodePoolapiVersion:nodes.lailin.xyz/v1kind:NodePoolmetadata:name:masterspec:taints:-key:node-pool.lailin.xyzvalue:mastereffect:NoSchedulelabels:+"node-pool.lailin.xyz/master":"10"-"node-pool.lailin.xyz/master":"8"-"node-pool.lailin.xyz/test":"2"handler:runc可以看到RuntimeClassscheduling:nodeSelector:node-pool.lailin.xyz/master:"10"tolerations:-effect:NoSchedulekey:node-pool.lailin.xyzoperator:Equalvalue:master和node对应的label信息相应改变了labels:beta.kubernetes.io/arch:amd64beta.kubernetes.io/os:linuxkubernetes.io/arch:amd64kubernetes.io/hostname:kind-control-planekubernetes.io/os:linuxnode-pool.lailin.xyz/master:"10"node-role.kubernetes.io/control-plane:""节点角色.kubernetes.io/master:""预删除:Finalizers我们可以直接使用kubectldeleteNodePoolname删除对应的对象,但是这样可以发现一个问题,就是NodePool创建的RuntimeClass和它维护的NodeTaintLabels没有被清理向上。当我们要删除一个对象时,当我们清理我们要清理的信息时,我们可以使用Finalizers特性来进行预删除操作。k8s的资源对象中有一个Finalizers字段。该字段是一个字符串列表。当删除资源对象时,k8s会先更新DeletionTimestamp时间戳,然后检查Finalizers是否为空。如果为空,则执行Remove逻辑。所以我们可以利用这个特性来进行一些预删除操作。注意:预删除必须是幂等的func(r*NodePoolReconciler)Reconcile(ctxcontext.Context,reqctrl.Request)(ctrl.Result,error){_=r.Log.WithValues("nodepool",req.NamespacedName)//......+//进入预删除流程+if!pool.DeletionTimestamp.IsZero(){+returnctrl.Result{},r.nodeFinalizer(ctx,pool,nodes.Items)+}+//如果删除时间戳为空,说明现在不需要删除数据,我们在资源中添加nodeFinalizer+if!containsString(pool.Finalizers,nodeFinalizer){+pool.Finalizers=append(pool.Finalizers,nodeFinalizer)+iferr:=r.Client.Update(ctx,pool);err!=nil{+returnctrl.Result{},err+}+}//...}预删除逻辑如下//节点预删除逻辑func(r*NodePoolReconciler)nodeFinalizer(ctxcontext.Context,pool*nodesv1.NodePool,nodes[]corev1.Node)error{//如果不为空则表示进入预删除流程for_,n:=rangenodes{n:=n//更新节点Label和污点信息err:=r.Update(ctx,pool.Spec.CleanNode(n))iferr!=nil{returnerr}}//预删除完成,removenodeFinalizerpool.Finalizers=removeString(pool.Finalizers,nodeFinalizer)returnr.Client.Update(ctx,pool)}我们执行kubectldeleteNodePoolmaster,然后得到节点信息。我们可以发现,除了kubernetes标签外,NodePool上附加的所有标签都被删除了。标签s:beta.kubernetes.io/arch:amd64beta.kubernetes.io/os:linuxkubernetes.io/arch:amd64kubernetes.io/hostname:kind-control-planekubernetes.io/os:linuxnode-role.kubernetes.io/control-plane:""node-role.kubernetes.io/master:""OwnerReference上面我们在使用Finalizer的时候,只处理了Node的相关数据,没有处理RuntimeClass。可以用同样的方法处理吗?当然,这是可能的。但它不够优雅。对于这种一对一的映射或者用它创建的资源,更好的做法是在子资源的OwnerReference中加上对应的id,这样当我们删除对应的NodePool时,所有OwnerReference为这个对象会被删除,我们就不用自己去处理这些逻辑了。func(r*NodePoolReconciler)Reconcile(ctxcontext.Context,reqctrl.Request)(ctrl.Result,error){//...//如果不存在则新建ifruntimeClass.Name=""{+runtimeClass=池.RuntimeClass()+err=ctrl.SetControllerReference(pool,runtimeClass,r.Scheme)+iferr!=nil{+returnctrl.Result{},err+}+err=r.Create(ctx,runtimeClass)-err=r.Create(ctx,pool.RuntimeClass())returnctrl.Result{},err}//...}创建时使用controllerutil.SetOwnerReference设置OwnerReference,然后我们可以尝试删除发现RuntimeClass也是相同并被删除。请注意,RuntimeClass是集群级资源。我们一开始创建的NodePool是Namespace级别的,直接运行会报错,因为Cluster级别的OwnerReference不允许是Namespace资源。这需要在api/v1/nodepool_types.go中添加一行注释,指定为Cluster级别//+kubebuilder:object:root=true+//+kubebuilder:resource:scope=Cluster//+kubebuilder:subresource:status//NodePoolistheSchemaforthenodepoolsAPItypeNodePoolstruct{修改后需要先执行makeuninstall再执行makeinstall来总结回顾一下。在本文中,我们实现了一个NodePoolOperator来控制节点和对应的RuntimeClass。除了基本的CURD,我们还了解了预删除和OwnerReference的使用方式。过去,kubectl有时会在删除某个资源时卡住。这实际上是因为预删除操作可能比较慢,或者它可能在预删除期间返回了错误。在下一篇文章中,让我们为我们的Operator添加Event和Status。参考文献[^1]:ContainerRuntimeClass(运行时类):https://kubernetes.io/zh/docs/concepts/containers/runtime-class/[^2]:kubebuilder的高级使用:https://zhuanlan。zhihu.com/p/144978395[^3]:kubebuilder2.0学习笔记-构建和使用https://segmentfault.com/a/1190000020338350[^4]:KiND-我是如何浪费一天加载本地Docker镜像的:https://iximiuz.com/en/posts/kubernetes-kind-load-docker-image/