Kubernetes CRD&Controller入门实践(附PPT)
由《Kubernetes in Action》这本书自定义Controller章节启发,原作里面是作者自己hack了一个Controller(代码在这里),而我想通过纯正的Controller方式实现出来。
目录
CRD是什么
全称Custom Resource Definition,顾名思义就是「自定义资源定义」,也就是按照官方Scheme来定义官方没有的资源struct,即创建你自己的“Pod”,Kubernetes提供了这样的入口就是方便用户扩展Kubernetes,适应更多使用场景。由于是官方配置,所以CRD有它自己的特点或者叫约束:
- 强类型
- 能够被订阅更新事件,本质上是让api server能够识别
Controller是什么
也就是「控制器」,控制Kubernetes的资源实体。怎么控制呢?通过监听资源变化事件。这个事件可能是用户发起的(他希望把资源从A状态更新到B状态),Controller就会获取这个事件并处理事件,即更新目标资源。Kubernetes默认有很多控制器,他们控制着Kubernetes默认资源,如Pod、Deployment、Service等,他们都包含在Controller Manager中。但如果你的资源是个CRD,因为没有对应的控制器,你就得为它自己写Controller了。
改造「Kubernetes In Action」例子的计划
正如引文里面提到的,作为第一次写Controller,我想使用正确的方式去实现,有意不去用像Operator等成熟框架去写,使用更原生的方式更能帮助理解工作原理。Kubernetes官方提供了一个样例,里面提供的方式很原生,也够简单,所以准备“以瓢画葫”了。
“只要一个git repo就可以创建一个网站”,是这次代码实现的目标。开发计划如下:
- 创建CRD叫website,接受git repo地址、deployment name、replicas
- 定义Controller行为
- 创建Deployment ,一个pod包含两个容器服务,nginx 和 git-sync。git-sync用来同步git repo的代码
- 创建NodePort Service,提供对外访问能力
- 部署Controller服务
开始动手,定义Website CRD
以下是我的例子:
type Website struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec WebsiteSpec `json:"spec"` Status WebsiteStatus `json:"status"` } // 这里是CRD的主体,面向用户的日常使用 type WebsiteSpec struct { GitRepo string `json:"gitRepo"` DeploymentName string `json:"deploymentName"` Replicas *int32 `json:"replicas"` } type WebsiteStatus struct { AvailableReplicas int32 `json:"availableReplicas"` } type WebsiteList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Website `json:"items"` }
自动生成Controller基础代码
跟官方例子一样,我也使用「code generator」这个工具,基于已经定义好的CRD,自动生成Controller基础代码。先来看下你需要准备的代码框架:
如上图,首先需要定义好4个文件,其中:
- doc.go,这个文件的内容很简单,主要就是占位
// 下面两行是用来帮助生成Controller代码的 // +k8s:deepcopy-gen=package // +groupName=mycontroller.nevermosby.io // Package v1alpha1 是定义Controller的v1alpha1版本 // 所以你可以定义多个版本的Controller package v1alpha1
- types.go,这个文件就是上文CRD的定义
- register.go(内层),这个文件有点复杂,个人理解主要是为CRD作CustomResourceValidation,本次入门就不展开讨论了
- register.go(外层),这个文件也很简单,主要是给CRD取个groupname
package mycontroller // 定义CRD的GroupName const ( GroupName = "mycontroller.nevermosby.io" )
大家可以去我的GitHub上看到完整代码:
https://github.com/nevermosby/my-crd-controller
接下来就是使用「generate-groups.sh」这个文件,开始代码生成工作,以下是帮助文档:
./generate-groups.sh -h Usage: generate-groups.sh <generators> <output-package> <apis-package> <groups-versions> ... <generators> the generators comma separated to run (deepcopy,defaulter,client,lister,informer) or "all". <output-package> the output package name (e.g. github.com/example/project/pkg/generated). <apis-package> the external types dir (e.g. github.com/example/api or github.com/example/project/pkg/apis). <groups-versions> the groups and their versions in the format "groupA:v1,v2 groupB:v1 groupC:v2", relative to <api-package>. ... arbitrary flags passed to all generator binaries. Examples: generate-groups.sh all github.com/example/project/pkg/client github.com/example/project/pkg/apis "foo:v1 bar:v1alpha1,v1beta1" generate-groups.sh deepcopy,client github.com/example/project/pkg/client github.com/example/project/pkg/apis "foo:v1 bar:v1alpha1,v1beta1"
以下是我自己使用的命令:
./vendor/k8s.io/code-generator/generate-groups.sh \ # 期望生成的函数列表 "deepcopy,client,informer,lister" \ # 生成代码的目标目录 github.com/nevermosby/my-crd-controller/pkg/client \ # CRD所在目录 github.com/nevermosby/my-crd-controller/pkg/apis \ # CRD的group name和version "mycontroller:v1alpha1" \ # 这个文件里面其实是开源授权说明,但如果没有这个入参,该命令无法执行 --go-header-file /Users/davidli/gh/myk8scrd/src/github.com/nevermosby/my-crd-controller/hack/custom-boilerplate.go.txt
执行完成后,在原来定义CRD的目录下,你会得到下面的文件结构:
如上图,多出了「zzgenerated.deepcopy.go」这个文件,里面都是一堆「deepcopy」方法,实现CRD的复制功能。
在期望生成代码的目标目录下,你会得到如下的文件结构:
主要包含了「clientset」、「lister」和「informers」三个组件:「clientset」是使用CRD的SDK,对外暴露接口。「lister」和「informers」这里面的代码就是监听CRD变化事件并留出写handler的入口。
至此为止,新Controller的框架已经搭起来了。
编写Controller业务逻辑代码
Controller业务逻辑代码是无法自动生成的。通常的编码逻辑是,写一个入口用来初始化这个Controller,并填充上文提到的handler入口。
1.初始化Controller
在项目的main方法里,添加初始化Controller的核心代码如下:
controller := NewController(kubeClient, exampleClient, kubeInformerFactory.Apps().V1().Deployments(), kubeInformerFactory.Core().V1().Services(), exampleInformerFactory.Mycontroller().V1alpha1().Websites()) // notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh) // Start method is non-blocking and runs all registered informers in a dedicated goroutine. kubeInformerFactory.Start(stopCh) exampleInformerFactory.Start(stopCh) if err = controller.Run(2, stopCh); err != nil { klog.Fatalf("Error running controller: %s", err.Error()) }
详细初始化逻辑可以参照这里:https://github.com/nevermosby/my-crd-controller/blob/master/controller.go#L91
2.针对不同事件,实现处理事件的handler
针对「新增」、「更新」和「删除」这3种事件,实现了不同的处理行为,最终都是「syncHandler」这个方法来统一处理,核心逻辑就是:首先查找目标资源有没有,没有就创建。目标资源包括deployment和service。然后对比已经创建的资源状态是否符合期待,如副本(replica)数量是否满足。创建资源的核心代码参考如下:
// 创建service func newService(website *myv1alpha1.Website) *v1core.Service { deploymentName := website.Spec.DeploymentName // use deploynment name for service name serviceName := fmt.Sprintf("%s-%s", deploymentName, "npsvc") labels := map[string]string{ "app": "website", "controller": website.Name, } selectLabels := map[string]string{ "app": "website-nginx", "controller": website.Name, } return &v1core.Service{ ObjectMeta: metav1.ObjectMeta{ Name: serviceName, Namespace: website.Namespace, Labels: labels, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(website, myv1alpha1.SchemeGroupVersion.WithKind("Website")), }, }, Spec: v1core.ServiceSpec{ Ports: []v1core.ServicePort{ { Port: 80, Protocol: v1core.ProtocolTCP, }}, Type: v1core.ServiceTypeNodePort, Selector: selectLabels, }, } } // 创建deployment func newDeployment(website *myv1alpha1.Website) *appsv1.Deployment { labels := map[string]string{ "app": "website-nginx", "controller": website.Name, } return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: website.Spec.DeploymentName, Namespace: website.Namespace, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(website, myv1alpha1.SchemeGroupVersion.WithKind("Website")), }, }, Spec: appsv1.DeploymentSpec{ Replicas: website.Spec.Replicas, Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, }, Spec: corev1.PodSpec{ Containers:[]corev1.Container{ { // nginx container for hosting website Name: "nginx", Image: "nginx", VolumeMounts: []corev1.VolumeMount{ { Name: "html", MountPath: "/usr/share/nginx/html", ReadOnly: true, }, }, Ports: []corev1.ContainerPort{ { ContainerPort: 80, Protocol: "TCP", }, }, }, { // git sync container for fetching code Name: "git-sync", Image: "openweb/git-sync", Env: []corev1.EnvVar{ { Name: "GIT_SYNC_REPO", Value: website.Spec.GitRepo, }, { Name: "GIT_SYNC_DEST", Value: "/gitrepo", }, { Name: "GIT_SYNC_BRANCH", Value: "master", }, { Name: "GIT_SYNC_REV", Value: "FETCH_HEAD", }, { Name: "GIT_SYNC_WAIT", Value: "3600", }, }, VolumeMounts: []corev1.VolumeMount{ { Name: "html", MountPath: "/gitrepo", }, }, }, }, Volumes: []corev1.Volume{ { Name: "html", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: "", }, }, }, }, }, }, }, } }
这里有个麻烦的地方,相信写过Controller的同学都会遇到——原来通过yaml创建资源是件相对简单直接的事情,现在换成用go语言,变得到处都是大括号,还得找struct的key叫什么。。。一地鸡毛+反人类!
终于,拯救程序员还得靠程序员自己。强烈推荐下面的工具,绝对save the day:http://structify.carsonoid.net/
部署Controller服务到Kubernetes集群中
像用go语言写的Controller,一般只要写完编译出二进制文件,塞进容器镜像就完事儿了,随时随地docker run启动即可。为了管理方便,都是把它们部署到Kubernetes集群中去。一般两种模式:
- 使用deployment方式,跟管理应用一样,简单并能保证高可用,一般就部署一个pod
- 纯pod方式,类似Kubernetes核心基础组件,这种方式用的不多,如果是定位成很底层的Controller会这么玩
大功告成,来看效果
用户提交一个website资源,自动创建相关deployment和service;删除website资源时,也会自动删除相关资源。下面的demo里还用到了kubewatch和mattermost(下面是个gif动图,请耐心等待加载完毕):
一旦deployment和service创建完毕后,就能对外服务了:
简述Controller工作原理
要完全说清Controller工作原理其实整一篇文章都不够,主要是涉及太多基础知识,所以我这边就给个简述版,算是给自己做个笔记。
参照下图,主要使用到 Informer和workqueue两个核心组件。Controller可以有一个或多个informer来跟踪某一个resource。Informter跟API server保持通讯获取资源的最新状态并更新到本地的cache中,一旦跟踪的资源有变化,informer就会调用callback。把关心的变更的Object放到workqueue里面。然后woker执行真正的业务逻辑,计算和比较workerqueue里items的当前状态和期望状态的差别,然后通过client-go向API server发送请求,直到驱动这个集群向用户要求的状态演化。
当然有框架专门写Controller
大家也看到了,写一套CRD和Controller绝对不是简单的事情,因此社区提供了相关框架去尽量简化复杂度,让使用者能够更方便地专注业务,甚至连业务都不用写,以下是几个主流框架:
- Kubebuilder,是Kubernetes社区官方推荐的方案,用下来比较顺手,其本质就是本次文章的内容
- Operator Framework,是红帽主导的专门用来开发Kubernetes Operator的框架,那Operator又是什么呢?引用别人的一句定义:
Operator 是一种特殊的自定义 Controller,它的编写者,一定是某个“能力”对应的领域专家比如 TiDB 的开发人员,因为它包含了对这个领域的所有运维能力,如安装、升级、备份、恢复等。
- Kudo,全称「The Kubernetes Universal Declarative Operator」,定位和Operator很像,官方叫「Automate Day-2 Operations」。主要社区维护方是D2iQ,也就是Mesosphere(为什么我突然泪目了)。这款我已经做了深度体验,感觉很不错,期待和大家分享。
这三个框架之间的关系有点「本是同根生相煎何太急」,将来有机会给大家整理下。
参考材料
- 与本文相关的一个ppt,链接: https://pan.baidu.com/s/1UoQVl8MmQrbq9w2gyN_GUw,提取码: 7aa4
1 对 “Kubernetes CRD&Controller入门实践(附PPT)”的想法;