从零开发 Kubernetes Operator

什么是 Operator?

Operator 的主要目标是将工程师的逻辑转换为代码,以便实现原生 Kubernetes 无法完成的某些任务的自动化。

例如,Operator 可以在 Kubernetes 中部署和维护 MySQL、Elasticsearch 或 GitLab Runner 等工具,自动配置这些工具,根据事件调整系统状态,并对故障做出反应。

Operator 由两个组件组成:

  • CRD(Custom Resource Definition):Kubernetes 自定义类型的资源蓝图,用于描述其规范(Spec)和状态(Status)。CRD 的实例称为自定义资源(CR)
  • Controller(控制器):持续监视集群状态,并根据事件做出变更,目标是将资源的当前状态变为用户在 CR Spec 中定义的期望状态

控制器的类比:恒温器。设置温度 = 期望状态,房间实际温度 = 当前状态,恒温器通过打开或关闭空调使实际状态接近期望状态。

Manager(管理器) 负责启动所有控制器,并使控制循环共存。假设项目中有两个 CRD 和两个控制器,管理器将启动这两个控制器并使它们共存。

构建 Operator

可以使用 controller-runtime 从头构建,也可以使用框架加速开发:KubebuilderOperatorSDK(两个项目目前正在合并)。本文选择 Kubebuilder。

开发环境准备

# 必备工具
# Go v1.17.9+
# Docker 17.03+
# kubectl v1.11.3+
# K8s v1.11.3+ 集群(建议用 kind 设置本地集群)

# 安装 kubebuilder
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH) \
  && chmod +x kubebuilder \
  && mv kubebuilder /usr/local/bin/

# 验证
kubebuilder version

初始化项目

# 初始化新项目(下载 controller-runtime 二进制文件并准备项目)
kubebuilder init --domain my.domain --repo my.domain/tutorial

# 创建新的 API(CRD)和 Controller
kubebuilder create api --group tutorial --version v1 --kind Foo
# 提示创建 CRD 和 Controller 时均按 yes

生成的项目结构(Go 项目):

├── main.go          # 项目入口,设置并运行管理器
├── config/          # K8s 部署 Operator 的 manifest
├── Dockerfile       # 构建管理器镜像的容器文件
├── api/v1/          # Foo CRD 定义
└── controllers/     # Foo 控制器

自定义 CRD

Foo CRD 的定义中有 name 字段(Foo 正在寻找的朋友的名称)。如果 Foo 找到了一个朋友(一个和朋友同名的 Pod),happy 状态将被设置为 true。

// api/v1/foo_types.go
package v1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// FooSpec defines the desired state of Foo
type FooSpec struct {
    // Name of the friend Foo is looking for
    Name string `json:"name"`
}

// FooStatus defines the observed state of Foo
type FooStatus struct {
    // Happy will be set to true if Foo found a friend
    Happy bool `json:"happy,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Foo is the Schema for the foos API
type Foo struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   FooSpec   `json:"spec,omitempty"`
    Status FooStatus `json:"status,omitempty"`
}

实现 Controller

控制器逻辑:通过 Reconcile 请求获取 Foo 资源 → 获取 Foo 朋友的名称 → 列出所有同名的 Pod → 如果找到则设置 happy=true,否则设为 false

控制器也会对 Pod 事件做出反应——如果创建了一个新 Pod,Foo 资源能够相应更新其状态。

// controllers/foo_controller.go
package controllers

import (
    "context"

    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/types"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/handler"
    "sigs.k8s.io/controller-runtime/pkg/log"
    "sigs.k8s.io/controller-runtime/pkg/reconcile"
    "sigs.k8s.io/controller-runtime/pkg/source"

    tutorialv1 "my.domain/tutorial/api/v1"
)

type FooReconciler struct {
    client.Client
    Scheme *runtime.Scheme
}

// RBAC 权限标记
//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos/status,verbs=get;update;patch
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch

func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)
    log.Info("reconciling foo custom resource")

    // 1. 获取触发 reconciliation 的 Foo 资源
    var foo tutorialv1.Foo
    if err := r.Get(ctx, req.NamespacedName, &foo); err != nil {
        log.Error(err, "unable to fetch Foo")
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 2. 列出所有 Pod,查找与 Foo 朋友同名的 Pod
    var podList corev1.PodList
    var friendFound bool
    if err := r.List(ctx, &podList); err != nil {
        log.Error(err, "unable to list pods")
    } else {
        for _, item := range podList.Items {
            if item.GetName() == foo.Spec.Name {
                log.Info("pod linked to a foo custom resource found", "name", item.GetName())
                friendFound = true
            }
        }
    }

    // 3. 更新 Foo 的 happy 状态
    foo.Status.Happy = friendFound
    if err := r.Status().Update(ctx, &foo); err != nil {
        log.Error(err, "unable to update foo's happy status")
        return ctrl.Result{}, err
    }

    log.Info("foo custom resource reconciled")
    return ctrl.Result{}, nil
}

// 设置 Controller:监听 Foo 资源 + 监听 Pod 事件
func (r *FooReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&tutorialv1.Foo{}).
        Watches(
            &source.Kind{Type: &corev1.Pod{}},
            handler.EnqueueRequestsFromMapFunc(r.mapPodsReqToFooReq),
        ).
        Complete(r)
}

// Pod 事件映射:只有当 Pod 名称是某个 Foo 的"朋友"时,才触发该 Foo 的 reconciliation
func (r *FooReconciler) mapPodsReqToFooReq(obj client.Object) []reconcile.Request {
    ctx := context.Background()
    log := log.FromContext(ctx)

    req := []reconcile.Request{}
    var list tutorialv1.FooList
    if err := r.Client.List(context.TODO(), &list); err != nil {
        log.Error(err, "unable to list foo custom resources")
    } else {
        for _, item := range list.Items {
            if item.Spec.Name == obj.GetName() {
                req = append(req, reconcile.Request{
                    NamespacedName: types.NamespacedName{Name: item.Name, Namespace: item.Namespace},
                })
                log.Info("pod linked to a foo custom resource issued an event", "name", obj.GetName())
            }
        }
    }
    return req
}

运行与测试

# 1. 更新 manifest
make manifests

# 2. 安装 CRD 到集群
make install
kubectl get crds
# foos.tutorial.my.domain

# 3. 运行控制器(也可部署为集群中的 Deployment)
make run

测试流程

# 创建两个 Foo 自定义资源
apiVersion: tutorial.my.domain/v1
kind: Foo
metadata:
  name: foo-01
spec:
  name: jack    # 寻找名为 jack 的 Pod
---
apiVersion: tutorial.my.domain/v1
kind: Foo
metadata:
  name: foo-02
spec:
  name: joe     # 寻找名为 joe 的 Pod
kubectl apply -f config/samples
# 此时两个 Foo 的 happy 状态都为 false(没有同名 Pod)

# 部署一个名为 jack 的 Pod
kubectl apply -f jack-pod.yaml
# 控制器检测到 Pod 创建事件 → 触发 foo-01 的 reconciliation → happy 变为 true
# foo-02 不受影响(它的朋友是 joe)

# 修改 foo-02 的 spec.name 为 jack
# 控制器触发 reconciliation → foo-02 的 happy 也变为 true

# 删除 jack Pod → 两个 Foo 的 happy 都回到 false

进阶方向

  • 优化事件过滤(避免事件重复提交)
  • 完善 RBAC 权限
  • 改进日志记录系统
  • 当 Operator 更新资源时触发 Kubernetes 事件
  • 获取 Foo 自定义资源时添加自定义字段(如显示 happy 状态)
  • 编写单元测试和端到端测试

Operator 开发工具对比

工具 定位 语言 说明
Kubebuilder 官方推荐框架 Go 文档清晰、久经考验,与 OperatorSDK 正在合并
OperatorSDK Red Hat 主导 Go/Ansible/Helm 支持多种方式构建 Operator
controller-runtime 底层库 Go Kubebuilder/OperatorSDK 的底层依赖,可从头构建
Metacontroller 轻量级 任意语言 通过 Webhook 方式,用任意语言实现控制器逻辑

GitHub 完整代码:项目提供了完整的示例代码,可直接参考。