K8S 九月 25, 2021

【转】如何写一个operator

文章字数 14k 阅读约需 13 mins. 阅读次数

原文地址:https://www.jianshu.com/p/79476712575e

如何写一个operator

文章源地址请移步writing-a-controller-for-pod-labels

样例代码

k8s中的operator是什么?

operator旨在简化基于k8s部署有状态服务(例如:ceph集群、skywalking集群)

可以利用Operator SDK 构建一个operator
operator使扩展k8s及实现自定义调度变得更加简单。

尽管Operator SDK 适合构建功能齐全的operator
但也可以使用它来编写单个控制器。

这篇文章将指导您在Go中编写一个Kubernetes控制器,该控制器将向具有特定注释的pod添加pod-name标签

为什么我们需要一个控制器呢?

最近我们项目中有这么个需求:通过一个service将流量路由至同一ReplicaSet中指定pod内(service对应一个或多个pod

而原生k8s并不能实现该功能,因为原生service只能通过labelPod匹配,并且同一ReplicaSet内,Pod具有相同标签。

上述需求有两种解决方案:

  1. 创建service时不指定标签选择器,而是利用EndpointsEndpointSlices关联pod
    此时我们需要写一个自定义控制器,用于插入指定pod的端点地址至EndpointsEndpointSlices对象
  2. 为每个Pod添加具有唯一value的标签,接下来我们就可以利用标签选择器进行servicePod的关联。

由于k8s中的控制器实质是个控制循环程序,控制器可以对k8s的资源(Resource,比如namespace、service等)进行监听追踪。

此时如果我们创建一个控制器,仅监听Pod资源,针对指定Pod进行label处理,就可实现上述需求。

当然k8s原生资源StatefulSets也是可以实现这一功能的,但假设我们不想/不能使用StatefulSets类型去实现呢?

一般情况下,我们很少直接创建Pod类型,而是通过Deployment, ReplicaSet间接创建Pod

我们可以指定标签添加到PodSpec中的每个Pod,但不能使用动态值,因此无法复制StatefulSetpod-name标签。

我们尝试使用mutating admission webhook
实现。
当任何人创建Pod时,webhook会自动注入一个包含Pod名称的标签对Pod进行修改。

遗憾的是这种方式并不能实现我们的需求: 并不是所有的Pod在创建前都有名字。
举个例子:当ReplicaSet控制器创建一个Pod时,他向kube-apiserver发送一个请求,获取一个namePrefix而非name

kubeapi-server在将新的Pod持久化到etcd之前生成一个唯一的名称,
这个过程发生于在调用我们的许可webhook之后。所以在大多数情况下,我们无法知道一个带有mutating webhookPod的名字

一旦Pod持久化至K8s集群中时,它几乎不会发生变更,但我们仍然可以通过以下方式,添加label

kubectl label my-pod my-label-key=my-label-value

我们需要观察Kubernetes API中任何Pod的变化,并添加我们想要的标签。
我们将编写一个控制器来为我们做这件事,而不是手动做这件事

利用Operator SDK构建一个控制器

控制器是一个协调循环,它从Kubernetes API中读取期望的资源状态,并采取行动使集群的实际状态达到期望状态

安装配置

1.安装Operator SDK

  • 下载二进制
sudo curl -LO https://github.com/operator-framework/operator-sdk/releases/download/v1.12.0/operator-sdk_linux_amd64
sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk

2.构建工程

mkdir label-operator && cd label-operator

3.初始化工程

export GOPROXY=https://goproxy.cn
operator-sdk init --domain=weiliang.io --repo=github.com/weiliang-ms/label-operator

4.创建控制器

接下来我们创建一个控制器,这个控制器将会处理Pod资源,而非自定义资源,所以不需要生成资源代码。

operator-sdk create api --group=core --version=v1 --kind=Pod --controller=true --resource=false

初始化编码

controllers/pod_controller.go解析

现在我们拥有了一个新文件: controllers/pod_controller.go
该文件包含了PodReconciler类型,该类型包含两个方法:

  • Reconcile函数:
func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    _ = log.FromContext(ctx)

    // your logic here

    return ctrl.Result{}, nil
}

当创建、更新、或删除Pod时会调用Reconcile方法,Pod名称与命名空间作为函数入参,存于ctrl.Request对象之中

  • SetupWithManager函数:
func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
            For(&corev1.Pod{}).
            Complete(r)
}

operator会在启动时执行SetupWithManager函数,SetupWithManager函数用于生命监听资源类型

因为我们只想要监听Pod资源变化,所以监听资源这部分代码不动

RBAC设置

接下来为我们的控制器配置RBAC权限,代码生成器生成的默认权限如下:

//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update

显然我们并不需要以上全部权限,我们控制器从不会CRUD Podstatusfinalizers字段。

控制器需要的仅仅是对Pod的读权限与更新权限,本着最小原则,我们调整权限如下

// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch

此时我们已经编写好了控制器的基本调用逻辑。

实现Reconcile函数

我们希望Reconcile实现以下功能:

  1. 通过入参ctrl.Request中的Pod名称与命名空间字段,请求k8s api获取Pod对象
  2. 如果Pod拥有add-pod-name-label注解,给这个Pod添加一个pod-name标签
  3. 将上一步Pod的变更回写k8s

接下来我们为注解与标签定义一些常量

const (
    addPodNameLabelAnnotation = "padok.fr/add-pod-name-label"
    podNameLabel              = "padok.fr/pod-name"
)

根据入参获取Pod

首先我们根据入参信息,去k8s api获取Pod实例

func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    l := log.FromContext(ctx)

    var pod corev1.Pod
    if err := r.Get(ctx, req.NamespacedName, &pod); err != nil {
        l.Error(err, "unable to fetch Pod")
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

异常处理

当创建、更新或删除一个Pod时,会触发我们控制器的Reconcile方法

但当事件为’删除事件’时,r.Get()会返回一个指定错误对象,接下来我们通过引用下面的包来处理这个异常。

package controllers

import (
    // other imports...
    apierrors "k8s.io/apimachinery/pkg/api/errors"
    // other imports...
)
// other functions...
func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    l := log.FromContext(ctx)

    var pod corev1.Pod
    if err := r.Get(ctx, req.NamespacedName, &pod); err != nil {
        if apierrors.IsNotFound(err) {
            // we'll ignore not-found errors, since we can get them on deleted requests.
            return ctrl.Result{}, nil
        }
        l.Error(err, "unable to fetch Pod")
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}
// other functions...

编辑Pod,判断注解、标签是否存在

此时我们已经获取到了这个Pod对象(创建、更新事件),接下来我们获取Pod的注解元数据,判断是否需要添加标签

...
    /*
       Step 1: 添加或移除标签.
    */

    // 判断Pod是否存在注解 -> padok.fr/add-pod-name-label: true
    labelShouldBePresent := pod.Annotations[addPodNameLabelAnnotation] == "true"
    // 判断Pod是否存在标签 -> padok.fr/pod-name: Pod名称
    labelIsPresent := pod.Labels[podNameLabel] == pod.Name

    // 如果期望状态与实际状态一致(含有上述标签、注解),返回
    if labelShouldBePresent == labelIsPresent {
        log.Info("no update required")
        return ctrl.Result{}, nil
    }

    // 存在注解 -> padok.fr/add-pod-name-label: true
    if labelShouldBePresent {
        // 判断标签map是否为空
        if pod.Labels == nil {
            // 为空创建
            pod.Labels = make(map[string]string)
        }
        // 添加标签 -> padok.fr/pod-name: Pod名称
        pod.Labels[podNameLabel] = pod.Name
        log.Info("adding label")
    } else {
        // 不存在注解 -> padok.fr/add-pod-name-label: true
        // 移除标签
        delete(pod.Labels, podNameLabel)
        log.Info("removing label")
    }
...

回写Podk8s

    /*
        Step 2: Update the Pod in the Kubernetes API.
    */

    if err := r.Update(ctx, &pod); err != nil {
        l.Error(err, "unable to update Pod")
        return ctrl.Result{}, err
    }

当我们回写Pod变更至k8s时存在以下风险:集群内的Pod与我们获取到的Pod已经不一致(可能通过其他渠道变更了该Pod

在编写一个k8s控制器时,我们应该明白一个问题:我们编写的控制器并不是唯一能操作k8s资源对象的实例(其他控制器、kubectl等亦能操作k8s资源对象)

当发生这种情况时,最好的做法是通过重新排队事件,从头开始处理。

 if err := r.Update(ctx, &pod); err != nil {
    if apierrors.IsConflict(err) {
        // The Pod has been updated since we read it.
        // Requeue the Pod to try to reconciliate again.
        return ctrl.Result{Requeue: true}, nil
    }
    if apierrors.IsNotFound(err) {
        // The Pod has been deleted since we read it.
        // Requeue the Pod to try to reconciliate again.
        return ctrl.Result{Requeue: true}, nil
    }
    log.Error(err, "unable to update Pod")
    return ctrl.Result{}, err
}

在k8s集群内运行该控制器

本人本地开发环境为windows10 + Ubuntu 20

本地ubuntu安装Kubectl并配置kube-config

集群信息

weiliang@DESKTOP-O8QG6I5:/mnt/d/github/label-operator$ kubectl get node
NAME    STATUS   ROLES           AGE   VERSION
node1   Ready    master,worker   62d   v1.18.6
node2   Ready    master,worker   62d   v1.18.6
node3   Ready    master,worker   62d   v1.18.6
node4   Ready    worker          62d   v1.18.6

label-operator下执行

shell目录

weiliang@DESKTOP-O8QG6I5:/mnt/d/github/label-operator$ pwd
/mnt/d/github/label-operator

运行operator

export GOPROXY=https://goproxy.cn
make run

运行一个nginx服务Pod

新建一个ubuntu shell窗口执行

kubectl run --image=nginx:1.20.0 my-nginx

查看Pod信息

weiliang@DESKTOP-O8QG6I5:/mnt/d/github/label-operator$ kubectl get pod
NAME                                 READY   STATUS              RESTARTS   AGE
my-nginx                             1/1     Running             0          78s

此时运行operator的窗口会输出如下信息,说明监听成功

2021-09-24T11:52:10.588+0800    INFO    controller-runtime.manager.controller.pod       no update required      {"reconciler group": "", "reconciler kind": "Pod", "name": "m
y-nginx", "namespace": "default"}
2021-09-24T11:52:10.597+0800    INFO    controller-runtime.manager.controller.pod       no update required      {"reconciler group": "", "reconciler kind": "Pod", "name": "m
y-nginx", "namespace": "default"}
2021-09-24T11:52:10.630+0800    INFO    controller-runtime.manager.controller.pod       no update required      {"reconciler group": "", "reconciler kind": "Pod", "name": "m
y-nginx", "namespace": "default"}

查看Pod标签

weiliang@DESKTOP-O8QG6I5:/mnt/d/github/label-operator$ kubectl get pod my-nginx --show-labels
NAME       READY   STATUS    RESTARTS   AGE     LABELS
my-nginx   1/1     Running   0          4m38s   run=my-nginx

此时我们给该Pod打上以下注解,并查看是否已自动添加新的标签

weiliang@DESKTOP-O8QG6I5:/mnt/d/github/label-operator$ kubectl annotate pod my-nginx padok.fr/add-pod-name-label=true
pod/my-nginx annotated

查看标签

weiliang@DESKTOP-O8QG6I5:/mnt/d/github/label-operator$ kubectl get pod my-nginx --show-labels
NAME       READY   STATUS    RESTARTS   AGE     LABELS
my-nginx   1/1     Running   0          6m39s   padok.fr/pod-name=my-nginx,run=my-nginx

成功了! 我们成功的实现上面的需求

0%