在前两天的文章当中我们搭建好了本地的 K8s 开发环境,并且了解了 kubebuilder 的基本使用方法,今天就从我之前遇到的一个真实需求出发完整的写一个 Operator

在 K8s 运行的过程当中我们发现总是存在一些业务由于安全,可用性等各种各样的原因需要跑在一些独立的节点池上,这些节点池里面可能再划分一些小的节点池。
虽然我们可以使用 Taint,Label对节点进行划分,使用 nodeSelector 和 tolerations让 Pod 跑在指定的节点上,但是这样主要会有两个问题:
1.对应用来说我们可以在创建或者更新应用时便捷的选择的对应的节点池,默认情况下不需要进行选择
2.对于节点池来说
节点池资源如下
- 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: ""
 
节点和节点池之间的映射如何建立?
- NAME STATUS ROLES AGE VERSION
 - kind-control-plane Ready,SchedulingDisabled control-plane,master 2d2h v1.20.2
 
Pod 和节点池之间的映射如何建立?
注: 对于 MVP 版本来说其实我们不需要使用自定义资源,只需要通过标签和 RuntimeClass 结合就能满足需求,但是这里为了展示一个完整的流程,我们使用了自定义资源
- # 初始化项目
 - kubebuilder init --repo github.com/mohuishou/blog-code/k8s-operator/03-node-pool-operator --domain lailin.xyz --skip-go-version-check
 - # 创建 api
 - kubebuilder create api --group nodes --version v1 --kind NodePool
 
- // NodePoolSpec 节点池
 - type NodePoolSpec struct {
 - // Taints 污点
 - Taints []v1.Taint `json:"taints,omitempty"`
 - // Labels 标签
 - Labels map[string]string `json:"labels,omitempty"`
 - }
 
我们实现 Reconcile 函数,req会返回当前变更的对象的 Namespace和Name信息,有这两个信息,我们就可以获取到这个对象了,所以我们的操作就是
1.获取 NodePool 对象
2.通过 NodePool 对象生成对应的 Label 查找是否已经存在对应的 Label 的 Node
3.通过 NodePool 生成对应的 RuntimeClass ,查找是否已经存在对应的 RuntimeClass
- 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
 - }
 
相信聪明的你已经发现上面的创建逻辑存在很多的问题
1.如果 NodePool 对象更新,Node 是否更新对应的 Taint 和Label
2.如果 NodePool 对象更新,RuntimeClass是否更新,如何更新
我们 MVP 版本实现可以简单一些,我们约定,所有属于 NodePool 的节点 Tanit 和Label信息都应该由 NodePool管理,key 包含 kubernetes 标签污点除外
- 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 方法如下所示,主要是修改节点的标签和污点信息
- // ApplyNode 生成 Node 结构,可以用于 Patch 数据
 - func (s *NodePoolSpec) ApplyNode(node corev1.Node) *corev1.Node {
 - // 除了节点池的标签之外,我们只保留 k8s 的相关标签
 - // 注意:这里的逻辑如果一个节点只能属于一个节点池
 - 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/ 部署一下
- 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
 
部署之后可以获取到节点的标签
- 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
- 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
- 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
- scheduling:
 - nodeSelector:
 - node-pool.lailin.xyz/master: "10"
 - tolerations:
 - - effect: NoSchedule
 - key: node-pool.lailin.xyz
 - operator: Equal
 - value: master
 
和节点对应的标签信息都有了相应的变化
- 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: ""
 
我们可以直接使用 kubectl delete NodePool name删除对应的对象,但是这样可以发现一个问题,就是 NodePool 创建的 RuntimeClass 以及其维护的 Node Taint Labels 等信息都没有被清理。
当我们想要再删除一个对象的时候,清理一写想要清理的信息时,我们就可以使用 Finalizers 特性,执行预删除的操作。
k8s 的资源对象当中存在一个 Finalizers字段,这个字段是一个字符串列表,当执行删除资源对象操作的时候,k8s 会先更新 DeletionTimestamp 时间戳,然后会去检查 Finalizers是否为空,如果为空才会执行删除逻辑。所以我们就可以利用这个特性执行一些预删除的操作。注意:预删除必须是幂等的
- 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
 - + }
 - + }
 - // ......
 - }
 
预删除的逻辑如下
- // 节点预删除逻辑
 - 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
 - }
 - }
 - // 预删除执行完毕,移除 nodeFinalizer
 - pool.Finalizers = removeString(pool.Finalizers, nodeFinalizer)
 - return r.Client.Update(ctx, pool)
 - }
 
我们执行 kubectl delete NodePool master 然后再获取节点信息可以发现,除了 kubernetes 的标签其他 NodePool 附加的标签都已经被删除掉了
- 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: ""
 
我们上面使用 Finalizer 的时候只处理了 Node 的相关数据,没有处理 RuntimeClass,能不能用相同的方式进行处理呢?当然是可以的,但是不够优雅。
对于这种一一映射或者是附带创建出来的资源,更好的方式是在子资源的 OwnerReference 上加上对应的 id,这样我们删除对应的 NodePool 的时候所有 OwnerReference 是这个对象的对象都会被删除掉,就不用我们自己对这些逻辑进行处理了。
- 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 级别
- //+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。
参考文献
[^1]: 容器运行时类(Runtime Class):
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 - How I Wasted a Day Loading Local Docker Images:
https://iximiuz.com/en/posts/kubernetes-kind-load-docker-image/
                标题名称:一篇带你kubebuilder实战:CRUD
                
                浏览路径:http://www.csdahua.cn/qtweb/news8/327808.html
            
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网