关于kubernetes:Kubernetes-Operator-开发入门

6次阅读

共计 12189 个字符,预计需要花费 31 分钟才能阅读完成。

一、介绍

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.conf
cfg, 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

// 列出指定命名空间的 deployment
func 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)
}
// 列出指定命名空间的 Services
func 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-app
NAME                             READY   STATUS    RESTARTS   AGE
k8s-test-stub-7bcdb4f5ff-bmcgf   1/1     Running   0          16m
k8s-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-app
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
k8s-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-app
No 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/v1
kind: ClusterRole
metadata:
  name: k8s-operator、annotations:
    app: k8s-operator-test
rules:
  - 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: v1
kind: ServiceAccount
metadata:
  name: k8s-test-operator
  namespace: testing
  annotations:
    app: k8s-operator-test
secrets:
  - name: k8s-test-operator-token-2hfbn

绑定 ClusterRoleServiceAccount:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: k8s-test-operator-cluster
  annotations:
    app: k8s-operator-test
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: k8s-operator
subjects:
  - 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…

正文完
 0