Appearance
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。下面的 apiVersion 里 ops.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=True | ConfigMap 已经和 spec.targets 对齐 |
CronJobSynced=True | CronJob 已经和 spec.schedule、spec.image 对齐 |
Ready=False | Reconcile 失败,message 写明原因 |
排查 Operator 时,先看自定义资源的 status,再看 Controller 日志。比如 OpsCheck 的 Ready=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.go | OpsCheckSpec、OpsCheckStatus、OpsCheck 类型定义 |
internal/controller/opscheck_controller.go | Reconcile 逻辑 |
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-checker 的 Target 接近,但 JSON 字段使用 Kubernetes API 里常见的 camelCase,例如 timeoutSeconds、labelSelector。生成 ConfigMap 时,再转换成 ops-checker 读取的 targets.json。
继续修改文件:api/v1alpha1/opscheck_types.go
定义 spec 和 status:
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"`
}schedule 和 image 没有 omitempty,因为 CronJob 没有这两个字段就无法工作。concurrency 可以省略,Controller 生成命令行参数时再给默认值。
生成 CRD:
bash
make manifestsmake manifests 会根据 Go 结构体和 kubebuilder 注解更新 config/crd/、config/rbac/ 等清单。
六、controller-runtime
写 Operator 常用 controller-runtime。它在 client-go 之上封装了控制器常用能力。
| 组成 | 作用 |
|---|---|
| API Type | Go 结构体,定义 OpsCheckSpec 和 OpsCheckStatus |
| 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 使用的是 timeoutSeconds、labelSelector,ops-checker 读取的是 timeout_seconds、label_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: trueOwnerReference 表示这些子资源属于 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 OpsCheckmake 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 opsOperator 在集群内运行时使用自己的 ServiceAccount,不再读取本机 kubeconfig。权限不足时,Controller 日志里通常有 forbidden,用户侧现象是 OpsCheck 创建成功,但 ConfigMap、CronJob 没出现,或者 status 不更新。
十三、常见错误
| 现象 | 用户侧表现 | 常见原因 | 排查入口 |
|---|---|---|---|
no matches for kind | apply 示例 OpsCheck 时报错 | CRD 没安装或 apiVersion 写错 | kubectl get crd、kubectl api-resources |
OpsCheck 有了但没有 CronJob | kubectl get opscheck 能看到对象,子资源没有出现 | Controller 没运行、RBAC 不足、Reconcile 失败 | Controller 日志、Events、kubectl auth can-i |
| ConfigMap 内容没更新 | 修改 targets 后 CronJob 仍使用旧配置 | Reconcile 没对比 spec.targets 或更新失败 | ConfigMap YAML、Controller 日志 |
| status 不更新 | status 为空或 observedGeneration 落后 | status 子资源未启用、权限缺少 /status | CRD、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。