4、第一个CRD定义:创建API类型、定义Spec与Status字段、生成CRD YAML

在Operator开发中,CRD(Custom Resource Definition)是自定义资源的蓝图。本章将引导你使用Operator SDK创建第一个CRD,从初始化API类型到定义Spec/Status字段,最终生成可部署的CRD YAML清单。这是整个Operator开发的基础步骤,决定了你的自定义资源能描述什么样的业务对象。

4.1 使用Operator SDK创建API类型

Operator SDK提供了create api命令,用于快速生成CRD的Go类型骨架和对应的控制器框架。执行以下命令前,请确保已在项目根目录下初始化了Operator项目(operator-sdk init)。

# 创建API类型,指定组(group)、版本(version)和资源种类(kind)
operator-sdk create api \
  --group=example \
  --version=v1 \
  --kind=AppService \
  --resource=true \
  --controller=true

参数说明:

参数 含义 示例值
--group API组,用于资源分组 example
--version API版本,支持v1、v1beta1等 v1
--kind 资源种类名称,首字母大写 AppService
--resource 是否生成CRD资源类型 true
--controller 是否生成控制器 true

执行成功后,项目结构会新增以下关键文件:

  • api/v1/appservice_types.go:自定义资源的Go结构体定义,包含Spec和Status。
  • api/v1/zz_generated.deepcopy.go:自动生成的深拷贝函数,用于资源对象的深拷贝。
  • controllers/appservice_controller.go:控制器逻辑骨架。
  • config/crd/bases/:后续生成的CRD YAML存放目录。
提示:如果只生成资源类型而不生成控制器,可将--controller=false。对于纯CRD定义场景,可以先只生成资源,后续再补充控制器。

4.2 定义Spec与Status字段

CRD的核心在于Spec(期望状态)和Status(实际状态)的设计。打开api/v1/appservice_types.go,你会看到类似如下的骨架:

// AppServiceSpec defines the desired state of AppService
type AppServiceSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // 副本数
    Replicas int32 `json:"replicas,omitempty"`
    // 容器镜像
    Image string `json:"image,omitempty"`
    // 服务端口
    Port int32 `json:"port,omitempty"`
}

// AppServiceStatus defines the observed state of AppService
type AppServiceStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // 当前就绪副本数
    ReadyReplicas int32 `json:"readyReplicas,omitempty"`
    // 资源状态信息
    Conditions []metav1.Condition `json:"conditions,omitempty"`
}

字段设计原则:

  • Spec字段:用户可配置的期望值,如副本数、镜像、端口、环境变量等。使用json标签控制序列化名称,omitempty表示空值时忽略。
  • Status字段:由控制器更新的观测值,如当前就绪副本数、资源状态、错误信息等。通常包含Conditions数组用于表达资源生命周期阶段。
  • 类型选择:基础类型(string、int32、bool)、Kubernetes标准类型(corev1.ResourceRequirements)、自定义结构体。
注意:修改_types.go文件后,必须运行make generate重新生成深拷贝函数和CRD YAML。否则,控制器在运行时可能因缺少深拷贝方法而崩溃。

4.3 生成CRD YAML

定义完字段后,使用Operator SDK的代码生成工具生成CRD YAML清单。执行以下命令:

# 生成CRD YAML(基于Go类型定义)
make generate
make manifests

执行流程:

  1. make generate:调用controller-gen object:headerFile="hack/boilerplate.go.txt",更新zz_generated.deepcopy.go。
  2. make manifests:调用controller-gen crd,根据api/v1/目录下的类型定义生成CRD YAML,输出到config/crd/bases/目录。

生成的CRD YAML文件路径示例:

config/crd/bases/example.example.com_appservices.yaml

该YAML文件包含了完整的CRD定义,包括:

  • apiVersion:apiextensions.k8s.io/v1
  • kind:CustomResourceDefinition
  • spec.group:example.example.com
  • spec.names.kind:AppService
  • spec.versions:包含schema.openAPIV3Schema,即根据Go结构体自动生成的OpenAPI校验规则。
关键检查点:打开生成的CRD YAML,确认spec.versions[0].schema.openAPIV3Schema.properties.spec下包含了你在Go类型中定义的所有字段。如果缺少字段,说明make generate未正确执行或类型定义有语法错误。

4.4 CRD定义与生成流程总览

下图展示了从创建API类型到生成CRD YAML的完整流程,以及各步骤产出的关键文件。

CRD定义与生成流程 1. create api 生成Go类型骨架 2. 定义Spec/Status 编辑 _types.go 文件 3. make generate 生成深拷贝函数 4. make manifests 生成CRD YAML 关键产出文件 • api/v1/appservice_types.go —— Spec/Status定义 • api/v1/zz_generated.deepcopy.go —— 深拷贝函数 • config/crd/bases/example.example.com_appservices.yaml —— 可部署的CRD YAML清单 部署CRD到集群 kubectl apply -f config/crd/bases/example.example.com_appservices.yaml

4.5 验证CRD定义

生成CRD YAML后,建议进行以下验证:

  1. 语法校验:kubectl apply --dry-run=client -f config/crd/bases/example.example.com_appservices.yaml,确认无语法错误。
  2. 字段完整性:检查YAML中properties.spec和properties.status是否包含所有定义字段。
  3. 校验规则:确认openAPIV3Schema中字段类型、必填项、枚举值等约束是否正确。
最佳实践:在开发阶段,建议将CRD YAML纳入版本控制(Git),并配合CI流水线在每次修改类型后自动重新生成,确保CRD定义与代码一致。

4.6 常见问题与排查

问题现象 可能原因 解决方案
make generate报错:deepcopy未生成 类型定义中使用了未注册的第三方类型 确保所有字段类型都实现了runtime.Object接口,或使用// +kubebuilder:object:generate=true标记
CRD YAML中缺少字段 字段未添加json标签,或类型不可导出(小写开头) 检查字段首字母大写,并添加json:"fieldName"标签
部署CRD时报schema validation error OpenAPI校验规则与字段类型不匹配 检查openAPIV3Schema中字段类型,确保与Go类型一致
控制器无法识别自定义资源 CRD未部署或GVK(GroupVersionKind)不匹配 确认CRD已部署,且控制器中的For()或Watches()使用了正确的GVK