一、介绍
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.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
命名空间下有一个名为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-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-stub
的Deployment
和对应的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集群赋予容器的默认权限配置。它其实对应的就是以后命名空间里一个名为default
的ServiceAccount
(每个命名空间在创立的时候都会附带创立一个default
的ServiceAccount
并生成一个名称相似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
绑定ClusterRole
到ServiceAccount
:
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…
发表回复