一、介绍
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 URLkubeconfigPath
参数就是权限配置文件门路。
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
命名空间下有一个名为nginx
的Deployment
,应用的是nginx:alpine
镜像。一个名为nginx
的Service
以ClusterIP
模式映射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-stub
的Deployment
。容器应用的是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-stub
的Deployment
和对应的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集群赋予容器的默认权限配置。它其实对应的就是以后命名空间里一个名为default
的ServiceAccount
(每个命名空间在创立的时候都会附带创立一个default
的ServiceAccount
并生成一个名称相似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
绑定ClusterRole
到ServiceAccount
:
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...