注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
在前两天的文章当中我们搭建好了本地的 K8s 开发环境,并且了解了 kubebuilder 的基本使用方法,今天就从我之前遇到的一个真实需求出发完整的写一个 Operator
需求分析 背景 在 K8s 运行的过程当中我们发现总是存在一些业务由于安全,可用性等各种各样的原因需要跑在一些独立的节点池上,这些节点池里面可能再划分一些小的节点池。
虽然我们可以使用 Taint
,Label
对节点进行划分,使用 nodeSelector
和 tolerations
让 Pod 跑在指定的节点上,但是这样主要会有两个问题:
一个是管理上不方便,在实际的使用过程中我们会发现存在错配漏配的情况 虽然在 v1.16
之后也可以使用 RuntimeClass
来简化 pod 的配置,但是 RuntimClass
并不和节点进行关联 另一个就是拓展需求不好实现,例如我们想要的某个节点属于网段或者当节点加入这个节点池自动开墙等 需求 对应用来说我们可以在创建或者更新应用时便捷的选择的对应的节点池,默认情况下不需要进行选择 对于节点池来说一个节点池可能有多个节点,并且一个节点也可能同时属于多个节点池 不同节点池的标签、污点信息可能不同 后续可以支持不同节点池的机型、安全组或者防火墙策略不同等 MVP
版本支持标签、污点即可 方案设计 节点池资源如下
1 2 3 4 5 6 7 8 9 10 11 apiVersion: nodes.lailin.xyz/v1 kind: NodePool metadata: name: test spec: taints: - key: node-pool.lailin.xyz value: test effect: NoSchedule labels: node-pool.lailin.xyz/test: ""
节点和节点池之间的映射如何建立?
Pod 和节点池之间的映射如何建立?
我们可以复用 RuntimeClass
对象,当创建一个 NodePool 对象的时候我们就创建一个对应的 RuntimeClass
对象,然后在 Pod
中只需要加上 runtimeClassName: myclass
就可以了 注: 对于 MVP 版本来说其实我们不需要使用自定义资源,只需要通过标签和 RuntimeClass 结合就能满足需求,但是这里为了展示一个完整的流程,我们使用了自定义资源
开发 创建项目 1 2 3 4 5 kubebuilder init --repo github.com/mohuishou/blog-code/k8s-operator/03-node-pool-operator --domain lailin.xyz --skip-go-version-check kubebuilder create api --group nodes --version v1 --kind NodePool
定义对象 1 2 3 4 5 6 7 8 type NodePoolSpec struct { Taints []v1.Taint `json:"taints,omitempty"` Labels map [string ]string `json:"labels,omitempty"` }
创建 我们实现 Reconcile
函数,req
会返回当前变更的对象的 Namespace
和Name
信息,有这两个信息,我们就可以获取到这个对象了,所以我们的操作就是
获取 NodePool
对象 通过 NodePool
对象生成对应的 Label
查找是否已经存在对应的 Label
的 Node如果存在,就给对应的 Node
加上对应的 Taint
和 Label
如果不存在就跳过 通过 NodePool
生成对应的 RuntimeClass
,查找是否已经存在对应的 RuntimeClass
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 func (r *NodePoolReconciler) Reconcile (ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = r.Log.WithValues("nodepool" , req.NamespacedName) pool := &nodesv1.NodePool{} if err := r.Get(ctx, req.NamespacedName, pool); err != nil { return ctrl.Result{}, err } var nodes corev1.NodeList err := r.List(ctx, &nodes, &client.ListOptions{LabelSelector: pool.NodeLabelSelector()}) if client.IgnoreNotFound(err) != nil { return ctrl.Result{}, err } if len (nodes.Items) > 0 { r.Log.Info("find nodes, will merge data" , "nodes" , len (nodes.Items)) for _, n := range nodes.Items { n := n err := r.Patch(ctx, pool.Spec.ApplyNode(n), client.Merge) if err != nil { return ctrl.Result{}, err } } } var runtimeClass v1beta1.RuntimeClass err = r.Get(ctx, client.ObjectKeyFromObject(pool.RuntimeClass()), &runtimeClass) if client.IgnoreNotFound(err) != nil { return ctrl.Result{}, err } if runtimeClass.Name == "" { err = r.Create(ctx, pool.RuntimeClass()) if err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, nil }
更新 相信聪明的你已经发现上面的创建逻辑存在很多的问题
如果 NodePool
对象更新,Node
是否更新对应的 Taint
和Label
如果 NodePool
删除了一个 Label
或Taint
对应 Node
的Label
或Taint
是否需要删除,怎么删除? 如果 NodePool
对象更新,RuntimeClass
是否更新,如何更新 我们 MVP
版本实现可以简单一些,我们约定,所有属于 NodePool
的节点 Tanit
和Label
信息都应该由 NodePool
管理,key 包含 kubernetes 标签污点除外
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 func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // .... if len(nodes.Items) > 0 { r.Log.Info("find nodes, will merge data", "nodes", len(nodes.Items)) for _, n := range nodes.Items { n := n // 更新节点的标签和污点信息+ err := r.Update(ctx, pool.Spec.ApplyNode(n)) - err := r.Patch(ctx, pool.Spec.ApplyNode(n), client.Merge) if err != nil { return ctrl.Result{}, err } } } //... // 如果存在则更新+ err = r.Client.Patch(ctx, pool.RuntimeClass(), client.Merge) + if err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err }
ApplyNode
方法如下所示,主要是修改节点的标签和污点信息
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 func (s *NodePoolSpec) ApplyNode (node corev1.Node) *corev1 .Node { nodeLabels := map [string ]string {} for k, v := range node.Labels { if strings.Contains(k, "kubernetes" ) { nodeLabels[k] = v } } for k, v := range s.Labels { nodeLabels[k] = v } node.Labels = nodeLabels var taints []corev1.Taint for _, taint := range node.Spec.Taints { if strings.Contains(taint.Key, "kubernetes" ) { taints = append (taints, taint) } } node.Spec.Taints = append (taints, s.Taints...) return &node }
我们使用 make run
将服务跑起来测试一下
首先我们准备一份 NodePool 的 CRD,使用 kubectl apply -f config/samples/
部署一下
1 2 3 4 5 6 7 8 9 10 11 12 13 apiVersion: nodes.lailin.xyz/v1 kind: NodePool metadata: name: master spec: taints: - key: node-pool.lailin.xyz value: master effect: NoSchedule labels: "node-pool.lailin.xyz/master": "8" "node-pool.lailin.xyz/test": "2" handler: runc
部署之后可以获取到节点的标签
1 2 3 4 5 6 7 8 9 10 labels: beta.kubernetes.io/arch: amd64 beta.kubernetes.io/os: linux kubernetes.io/arch: amd64 kubernetes.io/hostname: kind-control-plane kubernetes.io/os: linux node-pool.lailin.xyz/master: "8" node-pool.lailin.xyz/test: "2" node-role.kubernetes.io/control-plane: "" node-role.kubernetes.io/master: ""
以及 RuntimeClass
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: node.k8s.io/v1 handler: runc kind: RuntimeClass scheduling: nodeSelector: node-pool.lailin.xyz/master: "8" node-pool.lailin.xyz/test: "2" tolerations: - effect: NoSchedule key: node-pool.lailin.xyz operator: Equal value: master
我们更新一下 NodePool
1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVersion: nodes.lailin.xyz/v1 kind: NodePool metadata: name: master spec: taints: - key: node-pool.lailin.xyz value: master effect: NoSchedule labels:+ "node-pool.lailin.xyz/master": "10" - "node-pool.lailin.xyz/master": "8" - "node-pool.lailin.xyz/test": "2" handler: runc
可以看到 RuntimeClass
1 2 3 4 5 6 7 8 scheduling: nodeSelector: node-pool.lailin.xyz/master: "10" tolerations: - effect: NoSchedule key: node-pool.lailin.xyz operator: Equal value: master
和节点对应的标签信息都有了相应的变化
1 2 3 4 5 6 7 8 9 labels: beta.kubernetes.io/arch: amd64 beta.kubernetes.io/os: linux kubernetes.io/arch: amd64 kubernetes.io/hostname: kind-control-plane kubernetes.io/os: linux node-pool.lailin.xyz/master: "10" node-role.kubernetes.io/control-plane: "" node-role.kubernetes.io/master: ""
预删除: Finalizers 我们可以直接使用 kubectl delete NodePool name
删除对应的对象,但是这样可以发现一个问题,就是 NodePool 创建的 RuntimeClass 以及其维护的 Node Taint Labels 等信息都没有被清理。
当我们想要再删除一个对象的时候,清理一写想要清理的信息时,我们就可以使用 Finalizers
特性,执行预删除的操作。
k8s 的资源对象当中存在一个 Finalizers
字段,这个字段是一个字符串列表,当执行删除资源对象操作的时候,k8s 会先更新 DeletionTimestamp
时间戳,然后会去检查 Finalizers
是否为空,如果为空才会执行删除逻辑。所以我们就可以利用这个特性执行一些预删除的操作。注意:预删除必须是幂等的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = r.Log.WithValues("nodepool", req.NamespacedName) // ......+ // 进入预删除流程 + if !pool.DeletionTimestamp.IsZero() { + return ctrl.Result{}, r.nodeFinalizer(ctx, pool, nodes.Items) + } + // 如果删除时间戳为空说明现在不需要删除该数据,我们将 nodeFinalizer 加入到资源中 + if !containsString(pool.Finalizers, nodeFinalizer) { + pool.Finalizers = append(pool.Finalizers, nodeFinalizer) + if err := r.Client.Update(ctx, pool); err != nil { + return ctrl.Result{}, err + } + } // ...... }
预删除的逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (r *NodePoolReconciler) nodeFinalizer (ctx context.Context, pool *nodesv1.NodePool, nodes []corev1.Node) error { for _, n := range nodes { n := n err := r.Update(ctx, pool.Spec.CleanNode(n)) if err != nil { return err } } pool.Finalizers = removeString(pool.Finalizers, nodeFinalizer) return r.Client.Update(ctx, pool) }
我们执行 kubectl delete NodePool master
然后再获取节点信息可以发现,除了 kubernetes
的标签其他 NodePool 附加的标签都已经被删除掉了
1 2 3 4 5 6 7 8 labels: beta.kubernetes.io/arch: amd64 beta.kubernetes.io/os: linux kubernetes.io/arch: amd64 kubernetes.io/hostname: kind-control-plane kubernetes.io/os: linux node-role.kubernetes.io/control-plane: "" node-role.kubernetes.io/master: ""
OwnerReference 我们上面使用 Finalizer
的时候只处理了 Node 的相关数据,没有处理 RuntimeClass,能不能用相同的方式进行处理呢?当然是可以的,但是不够优雅。
对于这种一一映射或者是附带创建出来的资源,更好的方式是在子资源的 OwnerReference 上加上对应的 id,这样我们删除对应的 NodePool 的时候所有 OwnerReference 是这个对象的对象都会被删除掉,就不用我们自己对这些逻辑进行处理了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { //... // 如果不存在创建一个新的 if runtimeClass.Name == "" {+ runtimeClass = pool.RuntimeClass() + err = ctrl.SetControllerReference(pool, runtimeClass, r.Scheme) + if err != nil { + return ctrl.Result{}, err + } + err = r.Create(ctx, runtimeClass) - err = r.Create(ctx, pool.RuntimeClass()) return ctrl.Result{}, err } // ... }
在创建的时候使用 controllerutil.SetOwnerReference
设置一下 OwnerReference 即可,然后我们再试试删除就可以发现 RuntimeClass 也一并被删除了。
注意,RuntimeClass 是一个集群级别的资源,我们最开始创建的 NodePool 是 Namespace 级别的,直接运行会报错,因为 Cluster 级别的 OwnerReference 不允许是 Namespace 的资源。
这个需要在 api/v1/nodepool_types.go
添加一行注释,指定为 Cluster 级别
1 2 3 4 5 6 //+kubebuilder:object:root=true+//+kubebuilder:resource:scope=Cluster //+kubebuilder:subresource:status // NodePool is the Schema for the nodepools API type NodePool struct {
修改之后我们需要先执行 make uninstall
然后再执行 make install
总结 回顾一下,这篇文章我们实现了一个 NodePool 的 Operator 用来控制节点以及对应的 RuntimeClass,除了基本的 CURD 之外我们还学习了预删除和 OwnerReference 的使用方式。之前在 kubectl delete 某个资源的时候有时候会卡住,这个其实是因为在执行预删除的操作,可能本来也比较慢,也有可能是预删除的时候返回了错误导致的。
下一篇我们一起来为我们的 Operator 加上 Event 和 Status。
参考文献 关注我获取更新 猜你喜欢