3. 第一个控制器脚手架:使用 kubebuilder init 创建项目、理解项目目录结构、运行第一个空 Reconciler
本章是动手实战的起点。我们将使用 kubebuilder 初始化一个标准的控制器项目,逐层拆解其目录结构,并运行一个不包含任何业务逻辑的“空 Reconciler”,验证整个脚手架能否正常启动并与 API Server 建立连接。这是后续所有调试技巧的基础。
3.1 使用 kubebuilder init 创建项目
确保已安装 kubebuilder(版本 ≥ 3.0)和 go(版本 ≥ 1.19)。在终端执行以下命令:
# 创建项目目录并进入
mkdir -p my-first-operator && cd my-first-operator
# 初始化项目,指定 module 名称和 domain
kubebuilder init --domain example.com --repo github.com/me/my-first-operator
参数说明:
| 参数 | 作用 | 示例值 |
|---|---|---|
--domain |
API 组的域名后缀,用于生成 CRD 的 group 字段 |
example.com |
--repo |
Go module 路径,影响 import 路径和 go.mod | github.com/me/my-first-operator |
执行成功后,终端会输出类似 Your project is ready at /path/to/my-first-operator 的提示。
调试提示: 如果
kubebuilder init 因网络问题拉取依赖失败,可先设置 GOPROXY=https://goproxy.cn,direct 再重试。这是国内环境最常见的脚手架初始化失败原因。
3.2 项目目录结构详解
初始化后的目录结构如下,每个文件/目录都有明确的职责:
my-first-operator/
├── .github/ # GitHub Actions 工作流模板(CI/CD)
├── config/ # Kubernetes 资源清单(部署、RBAC、CRD)
│ ├── crd/ # 自定义资源定义(kustomize 形式)
│ ├── rbac/ # ServiceAccount、Role、RoleBinding
│ ├── manager/ # 控制器 Deployment 和 Service
│ └── samples/ # CR 示例 YAML
├── controllers/ # Reconciler 实现(核心代码)
├── internal/ # 内部辅助包(非公开 API)
├── api/ # API 类型定义(GVK + 校验/默认逻辑)
├── hack/ # 代码生成脚本(boilerplate、补全)
├── Dockerfile # 控制器镜像构建文件
├── Makefile # 常用命令入口(build、test、deploy)
├── PROJECT # kubebuilder 元数据文件(项目配置)
├── go.mod / go.sum # Go 依赖管理
└── main.go # 程序入口,初始化 Manager 和 Reconciler
关键目录职责速查表:
| 目录/文件 | 核心职责 | 调试关注点 |
|---|---|---|
main.go |
创建 Manager、注册 Scheme、启动 Reconciler | Manager 选项(MetricsBindAddress、LeaderElection) |
controllers/ |
Reconciler 结构体 + Reconcile 方法 | 日志注入、Client 构造、返回结果 |
config/ |
部署到集群所需的全部 YAML | 镜像拉取策略、资源限制、Webhook 配置 |
api/ |
CRD 的 Go 类型定义(DeepCopy、Defaulter、Validator) | 校验逻辑是否阻塞 Reconcile |
调试视角: 在调试阶段,
main.go 和 controllers/ 是修改最频繁的两个区域。理解 config/ 下的资源清单能帮助你快速在集群中部署调试版本。
3.3 理解核心入口:main.go 的骨架
main.go 是控制器的启动入口。初始化后生成的代码结构如下:
package main
import (
"flag"
"os"
"ctrlruntime"
// 其他依赖
)
func main() {
var metricsAddr string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "metrics 监听地址")
flag.Parse()
// 1. 创建 Manager
mgr, err := ctrlruntime.NewManager(ctrlruntime.GetConfigOrDie(), ctrlruntime.Options{
MetricsBindAddress: metricsAddr,
LeaderElection: false,
Scheme: scheme,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
// 2. 注册 Reconciler(目前为空)
if err = (&controllers.MyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller")
os.Exit(1)
}
// 3. 启动 Manager(阻塞)
setupLog.Info("starting manager")
if err := mgr.Start(ctrlruntime.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
三个关键阶段:
- NewManager:初始化与 API Server 的连接、缓存、选举器。
- SetupWithManager:将 Reconciler 注册到 Controller 的 Watch 链中。
- Start:启动所有组件,进入事件循环。
3.4 第一个空 Reconciler 的结构
在 controllers/ 目录下,kubebuilder 会生成一个示例 Reconciler 文件(通常名为 myreconciler.go)。其核心结构如下:
package controllers
import (
"context"
"ctrlruntime"
"ctrlruntime/log"
)
// MyReconciler 结构体
type MyReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// Reconcile 方法:控制器核心逻辑
func (r *MyReconciler) Reconcile(ctx context.Context, req ctrlruntime.Request) (ctrlruntime.Result, error) {
log := log.FromContext(ctx)
log.Info("Reconciling", "namespace", req.Namespace, "name", req.Name)
// 目前为空:只打印日志,不做任何操作
return ctrlruntime.Result{}, nil
}
// SetupWithManager 将 Reconciler 注册到 Manager
func (r *MyReconciler) SetupWithManager(mgr ctrlruntime.Manager) error {
return ctrlruntime.NewControllerManagedBy(mgr).
For(&mygroupv1.MyResource{}). // 监听的自定义资源
Complete(r)
}
常见陷阱: 空 Reconciler 的
For() 方法需要指定一个具体的资源类型。如果尚未定义 CRD,可先使用 For(&corev1.Pod{}) 监听内置 Pod 资源进行调试,避免因 CRD 未安装导致启动失败。
3.5 运行第一个空 Reconciler
在本地开发环境(无需真实集群)中,可以使用 make run 启动控制器:
# 在项目根目录执行
make run
启动后,终端会输出类似以下日志:
I0115 10:23:45.123456 1 main.go:60] "starting manager"
I0115 10:23:45.234567 1 controller.go:123] "Starting EventSource" "source"="kind source: /, Kind="
I0115 10:23:45.345678 1 controller.go:130] "Starting Controller"
I0115 10:23:45.456789 1 controller.go:137] "Starting workers" "worker count"=1
此时控制器已成功连接到 API Server(或本地 fake client),并开始监听资源事件。由于 Reconciler 为空,每次触发 Reconcile 只会打印一行日志并返回 Result{}, nil。
调试验证: 在另一个终端中创建一个 Pod(
kubectl run test-pod --image=nginx),观察控制器终端是否打印出对应的 Reconcile 日志。这是验证脚手架是否正常工作的最简方法。
3.6 空 Reconciler 的启动流程 SVG
下图展示了从 make run 到空 Reconciler 就绪的完整启动流程:
3.7 项目目录结构关系 SVG
下图展示了项目各目录之间的依赖与协作关系:
3.8 常见问题与调试技巧
- 启动后立即退出:检查
--kubeconfig是否指向有效的集群配置,或使用--kubeconfig=/dev/null强制使用本地 fake client 测试。 - 日志不输出:确认
log.FromContext(ctx)正确注入,且日志级别未设置为0(默认 info 级别应可见)。 - Reconcile 未被触发:检查
For()中指定的资源类型是否已安装 CRD,或是否使用了正确的 GroupVersionKind。 - 端口冲突:默认 metrics 端口为
:8080,如果本地已被占用,通过--metrics-bind-address=:8081修改。
调试黄金法则: 在空 Reconciler 阶段,优先验证“控制器能否启动”和“Reconcile 能否被触发”,而不是急于实现业务逻辑。这两个基础能力一旦确认,后续所有调试都会更加顺畅。