Skip to content

Operator入门

ops-checker 到现在还是一个命令行程序:读取配置、执行检查、输出结果、退出。它可以放到服务器上用 cron 跑,也可以打成镜像放进 Kubernetes CronJob 里跑。

需求继续往 Kubernetes 里靠时,会出现新的问题:检查规则希望用 YAML 声明,执行周期希望由集群管理,检查任务希望有 ownerReference,状态希望写回 Kubernetes 对象。这个时候,工具本身不用推倒重写,只是在外面加一层 Controller,把 ops-checker 变成集群里的声明式对象。

Operator 做的就是这件事:定义一个自定义资源,监听它的变化,再把它翻译成真实资源和状态。

一、运行形态

ops-checker 的几种运行方式:

阶段形态适合场景
命令行手动执行二进制临时巡检、调试配置
cron / systemd timer定时执行二进制主机侧定时巡检
Kubernetes CronJob定时运行 ops-checker 容器集群内定时巡检
Operator用自定义资源声明检查任务检查任务也需要被 Kubernetes 管理

前三种方式里,配置文件通常在本机或 ConfigMap 里,运行周期由 cron、timer 或 CronJob 管。Operator 这层增加的是“声明式入口”:创建一个 OpsCheck 对象,Controller 自动创建 ConfigMap、CronJob,并把处理结果写回 status。

二、OpsCheck

OpsCheck 是给 ops-checker 准备的自定义资源。它不是 Kubernetes 内置对象,安装 CRD 后 API Server 才认识这个 kind。下面的 apiVersionops.example.com 是 API group,这个域名在 Kubebuilder 初始化时指定(见第四节),CRD 会根据它生成。

示例对象放在 Operator 项目的 sample 目录里。

新增文件:config/samples/ops_v1alpha1_opscheck.yaml

yaml
apiVersion: ops.example.com/v1alpha1
kind: OpsCheck
metadata:
  name: platform-health
  namespace: ops
spec:
  schedule: "*/5 * * * *"
  image: "registry.example.com/ops/ops-checker:v0.1.0"
  concurrency: 5
  targets:
    - type: http
      name: api
      url: https://example.com/health
      timeoutSeconds: 3
    - type: kubernetes_pods
      name: nginx-pods
      namespace: default
      labelSelector: app=nginx
      minReady: 2

这个 YAML 表达的是“每 5 分钟运行一次 ops-checker,用这些目标做检查”。使用者只写 OpsCheck,不直接维护 CronJob 和 ConfigMap。

Controller 看到这个对象后,生成几类资源:

CRD 负责让 API Server 保存 OpsCheck,Controller 负责把 OpsCheck.spec 翻译成 ConfigMap 和 CronJob。

三、状态字段

spec 是期望状态,通常由人、GitOps 或平台写入。status 是当前状态,通常由 Controller 写入。

一个处理后的对象大致会变成:

yaml
apiVersion: ops.example.com/v1alpha1
kind: OpsCheck
metadata:
  name: platform-health
  namespace: ops
spec:
  schedule: "*/5 * * * *"
  image: "registry.example.com/ops/ops-checker:v0.1.0"
  concurrency: 5
status:
  observedGeneration: 3
  ready: true
  cronJobName: platform-health
  message: "cronjob synced"

observedGeneration 用来判断 status 是否对应最新 spec。使用者修改 spec 后,metadata.generation 会增加;Controller 处理完成后,把 status.observedGeneration 更新成同一个值。两者不一致时,status 可能还是旧状态。

成熟一点的 status 会用 conditions:

condition含义
Ready=True关联资源已经创建或更新成功
ConfigSynced=TrueConfigMap 已经和 spec.targets 对齐
CronJobSynced=TrueCronJob 已经和 spec.schedule、spec.image 对齐
Ready=FalseReconcile 失败,message 写明原因

排查 Operator 时,先看自定义资源的 status,再看 Controller 日志。比如 OpsCheckReady=False,message 写着 failed to create CronJob: forbidden,基本能判断是 RBAC 权限问题;如果 status 长时间不更新,再去看 Controller 是否在跑、日志里有没有收到 reconcile 请求。status 是 Controller 留给外部系统的第一层反馈。

四、项目骨架

Kubebuilder 常用于生成 controller-runtime 项目骨架——它是 Kubernetes SIG 维护的脚手架工具,不是 Go 自带的,需要单独安装。ops-checker 命令行工具保留在原项目里,Operator 单独建一个项目,名字可以叫 ops-checker-operator

安装 kubebuilder(macOS/Linux):

bash
# 下载最新 release:https://github.com/kubernetes-sigs/kubebuilder/releases
curl -L -o kubebuilder https://github.com/kubernetes-sigs/kubebuilder/releases/download/v4.5.0/kubebuilder_linux_amd64
chmod +x kubebuilder
sudo mv kubebuilder /usr/local/bin/
kubebuilder version

初始化:

bash
kubebuilder init \
  --domain example.com \
  --repo example.com/ops-checker-operator

kubebuilder create api \
  --group ops \
  --version v1alpha1 \
  --kind OpsCheck

生成后的目录大致是:

text
ops-checker-operator/
├── api/
│   └── v1alpha1/
│       ├── groupversion_info.go
│       └── opscheck_types.go
├── internal/
│   └── controller/
│       └── opscheck_controller.go
├── config/
│   ├── crd/
│   ├── rbac/
│   ├── manager/
│   └── samples/
├── cmd/
│   └── main.go
├── go.mod
└── Makefile

文件落点:

文件内容
api/v1alpha1/opscheck_types.goOpsCheckSpecOpsCheckStatusOpsCheck 类型定义
internal/controller/opscheck_controller.goReconcile 逻辑
config/crd/根据 API Type 生成的 CRD
config/rbac/Controller 需要的权限
config/samples/示例 OpsCheck 对象

config/samples/ 里的 YAML 是示例检查任务,不是 CRD 本身。CRD 安装后,API Server 才能接收这个示例对象。

五、API Type

API Type 是代码里的资源定义。Kubebuilder 会根据它生成 CRD YAML。

修改文件:api/v1alpha1/opscheck_types.go

先定义 target:

go
type OpsCheckTarget struct {
	Type           string `json:"type"`
	Name           string `json:"name"`
	URL            string `json:"url,omitempty"`
	TimeoutSeconds int    `json:"timeoutSeconds,omitempty"`

	Namespace     string `json:"namespace,omitempty"`
	LabelSelector string `json:"labelSelector,omitempty"`
	MinReady      int    `json:"minReady,omitempty"`
}

字段跟 ops-checkerTarget 接近,但 JSON 字段使用 Kubernetes API 里常见的 camelCase,例如 timeoutSecondslabelSelector。生成 ConfigMap 时,再转换成 ops-checker 读取的 targets.json

继续修改文件:api/v1alpha1/opscheck_types.go

定义 specstatus

go
type OpsCheckSpec struct {
	Schedule    string           `json:"schedule"`
	Image       string           `json:"image"`
	Concurrency int              `json:"concurrency,omitempty"`
	Targets     []OpsCheckTarget `json:"targets"`
}

type OpsCheckStatus struct {
	ObservedGeneration int64  `json:"observedGeneration,omitempty"`
	Ready              bool   `json:"ready,omitempty"`
	CronJobName        string `json:"cronJobName,omitempty"`
	Message            string `json:"message,omitempty"`
}

scheduleimage 没有 omitempty,因为 CronJob 没有这两个字段就无法工作。concurrency 可以省略,Controller 生成命令行参数时再给默认值。

生成 CRD:

bash
make manifests

make manifests 会根据 Go 结构体和 kubebuilder 注解更新 config/crd/config/rbac/ 等清单。

六、controller-runtime

写 Operator 常用 controller-runtime。它在 client-go 之上封装了控制器常用能力。

组成作用
API TypeGo 结构体,定义 OpsCheckSpecOpsCheckStatus
CRD根据 API Type 生成,安装到集群后提供 OpsCheck 类型
Manager管理 RESTConfig、缓存、Client、Controller、指标和 leader election
Client读写 Kubernetes 对象,包括 OpsCheck、ConfigMap、CronJob
Reconciler对比期望状态和实际状态,创建或更新资源
Scheme注册 Go 类型和 Kubernetes GVK 的对应关系
RBAC授权 Controller 读写相关资源

API Type 和 CRD 是同一件事的两种形态。API Type 是代码里的结构体,CRD 是安装到集群里的 YAML。Kubebuilder 会根据 API Type 生成 CRD。

Scheme 解决的是“Go 结构体和 Kubernetes 资源类型怎么对应”的问题。没有把 OpsCheck{} 注册进 Scheme,controller-runtime client 就不知道它对应哪个 API 路径。

七、Reconcile

Reconcile 的输入通常只有一个 namespace/name。Controller 拿到 key 后重新读取当前对象,再处理。

OpsCheck 的 Reconcile 可以按这个顺序:

步骤操作
读取 OpsCheck对象不存在说明已经删除,直接结束
生成目标配置spec.targets 转成 targets.json
对齐 ConfigMap不存在就创建,内容变化就更新
对齐 CronJob不存在就创建,schedule、image、参数变化就更新
设置 OwnerReference让 ConfigMap、CronJob 归属于 OpsCheck
更新 status写入 ready、cronJobName、message、observedGeneration

修改文件:internal/controller/opscheck_controller.go

Reconcile 的外层骨架:

go
func (r *OpsCheckReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	var check opsv1alpha1.OpsCheck
	if err := r.Get(ctx, req.NamespacedName, &check); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	if err := r.syncConfigMap(ctx, &check); err != nil {
		return ctrl.Result{}, err
	}

	if err := r.syncCronJob(ctx, &check); err != nil {
		return ctrl.Result{}, err
	}

	check.Status.Ready = true
	check.Status.CronJobName = check.Name
	check.Status.ObservedGeneration = check.Generation
	check.Status.Message = "cronjob synced"
	if err := r.Status().Update(ctx, &check); err != nil {
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

Reconcile 要保持幂等。CronJob 已经存在且字段正确时,再跑一次不应该反复更新;字段不一致时才更新。控制器重启、watch 重连、事件重复投递都会让同一个对象被处理多次。

返回错误时,controller-runtime 会把这个 key 重新放回队列,后续再试。返回 RequeueAfter 时,即使没有新事件,也会在指定时间后再次处理,适合定期检查外部状态。

八、配置生成

OpsCheck.spec.targets 使用 Kubernetes API 的字段名,ops-checker 读取的是 targets.json。Controller 中间要做一次转换,再把结果写入 ConfigMap。

修改文件:internal/controller/opscheck_controller.go

新增函数:buildTargetsJSON

go
type checkerTarget struct {
	Type           string `json:"type"`
	Name           string `json:"name"`
	URL            string `json:"url,omitempty"`
	TimeoutSeconds int    `json:"timeout_seconds,omitempty"`

	Namespace     string `json:"namespace,omitempty"`
	LabelSelector string `json:"label_selector,omitempty"`
	MinReady      int    `json:"min_ready,omitempty"`
}

func buildTargetsJSON(check *opsv1alpha1.OpsCheck) (string, error) {
	targets := make([]checkerTarget, 0, len(check.Spec.Targets))
	for _, target := range check.Spec.Targets {
		targets = append(targets, checkerTarget{
			Type:           target.Type,
			Name:           target.Name,
			URL:            target.URL,
			TimeoutSeconds: target.TimeoutSeconds,
			Namespace:      target.Namespace,
			LabelSelector:  target.LabelSelector,
			MinReady:       target.MinReady,
		})
	}

	data, err := json.MarshalIndent(targets, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal targets: %w", err)
	}

	return string(data), nil
}

API Type 使用的是 timeoutSecondslabelSelectorops-checker 读取的是 timeout_secondslabel_selector。中间的 checkerTarget 专门处理这层字段名转换。少了这一步,CronJob 里的程序能启动,但会读到空的超时时间或标签字段。

对齐 ConfigMap:

go
func (r *OpsCheckReconciler) syncConfigMap(ctx context.Context, check *opsv1alpha1.OpsCheck) error {
	targetsJSON, err := buildTargetsJSON(check)
	if err != nil {
		return err
	}

	desired := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      check.Name,
			Namespace: check.Namespace,
		},
		Data: map[string]string{
			"targets.json": targetsJSON,
		},
	}

	if err := ctrl.SetControllerReference(check, desired, r.Scheme); err != nil {
		return err
	}

	var existing corev1.ConfigMap
	err = r.Get(ctx, client.ObjectKeyFromObject(desired), &existing)
	if apierrors.IsNotFound(err) {
		return r.Create(ctx, desired)
	}
	if err != nil {
		return err
	}

	existing.Data = desired.Data
	return r.Update(ctx, &existing)
}

SetControllerReference 会写入 OwnerReference。这样删除 OpsCheck 后,Kubernetes 垃圾回收可以清理它创建的 ConfigMap。

九、CronJob

CronJob 负责按周期运行 ops-checker 镜像。Controller 生成 CronJob 时,要把 ConfigMap 挂载成配置文件,再通过参数传给二进制。

修改文件:internal/controller/opscheck_controller.go

生成命令参数时保留配置路径和并发数:

go
args := []string{
	"-config", "/etc/ops-checker/targets.json",
	"-concurrency", strconv.Itoa(defaultConcurrency(check.Spec.Concurrency)),
	"-output", "json",
}

配置路径 /etc/ops-checker/targets.json 要和 volume mount 对齐。路径不一致时,Pod 会启动成功,但 ops-checker 会因为找不到配置文件退出。

CronJob 里的关键片段:

go
container := corev1.Container{
	Name:  "ops-checker",
	Image: check.Spec.Image,
	Args:  args,
	VolumeMounts: []corev1.VolumeMount{
		{
			Name:      "config",
			MountPath: "/etc/ops-checker",
			ReadOnly:  true,
		},
	},
}

镜像名来自 OpsCheck.spec.image。如果这个镜像不存在、架构不匹配或拉取凭据缺失,Controller 侧通常不会报错,失败会出现在 Job/Pod 的 Events 和日志里。

十、资源归属

Controller 创建 ConfigMap 和 CronJob 时,会给它们设置 OwnerReference:

yaml
metadata:
  ownerReferences:
    - apiVersion: ops.example.com/v1alpha1
      kind: OpsCheck
      name: platform-health
      controller: true

OwnerReference 表示这些子资源属于 OpsCheck。删除 OpsCheck 后,Kubernetes 垃圾回收可以自动清理它创建的 ConfigMap 和 CronJob。

finalizer 用于删除前清理 Kubernetes 之外的资源。OpsCheck 这个例子只创建集群内资源,OwnerReference 通常够用。如果还创建外部告警规则、DNS 记录、云资源,就要考虑 finalizer。

十一、本地运行链路

本地开发时,CRD 安装进集群后,Controller 才能 watch OpsCheck 对象。

当前状态:集群还不认识 OpsCheck

安装 CRD:

bash
make install
kubectl get crd | grep opschecks
kubectl api-resources | grep OpsCheck

make install 通常只安装 CRD,不会启动 Controller。执行后,集群能保存 OpsCheck 对象,但不会自动创建 ConfigMap 和 CronJob。

启动 Controller:

bash
make run

另一个终端创建示例资源:

bash
kubectl apply -f config/samples/ops_v1alpha1_opscheck.yaml
kubectl get opscheck -n ops
kubectl get configmap,cronjob -n ops
kubectl describe opscheck platform-health -n ops

创建示例资源后,API Server 保存 OpsCheck,Controller 通过缓存和队列收到变化,再进入 Reconcile。Reconcile 创建 ConfigMap 和 CronJob 后,可以通过 kubectl get configmap,cronjob -n ops 验证。

本地 make run 使用当前 kubeconfig。运行前确认 context:

bash
kubectl config current-context

多集群环境里,context 错了会把 CRD 和示例资源装到另一个集群。

十二、集群部署

这个阶段有两个镜像:

镜像作用
ops-checker执行巡检的命令行工具,由 CronJob 运行
ops-checker-operator监听 OpsCheck,创建 ConfigMap 和 CronJob

先构建 ops-checker

bash
GOOS=linux GOARCH=amd64 go build -o bin/ops-checker ./cmd/ops-checker

再把它做成镜像并推送到仓库,示例地址写进 OpsCheck.spec.image

Operator 镜像:

bash
make docker-build docker-push IMG=registry.example.com/ops/ops-checker-operator:v0.1.0
make deploy IMG=registry.example.com/ops/ops-checker-operator:v0.1.0

部署后检查:

bash
kubectl get deploy,pod -n ops-checker-operator-system
kubectl logs deploy/ops-checker-operator-controller-manager -n ops-checker-operator-system
kubectl auth can-i create cronjobs.batch \
  --as=system:serviceaccount:ops-checker-operator-system:ops-checker-operator-controller-manager \
  -n ops

Operator 在集群内运行时使用自己的 ServiceAccount,不再读取本机 kubeconfig。权限不足时,Controller 日志里通常有 forbidden,用户侧现象是 OpsCheck 创建成功,但 ConfigMap、CronJob 没出现,或者 status 不更新。

十三、常见错误

现象用户侧表现常见原因排查入口
no matches for kindapply 示例 OpsCheck 时报错CRD 没安装或 apiVersion 写错kubectl get crdkubectl api-resources
OpsCheck 有了但没有 CronJobkubectl get opscheck 能看到对象,子资源没有出现Controller 没运行、RBAC 不足、Reconcile 失败Controller 日志、Events、kubectl auth can-i
ConfigMap 内容没更新修改 targets 后 CronJob 仍使用旧配置Reconcile 没对比 spec.targets 或更新失败ConfigMap YAML、Controller 日志
status 不更新status 为空或 observedGeneration 落后status 子资源未启用、权限缺少 /statusCRD、RBAC、Controller 日志
删除后子资源还在OpsCheck 删除了,ConfigMap/CronJob 没清理OwnerReference 没设置或不正确子资源 YAML、ownerReferences
CronJob 运行失败Job/Pod 创建了,但巡检失败ops-checker 镜像、参数、配置、RBAC 问题Job 日志、Pod Events

Operator 的排查入口通常放在 OpsCheck status、Controller 日志、Events、RBAC 和子资源 OwnerReference 上。业务 Pod 日志只能看到资源创建出来之后的运行状态。Controller 负责说明“为什么创建这些资源、为什么没创建、为什么状态没更新”。

十四、组件关系

ops-checker 的检查逻辑仍然在命令行工具里,Operator 只是把它变成 Kubernetes 原生的声明式任务:

检查逻辑继续由 ops-checker 维护,集群内编排由 Operator 维护。加 HTTP、Kubernetes、证书、TCP 检查时,主要扩展 ops-checker;加声明式配置、状态回写和资源生命周期时,主要扩展 Operator。