Kubebuilder 架构介绍

1、Kubebuilder 介绍

Kubebuilder 是一个用 Go 语言构建 Kubernetes APIs 的框架,通过使用 Kubebuilder,用户可以遵循一套简单的编程框架,使用 CRD 构建API、Controllers 和 Admission WebHooks,实现对 k8s 的扩展。

Kubebuilder 中的主要组件包含 Manager、Cache、Client 与 Finalizers。

  • Manager组件主要实现管理外层,负责初始化 Controller、Cache、Client的工作;
  • Cache 组件负责生成 ShareInformer,Watch关注的GVK下的GVR的变化(增、删、改),以触发Controller 的Reconcile逻辑;
  • Client 组件在工作中实现对资源进行 CURD 操作,CURD操作封装到Client中进行,其中的写操作(增、删、改)直接访问APIServer,读操作(查)对接的是本地的Cache;
  • Finzlizers组件主要用于处理Kubernetes资源的预删除逻辑,保障资源被删除后能够从Cache中读取到,清理相关的其它资源。

Kubebuilder 是个脚手架中间,是将 Kubernetes 的可扩展能力CRD进行了简化封装。kubebuilder 大致划分为四大块:User Defined、API Scaffolds、Controller Runtime、Kubernetes集群。

当自定义好的 CRD 结构,想要在 Kubernetes 集群中实现这样的 CRD 结构定义, 这时候需要 Reconcile 去协调。定义好的 CRD 需要安装然后运行 Controller,这个 Controller 的运行是通过 Controller Runtime 库实现。

在 Controller Runtime 模块中,k8s 构建出来的 CRD 会注册到 Scheme 模块,它会提供 Kinds 与对应的 Go Type 的映射,就能知道它的 GKV(Group Kind Version)。

2、Kubebuilder 模块分析

2.1、CRD 创建命令
shell 复制代码
kubebuilder create api --group demo --version v1 --kind Demo
kubebuilder create api --group ship --version v1beta1 --kind Test1
kubebuilder create api --group ship --version v1beta1 --kind Test2
  • 自定义 CRD,Group 表示 CRD 所属的组,它可以支持多种不同版本、不同类型的资源构建;

  • Version 表示 CRD 的版本号;

  • Kind 表示 CRD 的类型;

上面创建了 1 个 v1 版本的 Demo 类型的资源,它会自动生成了 {kind}types.go 的文件, 即 demo_types.go ;同时创建了 2 个 v1beta1 版本的不 同类型的资源, 可以看到生成了 2 个资源文件, 分别是 test1_types.go、test2_types.go。目录如下:

shell 复制代码
➤ tree api/                                                                                                                                                    
api/
├── v1
│   ├── demo_types.go
│   ├── groupversion_info.go
│   └── zz_generated.deepcopy.go
└── v1beta1
    ├── groupversion_info.go
    ├── test1_types.go
    ├── test2_types.go
    └── zz_generated.deepcopy.go

demo_types.go 文件里面内容

go 复制代码
type Demo struct {  
   metav1.TypeMeta   `json:",inline"`  
   metav1.ObjectMeta `json:"metadata,omitempty"`  
  
   Spec   DemoSpec   `json:"spec,omitempty"`  
   Status DemoStatus `json:"status,omitempty"`  
}

type DemoList struct {  
   metav1.TypeMeta `json:",inline"`  
   metav1.ListMeta `json:"metadata,omitempty"`  
   Items           []Demo `json:"items"`  
}

func init() {  
   SchemeBuilder.Register(&Demo{}, &DemoList{})  
}

Demo 资源结构体包含:Metadata、Spec、Status、及继承的 Kubernetes 资源属性, 如 kind、 apiVersion 等

DemoList 表明资源的列表结构体,当用户查询这一类资源时,各 demo 的内容放在了 Items 键的下面。

init() 初始化函数的作用是将资源的类型注册到 Scheme 对应的 demo 组的 v1 版本下。

每一个 CRD, 默认会创建对应的 {kind}controller.go 文件, 如 demo_controller.go, 这 就是 CRD Controller 逻辑构造的了。

shell 复制代码
➤ tree controllers/                                                                                                                                            
controllers/
├── demo_controller.go
├── suite_test.go
├── test1_controller.go
└── test2_controller.go

demo_controller.go 代码里面,自动生成的 Reconciler 的对象名称是 {kind}Reconciler,它的主方法是 Reconcile(),即通过在这个函数的空白处填入逻辑完成对应的 CRD 构造工作。SetupWithManager 方法作用是用于 CRD Controller 的安装,这样CRD Controller 才能运行。、

go 复制代码
func (r *DemoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {  
   _ = log.FromContext(ctx)  
  
   // TODO(user): your logic here  
  
   return ctrl.Result{}, nil  
}  

// 它用于 CRD Controller 的安装。安 装完成后,CRD Controller 才能运行,  
func (r *DemoReconciler) SetupWithManager(mgr ctrl.Manager) error {  
   return ctrl.NewControllerManagedBy(mgr).  
      For(&demov1.Demo{}).  
      Complete(r)  
}
2.2、Manager 初始化

在 main 文件,Manager 初始化是借助于 ctrl.NewManager 方法实现,进去原来是 controller-runtime 包的 manager.New方法。

go 复制代码
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{  
   Scheme:                 scheme, // 将 crd 加入到scheme 中  
   MetricsBindAddress:     metricsAddr,  
   Port:                   9443,  
   HealthProbeBindAddress: probeAddr,  
   LeaderElection:         enableLeaderElection,  
   LeaderElectionID:       "ecaf1259.my.domain",  
})
if err != nil {  
   setupLog.Error(err, "unable to start manager")  
   os.Exit(1)  
}

在 New 方法中,实际是根据传入的参数 进行 Manager 对象的 Scheme、Cache、Client 等模块的初始化构建。

go 复制代码
// sigs.k8s.io/controller-runtime@v0.13.0/pkg/manager/manager.go
func New(config *rest.Config, options Options) (Manager, error) {  
   // Set default values for options fields  
   options = setOptionsDefaults(options)  
  
   cluster, err := cluster.New(config, func(clusterOptions *cluster.Options) {  
      clusterOptions.Scheme = options.Scheme  
      clusterOptions.MapperProvider = options.MapperProvider  
      clusterOptions.Logger = options.Logger  
      clusterOptions.SyncPeriod = options.SyncPeriod  
      clusterOptions.Namespace = options.Namespace  
      clusterOptions.NewCache = options.NewCache  
      clusterOptions.NewClient = options.NewClient  
      clusterOptions.ClientDisableCacheFor = options.ClientDisableCacheFor  
      clusterOptions.DryRunClient = options.DryRunClient  
      clusterOptions.EventBroadcaster = options.EventBroadcaster //nolint:staticcheck  
   })  
   if err != nil {  
      return nil, err  
   }  
    
   recorderProvider, err := options.newRecorderProvider(config, cluster.GetScheme(), options.Logger.WithName("events"), options.makeBroadcaster)  
   if err != nil {  
      return nil, err  
   }  
  
   // Create the resource lock to enable leader election)  
   var leaderConfig *rest.Config  
   var leaderRecorderProvider *intrec.Provider  
  
   if options.LeaderElectionConfig == nil {  
      leaderConfig = rest.CopyConfig(config)  
      leaderRecorderProvider = recorderProvider  
   } else {  
      leaderConfig = rest.CopyConfig(options.LeaderElectionConfig)  
      leaderRecorderProvider, err = options.newRecorderProvider(leaderConfig, cluster.GetScheme(), options.Logger.WithName("events"), options.makeBroadcaster)  
      if err != nil {  
         return nil, err  
      }  
   }  
  
   resourceLock, err := options.newResourceLock(leaderConfig, leaderRecorderProvider, leaderelection.Options{  
      LeaderElection:             options.LeaderElection,  
      LeaderElectionResourceLock: options.LeaderElectionResourceLock,  
      LeaderElectionID:           options.LeaderElectionID,  
      LeaderElectionNamespace:    options.LeaderElectionNamespace,  
   })  
   if err != nil {  
      return nil, err  
   }  
  
   // Create the metrics listener. This will throw an error if the metrics bind  
   // address is invalid or already in use.   
   metricsListener, err := options.newMetricsListener(options.MetricsBindAddress)  
   if err != nil {  
      return nil, err  
   }  
  
   // By default we have no extra endpoints to expose on metrics http server.  
   metricsExtraHandlers := make(map[string]http.Handler)  
  
   // Create health probes listener. This will throw an error if the bind  
   // address is invalid or already in use.   
   healthProbeListener, err := options.newHealthProbeListener(options.HealthProbeBindAddress)  
   if err != nil {  
      return nil, err  
   }  
  
   errChan := make(chan error)  
   runnables := newRunnables(options.BaseContext, errChan)  
  
   return &controllerManager{...}, nil  
}
2.3、Manager 启动

启动 Cache

go 复制代码
// sigs.k8s.io/controller-runtime@v0.13.0/pkg/cache/multi_namespace_cache.go
func (c *multiNamespaceCache) Start(ctx context.Context) error {  
   // start global cache  
   go func() {  
      err := c.clusterCache.Start(ctx)  
      if err != nil {  
         log.Error(err, "cluster scoped cache failed to start")  
      }  
   }()  
  
   // start namespaced caches  
   for ns, cache := range c.namespaceToCache {  
      go func(ns string, cache Cache) {  
	     // namespaceToCache 存储每个 ns 的 cache,默认是 InformersMap 类型
         err := cache.Start(ctx)  // chach 入口
         if err != nil {  
            log.Error(err, "multinamespace cache failed to start namespaced informer", "namespace", ns)  
         }  
      }(ns, cache)  
   }  
  
   <-ctx.Done()  
   return nil  
}

InformersMap 抽象出 3 个 Map 结构,存储不同的 Informer

go 复制代码
func (m *InformersMap) Start(ctx context.Context) error {  
   go m.structured.Start(ctx)  
   go m.unstructured.Start(ctx)  
   go m.metadata.Start(ctx)  
   <-ctx.Done()  
   return nil  
}

启动每个 informer

go 复制代码
func (ip *specificInformersMap) Start(ctx context.Context) {  
   func() {  
      ip.mu.Lock()  
      defer ip.mu.Unlock()  
  
      // Set the stop channel so it can be passed to informers that are added later  
      ip.stop = ctx.Done()  
  
      // Start each informer  
      for _, informer := range ip.informersByGVK {  
         go informer.Informer.Run(ctx.Done())  
      }  
  
      // Set started to true so we immediately start any informers added later.  
      ip.started = true  
      close(ip.startWait)  
   }()  
   <-ctx.Done()  
}

Cache 的核心逻辑是初始化内部所有的 Informer, 初始化 Informer 后就创建了 Reflector 和内部 Controller,Reflector 和 Controller 两个组件是一个"生产者---消费者" 模型,Reflector 负责监听 APIServer 上指定的 GVK 资源的变化,然后将变更写入 delta 队列中,Controller 负责消费这些变更的事件,然后更新本地 Indexer,最后计算出是创建、 更新,还是删除事件,推给我们之前注册的 Watch Handler。

2.4、Controller 初始化

Kubebuilder 脚手架生成后 controller 文件里,CRD 的 Controller 初始化的核心代码是 SetupWithManager 方法。

go 复制代码
// 它用于 CRD Controller 的安装。安装完成后,CRD Controller 才能运行,  
func (r *DemoReconciler) SetupWithManager(mgr ctrl.Manager) error {  
   return ctrl.NewControllerManagedBy(mgr).  
      For(&demov1.Demo{}).  
      Complete(r)  
}

完成 CRD 在 Manager 对象中的安装,最后通过 Manager 对象的 start 方法来完成 CRD Controller 的运行。

使用 Controller-runtime 包初始化 Builder 对象,当它完成 Complete 方法时,实 际完成了 CRD Reconciler 对象的初始化,而这个对象是一个接口方法,它必须实现 Reconcile 方法。

go 复制代码
type Builder struct {  
   forInput         ForInput  
   ownsInput        []OwnsInput  
   watchesInput     []WatchesInput  
   mgr              manager.Manager  
   globalPredicates []predicate.Predicate  
   ctrl             controller.Controller  
   ctrlOptions      controller.Options  
   name             string  
}

func ControllerManagedBy(m manager.Manager) *Builder {  
   return &Builder{mgr: m}  
}

func (blder *Builder) Complete(r reconcile.Reconciler) error {  
   _, err := blder.Build(r)  
   return err  
}  
  
// Build builds the Application Controller and returns the Controller it created.
func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {  
   if r == nil {  
      return nil, fmt.Errorf("must provide a non-nil Reconciler")  
   }  
   if blder.mgr == nil {  
      return nil, fmt.Errorf("must provide a non-nil Manager")  
   }  
   if blder.forInput.err != nil {  
      return nil, blder.forInput.err  
   }  
   // Checking the reconcile type exist or not  
   if blder.forInput.object == nil {  
      return nil, fmt.Errorf("must provide an object for reconciliation")  
   }  
  
   // Set the ControllerManagedBy  
   if err := blder.doController(r); err != nil {  
      return nil, err  
   }  
  
   // Set the Watch  
   if err := blder.doWatch(); err != nil {  
      return nil, err  
   }  
  
   return blder.ctrl, nil  
}
相关推荐
阿里云云原生12 小时前
不改代码也能监控 AI Agent?揭秘 OBI 如何在内核层精准解析 GenAI 语义流量
云原生·agent
众人皆醒我独醉13 小时前
Pod 一直 Terminating,死活删不掉?三个凶手,每一个你都意想不到
面试·kubernetes
飞翔沫沫情14 小时前
K8s Alloy 采集 Pod 控制台日志
云原生·容器·kubernetes
潮起鲸落入海20 小时前
Kubernetes Metric Server, Quota and Limits
容器·kubernetes
梦想的颜色20 小时前
Docker 容器化本地部署 Claude Code 完整硬核教程|隔离沙盒 + 国产模型直连 + 持久化 Skill
运维·容器·ai 工程化·沙盒隔离·docker 本地部署·claude code硬核实战·国内国产模型接入
摇滚侠20 小时前
云原生 Java 架构师的第一课 K8s+Docker+KubeSphere+DevOps 19-25
java·云原生·kubernetes
渣渣盟20 小时前
Docker 运维常用命令手册(含扩容与实战)
运维·docker·容器
我叫张小白。1 天前
一个微服务电商+社区项目(瓷韵app)的技术深度复盘
docker·微服务·云原生·架构
咕噜咕噜的猪大侠1 天前
《K8s 核心知识梳理:Deployment 滚动更新、JobCronJob、DaemonSet 和 Service》
云原生·容器·kubernetes
虚心的百褶裙1 天前
远程调用服务架构设计及zookeeper技术详解(下篇)
分布式·zookeeper·云原生