(翻)如何动手写一个kubernetes 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

成功了! 我们成功编写一个简单的operator,实现上面的需求

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容