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 中可能会有多个版本(如
v1、v1beta1、v1alpha1等),每个版本可能会有不同的功能和行为 - Kind:资源的类型。通常是资源的单数形式,如
Pod、Deployment、Service。 - Resource:资源的名称。通常是复数形式,如
pods、deployments、services
具体区别:
- GVK 里的
Kind是单数;GVR 里的Resource是复数 - GVK用于标识k8s资源的类型,资源的序列化和反序列化;GVR用于标识 k8s Restful API的端点,适用于client-go 访问k8s资源
声明式的API
k8s 使用声明式API,所谓声明式就是“你告诉k8s你想要什么,而不是告诉他具体如何做什么命令”
利用kubebuilder进行Operator的开发
大体的开发步骤
- 使用kubebuilder 初始化项目
- 创建api对象,填写spec和status
- 实现controller
- 创建webhook(可选)
- 本地开发调试
- 构建镜像并生成部署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