一、介绍

Kubernetes operator是一种封装、部署、治理kubernetes利用的办法。它是Kubernetes的扩大软件,利用自定义资源治理利用及组件。
operator所有的操作都是调用Kubernetes Apiserver的接口,所以实质上它也是Apiserver的客户端软件。

本文是对于Kubernetes operator开发入门的教程,旨在率领有趣味理解Operator开发的老手一窥Operator开发的根本流程。

二、 筹备工作

  • 首先你须要有一个可用的kubernetes测试集群,如果你对kubernetes相干概念,集群建设还没有充沛的理解,我倡议你先理解这方面的常识
  • 本教程应用Go语言,须要你对Go语言的语法有简略的理解,Go语言也是kubernetes的开发语言。如果你应用其余语言也是没有问题的,进入到底层都是HTTP申请。官网的或社区的SDK也提供了多种语言可供选择,当你理解了其中原理,再应用其余语言进行开发该当是能得心应手
  • 咱们将应用官网提供的k8s.io/client-go库来做测试, 它根本封装了对Kurbernetes的大部分操作。

示例代码目录如下:

├── Dockerfile├── go.mod├── go.sum├── k8s           //客户端封装│   └── client.go├── LICENSE├── main.go├── Makefile├── utils       //助手组件│   ├── errs│   │   └── errs.go│   └── logs│       └── slog.go└── yaml    ├── Deployment.yaml    //operator 部署文件    └── ServiceAccount.yaml //权限绑定文件, 下文把权限配置,绑定定义离开了,这里放在一起也是能够的

作为演示,本教程咱们次要关注以下几个方面的操作:

  • 列出所有Node/namespace
  • 列出指定命名空间的Deployment/Services
  • 创立一个Deployment/Service
  • 删除Deployment/Service

Operator的开发跟你平时开发的程序并无二致,它最重要的关注点是权限问题。Kubernetes有十分严格粗疏的权限设计,具体到每个资源每个操作。
所以咱们的Operator软件并无严格要求必须运行在Kubernetes集群的容器里,只有权限配置切当,你能够间接运行go build进去的二进制包,甚至你能够间接在你的开发环境里go run都是能够的。通常咱们为了开发调试不便,都会间接采纳这种形式运行。

如果你对Kubernetes的权限治理并不相熟,我倡议你把你的代码放在你的测试集群的Master节点里运行,Master节点领有集群的最高权限,省去了你配置权限的麻烦,把次要精力集中在业务逻辑下面。

三、开始

0x01、初始化客户端对象

首先咱们须要在代码中实例化一个k8s.io/client-go/kubernetes.Clientset类型的对象变量,它就是咱们整个Operator利用操作的客户端对象。

它能够由

  • func NewForConfig(c *rest.Config) (*Clientset, error)
  • func NewForConfigOrDie(c *rest.Config) *Clientset

两个函数实例化。
两个函数的区别:一个是实例化失败返回谬误,另一个间接抛出异样。通常倡议应用前者,由程序处理谬误,而不是间接抛出异样。

两个办法都须要一个rest.Config对象作为参数, rest.Config最重要的配置我的项目就是权限配置。

SDK给咱们提供了func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) 办法来实例化rest.Config对象。

  • masterUrl参数就是主节点的Server URL
  • kubeconfigPath参数就是权限配置文件门路。

Master节点的权限配置文件通常是文件:/etc/kubernetes/admin.conf

kubernetes在部署master节点后通过会倡议你把/etc/kubernetes/admin.conf文件拷贝到$HOME/.kube/config,所以你看到这两个中央的文件内容是一样的。

咱们在传参的时候通常倡议应用$HOME/.kube/config文件,免得因为文件权限问题出现异常,减少问题的复杂性。

BuildConfigFromFlags办法两个参数其实都是能够传空值的,如果咱们的Operator程序在Kubernetes集群容器里运行,传空值(通过也是这么干的)进来它会应用容器里的默认权限配置。然而在非kubernetes集群容器里,它没有这个默认配置的,所以在非kubernetes集群容器咱们须要显式把权限配置文件的门路传入。

说了一堆,咱们间接上代码吧:

import "k8s.io/client-go/kubernetes"//调用之前请确认文件存在,如果不存在应用/etc/kubernetes/admin.confcfg, err := clientcmd.BuildConfigFromFlags("", "/root/.kube/config") if err != nil {    log.Fatalln(err)}k8sClient, err := kubernetes.NewForConfig(cfg)if err != nil {    log.Fatalln(err)}

k8sClient就是咱们频繁应用的客户端对象。

文章开端附带了本次教程的代码repo,最终的代码通过调整与润色,保障最终的代码是可用的。

上面咱们来开始展现“真正的技术”。

0x02、列出所有nodes/namespace

//ListNodes 获取所有节点func ListNodes(g *gin.Context) {    nodes, err := k8sClient.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})    if err != nil {        g.Error(err)        return    }    g.JSON(0, nodes)}//ListNamespace 获取所有命令空间func ListNamespace(g *gin.Context) {    ns, err := k8sClient.CoreV1().Namespaces().List(context.Background(),metav1.ListOptions{})    if err != nil {        g.Error(err)        return    }    g.JSON(0, ns)}

为简略,咱们把接口返回的数据不作工作解决间接打印进去。

返回内容太多,我就不把内容贴出来了。从返回内容咱们能够看到节点信息蕴含了

  • 零碎信息
  • 节点状态
  • 节点事件
  • 资源使用量
  • 节点标签,注解,创立工夫等
  • 节点本地的镜像,容器组

不一一例举了,有趣味的读者在本人的环境运行起来看看输入后果。

上面是namespace打印进去的后果,截取了一个命名空间的数据。

{    "metadata": {        "resourceVersion": "190326"    },    "items": [        {            "metadata": {                "name": "default",                "uid": "acf4b9e4-b1ae-4b7a-bbdc-b65f088e14ec",                "resourceVersion": "208",                "creationTimestamp": "2021-09-24T11:17:29Z",                "labels": {                    "kubernetes.io/metadata.name": "default"                },                "managedFields": [                    {                        "manager": "kube-apiserver",                        "operation": "Update",                        "apiVersion": "v1",                        "time": "2021-09-24T11:17:29Z",                        "fieldsType": "FieldsV1",                        "fieldsV1": {                            "f:metadata": {                                "f:labels": {                                    ".": {},                                    "f:kubernetes.io/metadata.name": {}                                }                            }                        }                    }                ]            },            "spec": {                "finalizers": [                    "kubernetes"                ]            },            "status": {                "phase": "Active"            }        },        ... ...    ]}

0x03、列出指定命名空间的Deployment/Services

//列出指定命名空间的deploymentfunc ListDeployment(g *gin.Context) {    ns := g.Query("ns")        dps, err := k8sClient.AppsV1().Deployments(ns).List(context.Background(), metav1.ListOptions{})    if err != nil {        g.Error(err)        return    }    g.JSON(200, dps)}//列出指定命名空间的Servicesfunc ListService(g *gin.Context) {    ns := g.Query("ns")    svc, err := k8sClient.CoreV1().Services(ns).List(context.Background(), metav1.ListOptions{})    if err != nil {        g.Error(err)        return    }    g.JSON(200, svc)}

通过参数指定命名空间。
咱们来看看返回后果:

# deployment{    ... ...    "items": [        {            "metadata": {                "name": "nginx",                "namespace": "testing",                "labels": {                    "k8s.kuboard.cn/layer": "web",                    "k8s.kuboard.cn/name": "nginx"                },                ... ...            },            "spec": {                "replicas": 2,                "selector": {                    "matchLabels": {                        "k8s.kuboard.cn/layer": "web",                        "k8s.kuboard.cn/name": "nginx"                    }                },                "template": {                    "metadata": {                        "labels": {                            "k8s.kuboard.cn/layer": "web",                            "k8s.kuboard.cn/name": "nginx"                        }                    },                    "spec": {                        "containers": [                            {                                "name": "nginx",                                "image": "nginx:alpine",                                ... ...                            }                        ],                    }                },                "strategy": {                    "type": "RollingUpdate",                    "rollingUpdate": {                        "maxUnavailable": "25%",                        "maxSurge": "25%"                    }                },            },            "status": ...        }        ... ...    ]}# Services{    "items": [        {            "metadata": {                "name": "nginx",                "namespace": "testing",                "labels": {                    "k8s.kuboard.cn/layer": "web",                    "k8s.kuboard.cn/name": "nginx"                },                "managedFields": [...]            },            "spec": {                "ports": [                    {                        "name": "nkcers",                        "protocol": "TCP",                        "port": 8080,                        "targetPort": 80                    }                ],                "selector": {                    "k8s.kuboard.cn/layer": "web",                    "k8s.kuboard.cn/name": "nginx"                },                "clusterIP": "10.96.55.66",                "clusterIPs": [                    "10.96.55.66"                ],                "type": "ClusterIP",                "sessionAffinity": "None",                "ipFamilies": [                    "IPv4"                ],                "ipFamilyPolicy": "SingleStack"            },            "status": ...        }        ... ...    ]}

从后果来看testing命名空间下有一个名为nginxDeployment,应用的是nginx:alpine镜像。一个名为nginxServiceClusterIP模式映射8080端口到同名Deployment的80端口。

0x04 创立一个Deployment/Service

func CreateDeployment(g *gin.Context) {    var replicas int32 = 2    var AutomountServiceAccountTokenYes bool = true    deployment := &apiAppv1.Deployment{        TypeMeta:   metav1.TypeMeta{            Kind:       "Deployment",            APIVersion: "apps/v1",        },        ObjectMeta: metav1.ObjectMeta{            Name:  "k8s-test-stub",            Namespace: "testing",            Labels: map[string]string{                "app": "k8s-test-app",            },            Annotations: map[string]string{                "creator":"k8s-operator-test",            },        },        Spec: apiAppv1.DeploymentSpec{            Selector: &metav1.LabelSelector{                MatchLabels: map[string]string{                    "app": "k8s-test-app",                },            },            Replicas:  &replicas,            Template: v1.PodTemplateSpec{                ObjectMeta: metav1.ObjectMeta{                    Labels: map[string]string{                        "app": "k8s-test-app",                    },                },                Spec:v1.PodSpec{                    Containers: []apiCorev1.Container{                        {                            Name:  "nginx",                            Image: "nginx:alpine",                        },                    },                    RestartPolicy: "Always",                    DNSPolicy: "ClusterFirst",                    NodeSelector: nil,                    ServiceAccountName: "",                    AutomountServiceAccountToken:  &AutomountServiceAccountTokenYes,                },            },            Strategy: apiAppv1.DeploymentStrategy{                Type: "RollingUpdate",                RollingUpdate: &apiAppv1.RollingUpdateDeployment{                    MaxUnavailable: &intstr.IntOrString{                        Type:   intstr.String,                        IntVal: 0,                        StrVal: "25%",                    },                    MaxSurge: &intstr.IntOrString{                        Type: intstr.String,                        IntVal: 0,                        StrVal: "25%",                    },                },            },        },    }    dp, err := k8sClient.AppsV1().Deployments("testing").Create(context.Background(), deployment, metav1.CreateOptions{})    if err != nil {        g.AbortWithStatusJSON(500, err)        return    }    g.JSON(200, dp)}

下面的代码就是在testing命名空间创立一个名为k8s-test-stubDeployment。容器应用的是nginx:alpine镜像,replicas指定为2.配置精简了很多非必要的配置项。
执行胜利后咱们能够看到两个pod曾经启动了:

root@main ~# kubectl get pods -n testing --selector=app=k8s-test-appNAME                             READY   STATUS    RESTARTS   AGEk8s-test-stub-7bcdb4f5ff-bmcgf   1/1     Running   0          16mk8s-test-stub-7bcdb4f5ff-cmng8   1/1     Running   0          16m

接下来咱们给这个Deployment创立Service,让它能够对外提供服务,代码如下:

func CreateService(g *gin.Context) {    svc := &apiCorev1.Service{        TypeMeta:   metav1.TypeMeta{            Kind:       "Service",            APIVersion: "v1",        },        ObjectMeta: metav1.ObjectMeta{            Name: "k8s-test-stub",            Namespace: "testing",            Labels: map[string]string{                "app": "k8s-test-app",            },            Annotations: map[string]string{                "creator":"k8s-test-operator",            },        },        Spec:apiCorev1.ServiceSpec{            Ports: []apiCorev1.ServicePort{                {                    Name:        "http",                    Protocol:    "TCP", //留神这里必须为大写                    Port:        80,                    TargetPort:  intstr.IntOrString{                        Type:   intstr.Int,                        IntVal: 80,                        StrVal: "",                    },                    NodePort:    0,                },            },            Selector: map[string]string{                "app": "k8s-test-app",            },            Type: "NodePort",        },    }    svs, err := k8sClient.CoreV1().Services("testing").Create(context.Background(), svc, metav1.CreateOptions{})    if err != nil {        g.AbortWithStatusJSON(500, err)        return    }    g.JSON(200, svs)}

下面代码为k8s-test-stub Deployment创立一个同名的Service。以NodePort形式对外提供服务

root@main ~# kubectl get svc -n testing --selector=app=k8s-test-appNAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGEk8s-test-stub   NodePort    10.96.138.143   <none>        80:30667/TCP   113s

0x05 删除Deployment/Service

func DeleteDeploymentAndService(g *gin.Context) {    //删除Deployment    err := k8sClient.AppsV1().Deployments("testing").Delete(context.Background(), "k8s-test-stub", metav1.DeleteOptions{})    if err != nil {        g.AbortWithStatusJSON(500, err)        return    }    //删除Service    err = k8sClient.CoreV1().Services("testing").Delete(context.Background(), "k8s-test-stub", metav1.DeleteOptions{})    if err != nil {        g.AbortWithStatusJSON(500, err)        return    }    g.JSON(200, nil)}

上述代码删除了testing命名空间中名为k8s-test-stubDeployment和对应的Service

root@main ~# kubectl get deployment,svc -n testing --selector=app=k8s-test-appNo resources found in testing namespace.

四、让你的Operator运行在Kubernetes集群里

后面的代码示例演示了创立命名空间,创立和删除Deployment、Service的基本操作,作为抛砖引玉,更多的操作留待读者去摸索分享。

后面的示例都是间接运行在master主节点的Host环境里,不便咱们援用主节点的权限配置。
咱们的operator最终是要运行在k8s集群里的。如果不进行必要的权限设置,咱们大概率会失去相似以下的谬误:

{    "ErrStatus": {        "metadata": {},        "status": "Failure",        "message": "nodes is forbidden: User \"system:serviceaccount:testing:default\" cannot list resource \"nodes\" in API group \"\" at the cluster scope",        "reason": "Forbidden",        "details": {            "kind": "nodes"        },        "code": 403    }}

下面的返回后果就是nodes操作被禁止了,因为operator没有足够的运行权限。

那如何赋予operator足够的权限来满足咱们的需要?

前文提到过k8s有着严格详尽的权限设计,为了平安思考,集群里一般的容器并没有赋予过多的权限。每个容器默认领有的权限无奈满足大部分operator的性能需要。

咱们先来看看Operator在容器里是如何获取权限配置的。

咱们先从SDK的代码开始。我在SDK中能够找到以下代码:

func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) {    if kubeconfigPath == "" && masterUrl == "" {        klog.Warning("Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.")        kubeconfig, err := restclient.InClusterConfig()        if err == nil {            return kubeconfig, nil        }        klog.Warning("error creating inClusterConfig, falling back to default config: ", err)    }    return NewNonInteractiveDeferredLoadingClientConfig(        &ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},        &ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig()}

这段代码是构建客户端配置的办法。咱们后面在调用这部分代码的时候输出了kubeconfigPath参数,把master节点的权限文件传进来了,所以咱们的operator领有了超级管理员的所有权限。尽管不便,也带了极大的平安危险,Operator领有所有权限能够干很多好事。

从代码能够看到BuildConfigFromFlags函数是容许传入参数空值,在传入的参数为空的时候会调用restclient.InClusterConfig()办法,咱们进入到这个办法:

func InClusterConfig() (*Config, error) {    const (        tokenFile  = "/var/run/secrets/kubernetes.io/serviceaccount/token"        rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"    )    host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")    if len(host) == 0 || len(port) == 0 {        return nil, ErrNotInCluster    }    token, err := ioutil.ReadFile(tokenFile)    if err != nil {        return nil, err    }    tlsClientConfig := TLSClientConfig{}    if _, err := certutil.NewPool(rootCAFile); err != nil {        klog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err)    } else {        tlsClientConfig.CAFile = rootCAFile    }    return &Config{        Host:            "https://" + net.JoinHostPort(host, port),        TLSClientConfig: tlsClientConfig,        BearerToken:     string(token),        BearerTokenFile: tokenFile,    }, nil}

咱们看到代码援用了容器里以下两个文件:

  • /var/run/secrets/kubernetes.io/serviceaccount/token
  • /var/run/secrets/kubernetes.io/serviceaccount/ca.crt

这两个文件就是k8s集群赋予容器的默认权限配置。它其实对应的就是以后命名空间里一个名为defaultServiceAccount(每个命名空间在创立的时候都会附带创立一个defaultServiceAccount并生成一个名称相似default-token-xxxx密文和名为kube-root-ca.crt字典)。上述两个文件映射的就是这两个配置。

更多对于ServiceAccount的常识,请参加官网的文档!

默认的default ServiceAccount满足不了Operator的须要,咱们须要创立一个新的ServiceAccount同时赋予它足够的权限。

首先须要定义ClusterRole

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:  name: k8s-operator、  annotations:    app: k8s-operator-testrules:  - apiGroups:      - apps    resources:      - daemonsets      - deployments      - replicasets      - statefulsets    verbs:      - create      - delete      - get      - list      - update      - watch      - patch  - apiGroups:      - ''    resources:      - nodes      - namespaces      - pods      - services      - serviceaccounts    verbs:      - create      - delete      - get      - list      - patch      - update      - watch

创立新的ServiceAccount,名为k8s-test-operator

apiVersion: v1kind: ServiceAccountmetadata:  name: k8s-test-operator  namespace: testing  annotations:    app: k8s-operator-testsecrets:  - name: k8s-test-operator-token-2hfbn

绑定ClusterRoleServiceAccount:

apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata:  name: k8s-test-operator-cluster  annotations:    app: k8s-operator-testroleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: k8s-operatorsubjects:  - kind: ServiceAccount    name: k8s-test-operator    namespace: testing

执行kubbectl apply -f *.yaml让权限绑定失效,而后咱们在Deployment的配置文件中的以下地位指定新的角色名

deployment.Spec.Template.Spec.ServiceAccountName: "k8s-test-operator"

咱们能够间接执行:kubectl edit deployment operator-test -n testing找到Spec.Template.Spec增加serviceAccountName: k8s-test-operator,使权限绑定失效。

咱们再顺次执行方才的命令

  • 列出所有Node/namespace
  • 列出指定命名空间的Deployment/Services
  • 创立一个Deployment/Service
  • 删除Deployment/Service
    能够看到都能失常的执行

总结

kubernetes operator开发跟平时开发软件没什么区别,最终都是调用ApiServer的http接口。惟一须要关注的是权限,operator只有领有足够的权限就能实现你能设想的所有性能!

demo repo: https://gitee.com/longmon/k8s...