返回归档
🏹Kubernetes

kubebuilder 总结

文章目录
mindmap
  root((Kubebuilder))
    基本概念
        CRD
            自定义资源定义
            扩展K8s API的方式
        Controller
            资源协调器
            确保实际状态符合期望状态
        Operator
            CRD + Controller
            自动化应用管理
        GVK和GVR
            Group API组
            Version API版本
            Kind/Resource 资源类型
        声明式API
            描述期望状态
            而非具体操作步骤
    开发流程
        安装Kubebuilder
        项目初始化
            domain设置
            repo设置
        创建API
            定义Spec
            定义Status
        实现Controller
            Reconcile循环
            资源管理
        创建Webhook(可选)
            Mutating
            Validating
        本地调试
        部署发布
    Make命令
        开发调试
            make manifests
            make generate
            make run
        代码检查
            make fmt
            make vet
            make lint
        测试
            make test
            make test-e2e
        构建部署
            make docker-build
            make deploy
    核心功能
        Reconcile架构
            获取资源实例
            处理删除逻辑
            确保Finalizer
            状态对比更新
            更新Status
        OwnerReference
            级联删除
            自动触发调谐
        Finalizers
            资源清理保障
            删除保护机制
        Event记录
            重要节点记录
            避免过多事件
        Watch机制
            子资源监听
            手动Watch配置
            Predicates过滤
    Webhook开发
        Mutating
            修改请求对象
            设置默认值
            注入配置
        Validating
            验证合法性
            实施策略
            准入控制

kuberbuilder是什么

Kubebuilder 是一个用于构建 Kubernetes API 和控制器的框架

基本概念

为了更好的使用kubebuilder,可以先看看一下的基础概念。

CRD是什么

CRD(Custom Resource Definition)是k8s中扩展API的方式,它允许你定义一种新的资源类型,看起来和k8s中原生的资源一样。

Controller是什么

Controller负责不断检查集群中的资源对象是否符合预期(Spec),对其不断的协调(reconcile)。

Operator 是什么

Operator是通过自定义资源(CRD)和自定义的控制器(controller),实现对应用的自动化管理。

k8s中资源表示符 GVK 和 GVR

在k8s中**,GVK**(Group, Version, Kind)和 GVR(Group, Version, Resource)是用于标识和访问 Kubernetes 资源的两个重要概念。

  • Group:API 组的名称。例如,apps 组包含 Deployment、StatefulSet 等资源,batch 组包含 Job、CronJob 等资源。
  • Version:API 版本。每个资源在 Kubernetes 中可能会有多个版本(如 v1v1beta1v1alpha1 等),每个版本可能会有不同的功能和行为
  • Kind:资源的类型。通常是资源的单数形式,如 PodDeploymentService
  • Resource:资源的名称。通常是复数形式,如 podsdeploymentsservices

具体区别:

  • GVK 里的 Kind 是单数;GVR 里的 Resource 是复数
  • GVK用于标识k8s资源的类型,资源的序列化和反序列化;GVR用于标识 k8s Restful API的端点,适用于client-go 访问k8s资源

声明式的API

k8s 使用声明式API,所谓声明式就是“你告诉k8s你想要什么,而不是告诉他具体如何做什么命令”

利用kubebuilder进行Operator的开发

大体的开发步骤

  1. 使用kubebuilder 初始化项目
  2. 创建api对象,填写spec和status
  3. 实现controller
  4. 创建webhook(可选)
  5. 本地开发调试
  6. 构建镜像并生成部署manifest

安装kubebuilder

os=$(go env GOOS)
arch=$(go env GOARCH)

# download kubebuilder and extract it to tmp
curl -L https://go.kubebuilder.io/dl/2.2.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)
sudo mv /tmp/kubebuilder_2.2.0_${os}_${arch} /usr/local/kubebuilder
export PATH=$PATH:/usr/local/kubebuilder/bin

项目初始化

mkdir x-operator
cd x-operator
kubebuilder init --domain crd.forrest.com --repo github.com/forrest-tao/x-operator

你的 API Group 最终会是 <resource-name>.<version>.<domain>,例如 application.v1.crd.forrest.com

创建 API对象

kubebuilder create api --group core --version v1 --kind Application --namespaced=true

创建webhook(可选

# GVK 需要和创建 API 时保持一致
kubebuilder create webhook --group core --version v1 --kind Application --defaulting --programmatic-validation

make相关的指令

本地开发和调试

# 生成代码和配置
make manifests    # 生成 CRD、RBAC 等 Kubernetes 资源文件
make generate     # 生成 DeepCopy 等代码

# 代码检查
make fmt         # 格式化代码
make vet         # 运行 go vet 进行代码检查
make lint        # 运行 golangci-lint 进行代码检查

# 测试
make test        # 运行单元测试
make test-e2e    # 运行端到端测试

# 本地运行控制器(开发调试用)
make run         # 在本地运行控制器(不需要构建镜像,直接运行代码)

# 安装 CRD 到集群
make install     # 只安装 CRD 定义到集群
make uninstall   # 从集群中删除 CRD

构建和部署

# 构建
make build            # 构建二进制文件(在 bin/ 目录下)
make docker-build     # 构建 Docker 镜像
make docker-push      # 推送 Docker 镜像到仓库

# 部署到集群
make deploy          # 将控制器部署到集群(作为 Deployment 运行)
make undeploy        # 从集群中删除控制器

reconcile的大体架构模式

一个典型的 Reconcile 实现通常包含以下主要步骤:

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 1. 获取资源实例
    instance := &myv1.MyResource{}
    if err := r.Get(ctx, req.NamespacedName, instance); err != nil {
        // 处理资源不存在的情况
        if errors.IsNotFound(err) {
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    // 2. 处理删除逻辑
    if !instance.DeletionTimestamp.IsZero() {
        // 2.1 检查 Finalizer
        if containsFinalizer(instance.Finalizers) {
            // 2.2 清理关联资源
            if err := r.cleanupResources(ctx, instance); err != nil {
                return ctrl.Result{}, err
            }
            // 2.3 移除 Finalizer
            if err := r.removeFinalizer(ctx, instance); err != nil {
                return ctrl.Result{}, err
            }
        }
        return ctrl.Result{}, nil
    }

    // 3. 确保 Finalizer 存在
    if err := r.ensureFinalizer(ctx, instance); err != nil {
        return ctrl.Result{}, err
    }

    // 4. 获取当前状态(子资源)
    currentState, err := r.getCurrentState(ctx, instance)
    if err != nil {
        return ctrl.Result{}, err
    }

    // 5. 对比并更新资源
    if currentState == nil {
        // 5.1 创建子资源
        if err := r.createResources(ctx, instance); err != nil {
            return ctrl.Result{}, err
        }
    } else {
        // 5.2 检查是否需要更新
        if needsUpdate(currentState, instance) {
            // 5.3 更新子资源
            if err := r.updateResources(ctx, instance); err != nil {
                return ctrl.Result{}, err
            }
        }
    }

    // 6. 更新状态
    if err := r.updateStatus(ctx, instance); err != nil {
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

细节部分

OwnerReference 的使用

  • 用途:实现资源的级联删除和自动触发调谐
  • 实现方式:使用 controllerutil.SetControllerReference(&app, &deploy, r.Scheme)
  • 好处:
    • 自动级联删除子资源
    • 子资源变更自动触发 owner 的调谐
    • 简化代码逻辑

Finalizers 的使用

  • 用途:确保资源删除前进行必要的清理工作

  • 关键逻辑:

    • 新建资源时添加 Finalizer
    • 删除时先执行清理,再移除 Finalizer
  • 实现建议:

    if app.ObjectMeta.DeletionTimestamp.IsZero() {
        // 新对象,添加 Finalizer
        controllerutil.AddFinalizer(&app, AppFinalizer)
    } else {
        // 对象正在删除,执行清理工作
        // 清理完成后移除 Finalizer
        controllerutil.RemoveFinalizer(&app, AppFinalizer)
    }

Event 记录

在Reconciler对象上增加 Recorder字段

// ApplicationReconciler reconciles a Application object
type ApplicationReconciler struct {
    client.Client
    Scheme *runtime.Scheme
    // See that we added the following code to allow us to pass the record.EventRecorder
    Recorder record.EventRecorder
}

同时,启动时初始化

if err = (&controller.ApplicationReconciler{
    Client: mgr.GetClient(),
    Scheme: mgr.GetScheme(),
    // Note that we added the following line:
    Recorder: mgr.GetEventRecorderFor("application-controller"),
}).SetupWithManager(mgr); err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "Application")
    os.Exit(1)
}
  • 建议:只记录重要节点的事件,避免过多事件影响集群性能

  • 实现方式:

    r.Recorder.Eventf(&app, corev1.EventTypeNormal, "Reason", "message")

Watching resources

除了设置OwnerReference,从而来自动监听子资源,也可以手动设置watch的资源是什么。

Watching Secondary Resources that are NOT Owned(如何监听不是子资源的变更

通过 EnqueueRequestsFromMapFunc 从 Deploy 触发 Application。

// SetupWithManager sets up the controller with the Manager.
func (r *AppServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&batchv1.AppService{}).
		Watches(&appsv1.Deployment{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request {
			app, ok := object.GetLabels()["app"]
			if !ok {
				return nil
			}
			return []reconcile.Request{
				{
					NamespacedName: types.NamespacedName{
						Namespace: object.GetNamespace(),
						Name:      app,
					},
				},
			}
		})).Named("AppService").
		Complete(r)
}

这里的逻辑是:如果一个带有app label的Deployment发送变化时,controller会自动触发AppService的reconcile。这是一种补充机制,用于处理那些可能与AppService 相关,但不是通过 OwnerReference 直接关联的 Deployment 资源。

using predicates to refine watches(优化watch

Predicates 是一种事件过滤机制,用于优化控制器的调谐触发条件,确保只对关心的变更做出响应。

定义一种predicate

var updatePred = predicate.Funcs{
    // 更新事件过滤
    UpdateFunc: func(e event.UpdateEvent) bool {
        oldObj := e.ObjectOld.(*v1.Application)
        newObj := e.ObjectNew.(*v1.Application)
        
        // 只在 image 或 enabled 字段变化时触发
        return oldObj.Spec.Image != newObj.Spec.Image || 
               oldObj.Spec.Enabled != newObj.Spec.Enabled
    },
    
    // 其他事件默认允许
    CreateFunc:  func(e event.CreateEvent) bool { return true },
    DeleteFunc:  func(e event.DeleteEvent) bool { return true },
    GenericFunc: func(e event.GenericEvent) bool { return true },
}

在manager中使用predicate

ctrl.NewControllerManagedBy(mgr).
    For(&v1.Application{}).
    WithEventFilter(updatePred).  // 应用过滤器
    Complete(r)

Webhook的开发

k8s 中的Admission Webhook 

mutating Admission webhook

  • 执行时机:对象持久化之前执行
  • 主要的功能:
    • 修改请求中的对象
    • 设置一些默认值
    • 注入sidecar容器,修改或添加label/annotation等等

比较常见的使用场景有:自动注入service Account、为pod添加envs、自动添加sidecar(比如istio中)、设置资源的默认值

validating Admission Webhook

  • 执行时机:在 Mutating webhook 之后执行
  • 主要功能
    • 验证请求的合法性
    • 实施自定义策略
    • 拒绝不符合要求的请求

比较常见的使用场景有:强制资源的命名规范、验证资源的配置是否符合安全策略、确保镜像来源的可靠性、限制资源的配额等等

整体的执行顺序是

flowchart LR
    A[API 请求] --> B{认证/授权}
    B --> C[Mutating Webhook]
    C --> D[Validating Webhook]
    D --> E[对象持久化]
    E --> F[(etcd)]

    subgraph Mutating处理
    C --> C1[设置默认值]
    C --> C2[注入Sidecar]
    C --> C3[修改配置]
    end

    subgraph Validating处理
    D --> D1[验证配置]
    D --> D2[检查策略]
    D --> D3[准入控制]
    end

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#dfd,stroke:#333,stroke-width:2px
    style D fill:#fdd,stroke:#333,stroke-width:2px
    style E fill:#ddd,stroke:#333,stroke-width:2px
    style F fill:#ff9,stroke:#333,stroke-width:2px

Reference

bookmark

bookmark

bookmark