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)
    }
}

三个关键阶段:

  1. NewManager:初始化与 API Server 的连接、缓存、选举器。
  2. SetupWithManager:将 Reconciler 注册到 Controller 的 Watch 链中。
  3. 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 就绪的完整启动流程:

空 Reconciler 启动流程 空 Reconciler 启动流程 make run main.go 解析参数 NewManager SetupWithManager Start 空 Reconcile 循环

3.7 项目目录结构关系 SVG

下图展示了项目各目录之间的依赖与协作关系:

项目目录结构关系 项目目录结构关系 main.go controllers/ api/ config/ internal/ hack/ Dockerfile main.go 依赖 controllers/ 和 api/;config/ 和 Dockerfile 用于部署;hack/ 和 internal/ 提供辅助能力

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 能否被触发”,而不是急于实现业务逻辑。这两个基础能力一旦确认,后续所有调试都会更加顺畅。