来,手写一个Operator (一)

1 篇文章 0 订阅
1 篇文章 0 订阅

参考:

  • https://time.geekbang.org/column/article/42076 张磊 – 深入剖析kubernetes
  • https://github.com/kubernetes/sample-controller

这篇文章主要介绍一个operator的工作原理,以及如何自己用代码实现一个operator。


在kubernetes项目中,一个API对象在ETCD中的完整资源路径是由Group(API组)Version(API版本)Resource(API资源类型)组成
在这里插入图片描述

现在我要声明要创建一个 CronJob 对象,那么我的 YAML 文件的开始部分会这么写:

apiVersion: batch/v2alpha1
kind: CronJob
...

在这个yaml中,CronJob就是这个 API 对象的资源类型(Resource),batch就是它的组(Group),v2alpha1 就是它的版本(Version)


CRD

在Kubernetes v1.7 之后,出现了一个新的API插件机制:CRD。 CRD的全称是Custom Resource Definition。它指的就是,允许用户在 Kubernetes 中添加一个跟 Pod、Node 类似的、新的 API 资源类型,即:自定义 API 资源。
例如我们要添加一个名字为Nginx的API资源类:

apiVersion: samplecrd.k8s.io/v1
kind: Nginx
metadata:
  name: nginx-sample
spec:
  size: 3
  image: nginx:1.7.9
  ports:
    - name: http
      port: 80
      targetPort: 80

从这个 yaml 可以看到,API 资源类型是 Nginx ,API 组是 samplecrd.k8s.io ,API 版本是 v1。上面的 YAML 文件,就是一个具体的自定义API资源实例,也叫 CR,这个 CR 就类似于我们创建的 deployment。samplecrd.k8s.io/v1 对应的就是 apps/v1 , Nginx 对应的就是 Deployment。我们之所以能创建 deployment 的前提是有 apps/v1 这个版本,或者其他能够识别 Deployment 资源类型的版本,那么我们要创建 Nginx 这个资源类型实例的时候,也需要认识这个 Nginx 资源类型,那么我们就需要通过 CRD 把这个 Resource 创建出来:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: nginxes.samplecrd.k8s.io
spec:
  group: samplecrd.k8s.io
  version: v1
  names:
    kind: Nginx
    plural: nginxes

通过上面的 yaml 我们创建了一个 group 为 apiextensions.k8s.io ,版本为 v1 , resource 为 Nginx 的资源,也就是说我们可以使用下面这种形式创建资源了。

apiVersion: samplecrd.k8s.io/v1
kind: Nginx

自定义控制器(Controller)

在这里插入图片描述

那么像 Deployment 我们可以指定什么 metadata , spec, name, labels 等等,那这个 Nginx 我们可以使用什么字段呢?这时候我们就要写代码了。 从这里开始就属于写operator了,这里有两种办法, 一是通过 client-go 来写,第二个就是通过 operator-sdk 来写。 这里我们通过 client-go 来写,了解一下其中的原理, 第二种方法,我们之后再说。首先我们创建一个代码的框架。

$ tree $GOPATH/src/sample-custom-controller
.
├── controller.go
├── go.mod
├── main.go
└── pkg
    ├── apis
    │   └── samplecontroller
    │       ├── register.go
    │       └── v1
    │           ├── docs.go
    │           ├── register.go
    │           └── types.go
    └── signals
        ├── signal.go
        └── signal_posix.go

在 pkg/apis/sample-controller 目录下创建一个 register.go 的文件,用来防止后面要用到的全局变量

package samplecontroller

const (
	GroupName = "samplecontroller.k8s.io"
	Version   = "v1"
)

然后在 pkg/apis/sample-controller/v1 下创建三个文件 docs.go, register.go, types.go ,我们看下 docs.go 文件:

// +k8s:deepcopy-gen=package
// +groupName=samplecontroller.k8s.io
package v1

在这个文件中,你会看到 +[=value]格式的注释,这就是 Kubernetes 进行代码生成要用的 Annotation 风格的注释。其中,+k8s:deepcopy-gen=package 意思是,请为整个 v1 包里的所有类型定义自动生成 DeepCopy 方法;而+groupName=samplecontroller.k8s.io,则定义了这个包对应的 API 组的名字。

下面看一下 types.go 文件,这个文件定义自定义资源的字段以及字段类型,我们可以在yaml使用什么字段。

// +k8s:deepcopy-gen=package
// +groupName=samplecontroller.k8s.io
package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	corev1 "k8s.io/api/core/v1"
)

type NginxSpec struct {
	Size int32 `json:"size"`
	Image string `json:"image"`
	Resources corev1.ResourceRequirements `json:"resources,omitempty"`
	Envs []corev1.EnvVar `json:"envs,omitempty"`
	Ports []corev1.ServicePort `json:"ports,omitempty"`
}

type NginxStatus struct {
	AvailableReplicas int32 `json:"availablereplicas "`
}


// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Nginx struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   NginxSpec   `json:"spec,omitempty"`
	Status NginxStatus `json:"status,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type NginxList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata"`

	Items []Nginx `json:"items"`
}

+genclient的意思是为这个 API 资源类型生成对应的 Client 代码,+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 意思是在生成 DeepCopy 的时候,实现 Kubernetes 提供的 runtime.Object 接口。否则,在某些版本的 Kubernetes 里,你的这个类型定义会出现编译错误。

下面看一下 register.go 文件:

func Kind(kind string) schema.GroupKind {
	return SchemeGroupVersion.WithKind(kind).GroupKind()
}

func addKnowTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(
		SchemeGroupVersion ,
		&Nginx{},
		&NginxList{},
	)
	metav1.AddToGroupVersion(scheme, SchemeGroupVersion )
	return nil
}

func Resource(resource string) schema.GroupResource {
	return SchemeGroupVersion.WithResource(resource).GroupResource()
}

这个文件的主要功能就是让客户端知道 Nginx 资源类型的定义, 有了这个文件,kubernetes 在生成客户端的时候就知道 Nginx类型和 NginxList 类型了。

有了这些东西,我们就可以通过kubernetes 的代码生成工具为 Nginx 资源类型生成 clientset,informer 和 lister 代码了。

$ export CUSTOM_RESOURCE_NAME="samplecontroller"
$ export CUSTOM_RESOURCE_VERSION="v1"
$ export ROOT_PACKAGE="sample-custom-controller"
$ go get -u k8s.io/code-generator/...
$ /bin/bash $GOPATH/src/pkg/mod/k8s.io/code-generator@v0.18.6/generate-groups.sh all "$ROOT_PACKAGE/pkg/client" "$ROOT_PACKAGE/pkg/apis" "$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"

当最后一条命令报这个错误的时候:
Failed loading boilerplate: open $GOPATH/src/k8s.io/code-generator/hack/boilerplate.go.txt: no such file or directory
可以在 G O P A T H / s r c / 下 创 建 一 个 k 8 s . i o 的 目 录 , 然 后 将 GOPATH/src/ 下创建一个 k8s.io 的目录, 然后将 GOPATH/src/k8s.ioGOPATH/src/pkg/mod/k8s.io/code-generator@v0.x 目录拷贝为 $GOPATH/src/k8s.io/code-generator,然后在执行最后一条命令。

生成了代码之后,我们需要写自定义控制器了。自定义控制器简单来说就是定义对 Nginx 资源的操作,类似于 Controller 对 Deployment 的操作,我在创建一个 Deployment 的时候,会有什么操作,删除的时候需要做什么操作,以及更新等。自定义控制器主要包含 main函数,控制器定义和业务逻辑 三个部分。 main函数的主要作用就是初始化自定义控制器,然后启动它。下面看一下 main 函数定义:

func main() {
	flag.Parse()

	stopCh := signals.SetupSignalHandler()

	cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)

	kubeClient, err := kubernetes.NewForConfig(cfg)
	
	nginxClient, err := clientset.NewForConfig(cfg)

	nginxInformerFactory := informers.NewSharedInformerFactory(nginxClient, time.Second * 30)

	controller := NewController(kubeClient, nginxClient, nginxInformerFactory.Samplecontroller().V1().Nginxes())

	go nginxInformerFactory.Start(stopCh)

	if err = controller.Run(2, stopCh); err != nil {
		glog.Fatalf("Error running controller: %s", err.Error())
	}
}

func init() {
	flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
	flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
}
  • 首先通过 apiserver 的url 和 kubeconfig 文件创建一个 kubernetes 的 client(kubeClient)和 Nginx 对象的 client(nginxClient)
  • 然后创建了一个 nginxInformerFactory ,并使用他生成一个 Nginx 对象的 Informer,传递给控制器
  • 最后main 函数启动上述的 Informer,然后执行 controller.Run,启动自定义控制器。

这个 Informer 是什么?
Informer,其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client。
我们启动控制器,需要从 APIServer 里获取它 Nginx 对象,这个操作是是通过 Informer 库完成的。Informer 与 API 对象是一一对应的,这里传递给控制器的,就是一个 Nginx 对象的 Informer,如果要对 Deployment 进行操作, 那么就需要传递一个 Deployment 的 Informer,这个后面会讲怎么操作一个 Deployment。在创建 Nginx 这个 Informer 的时候,需要用到 nginxClient,因为通过 nginxClient 和 APIserver 建立的连接,但是维护这个连接的是 Reflector 包,Reflector 包通过 ListAndWatch 机制,来获取和监听这些 Nginx 对象实例的变化。


Informer 作用是什么?
Informer 作用主要是有两个:

  • 同步本地缓存:当 APIServer 端有新的实例被创建、删除或者更新, Reflector 都会收到事件通知,此时改事件以及它对应的 API 对象组合就会被放进一个队列中,这个队列叫做 Delta FIFO Queue。Informer 会不断从这个队列中读取元素,判断他的事件类型,然后创建或更新本地对象的缓存。
  • 触发 在 ResourceEventHandler 中注册的事件

下面看一下自定义控制器的代码:

func NewController(
		kubeclientset kubernetes.Interface,
		nginxclientset clientset.Interface,
		nginxInformer informer.NginxInformer,
	) *Controller {

	utilruntime.Must(nginxscheme.AddToScheme(scheme.Scheme))
	klog.Infof("Create Event broadcaster")
	eventBroadcaster := record.NewBroadcaster()
	eventBroadcaster.StartLogging(klog.Infof)
	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})
	recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component:controllerAgentName})

	controller := &Controller {
		kubeclientset: kubeclientset,
		nginxclientset: nginxclientset,
		nginxLister: nginxInformer.Lister(),
		nginxSynced: nginxInformer.Informer().HasSynced,
		workerqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Nginx"),
		recorder: recorder,
	}
	nginxInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: controller.enqueueNginx,
		UpdateFunc: func(old, new interface{}) {
			oldNginx := old.(*samplecontrollerv1.Nginx)
			newNginx := new.(*samplecontrollerv1.Nginx)
			if oldNginx.ResourceVersion == newNginx.ResourceVersion{
				return
			}
			controller.enqueueNginx(new)
		},
		DeleteFunc: controller.enqueueNginxForDelete,
	})
	return controller
}

我们在 main 函数中创建了两个 client(kubeClient 和 nginxClient),和一个 Informer , 这里我们初始化 controller 的时候会用到这三个对象。在自定义控制器中,还设置了一个队列。上面说到 APIServer 有新的 Nginx 实例被创建、删除或者更新的时候,会把事件和对象的一个组合放在一个队列中,说的就是这个队列,但是实际加入队列的不是API对象本身,而是他们的key(<namespace>/<name>)初始化 Controller 后,又给 nginxInformer 绑定了对应的事件,分别对应新增,更新和删除操作。

最后,也是图中的最后一个部分,控制循环,也就是 controller.Run() 启动的控制循环:

func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
	defer utilruntime.HandleCrash()
	defer c.workerqueue.ShutDown()

	if ok := cache.WaitForCacheSync(stopCh, c.nginxSynced); !ok {
		return fmt.Errorf("Failed to wait for caches to sync")
	}

	for i := 0; i < threadiness; i++ {
		go wait.Until(c.runWorker, time.Second, stopCh)
	}

	...
	return nil
}

很容易看出,先等待 Informer 完成一次本地缓存的数据同步,然后通过 goroutine 启动无限循环的任务,而这个无限循环中的逻辑就是我们的业务逻辑。那我们来看一下我们的业务逻辑:

func (c *Controller) runWorker() {
	for c.processNextWorkItem() {
	}
}

func (c *Controller) processNextWorkItem() bool {
	obj, shutdown := c.workerqueue.Get()

	...
	err := func(obj interface{}) error {
		...
		if key, ok = obj.(string); !ok {
			c.workerqueue.Forget(obj)
			return nil
		}
		if err := c.syncHandler(key); err != nil {
			c.workerqueue.AddRateLimited(key)
			return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
		}
		c.workerqueue.Forget(obj)
		return nil
	}(obj)
	... 
}

func (c *Controller) syncHandler(key string) error {
	namespace, name, err := cache.SplitMetaNamespaceKey(key)
	if err != nil {
		utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
		return nil
	}
	nginx, err := c.nginxLister.Nginxes(namespace).Get(name)
	if err != nil {
		if errors.IsNotFound(err) {
			utilruntime.HandleError(fmt.Errorf("Nginx '%s' in work queue no longer exists", key))
			return nil
		}
		return err
	}
	
	c.recorder.Event(nginx, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced)
	// 这里补充你的业务逻辑
	// 例如你要创建一个deployment
	// 自定义控制器代码可以参考:https://github.com/kubernetes/sample-controller/blob/master/controller.go
	// 在sample中您可以看到controller中更详细的代码
	
	return nil
}

首先在队列中获取一个元素,也就是一个key,上面说了这个key并不是一个对象,而是 namespce/name 的一个组合,然后我们在 syncHandler 方法中通过 nginxLister 获取到对应的 Nginx 的对象。获取到 Nginx 对象后,Nginx 对象进行一些操作了。比如 Nginx 对象控制的是一个 deployment, 那我们就可以看这个deployment 是不是存在, 是不是期望的状态,如果不是则进行一些操作。 到此为止,一个简单的自定义控制器就完成了, 我们就可以直接运行了。

$ go run . -kubeconfig=/root/.kube/config -master='http://127.0.0.1:8080'
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值