由《Kubernetes in Action》这本书自定义Controller章节启发,原作里面是作者自己hack了一个Controller(代码在这里),而我想通过纯正的Controller方式实现出来。
全称Custom Resource Definition,顾名思义就是「自定义资源定义」,也就是按照官方Scheme来定义官方没有的资源struct,即创建你自己的“Pod”,Kubernetes提供了这样的入口就是方便用户扩展Kubernetes,适应更多使用场景。由于是官方配置,所以CRD有它自己的特点或者叫约束:
也就是「控制器」,控制Kubernetes的资源实体。怎么控制呢?通过监听资源变化事件。这个事件可能是用户发起的(他希望把资源从A状态更新到B状态),Controller就会获取这个事件并处理事件,即更新目标资源。Kubernetes默认有很多控制器,他们控制着Kubernetes默认资源,如Pod、Deployment、Service等,他们都包含在Controller Manager中。但如果你的资源是个CRD,因为没有对应的控制器,你就得为它自己写Controller了。
正如引文里面提到的,作为第一次写Controller,我想使用正确的方式去实现,有意不去用像Operator等成熟框架去写,使用更原生的方式更能帮助理解工作原理。Kubernetes官方提供了一个样例,里面提供的方式很原生,也够简单,所以准备“以瓢画葫”了。
“只要一个git repo就可以创建一个网站”,是这次代码实现的目标。开发计划如下:
以下是我的例子:
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"`
}
跟官方例子一样,我也使用「code generator」这个工具,基于已经定义好的CRD,自动生成Controller基础代码。先来看下你需要准备的代码框架:
如上图,首先需要定义好4个文件,其中:
// 下面两行是用来帮助生成Controller代码的
// +k8s:deepcopy-gen=package
// +groupName=mycontroller.nevermosby.io
// Package v1alpha1 是定义Controller的v1alpha1版本
// 所以你可以定义多个版本的Controller
package v1alpha1
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,并填充上文提到的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/
像用go语言写的Controller,一般只要写完编译出二进制文件,塞进容器镜像就完事儿了,随时随地docker run启动即可。为了管理方便,都是把它们部署到Kubernetes集群中去。一般两种模式:
用户提交一个website资源,自动创建相关deployment和service;删除website资源时,也会自动删除相关资源。下面的demo里还用到了kubewatch和mattermost
一旦deployment和service创建完毕后,就能对外服务了:
要完全说清Controller工作原理其实整一篇文章都不够,主要是涉及太多基础知识,所以我这边就给个简述版,算是给自己做个笔记。
参照下图,主要使用到 Informer和workqueue两个核心组件。Controller可以有一个或多个informer来跟踪某一个resource。Informter跟API server保持通讯获取资源的最新状态并更新到本地的cache中,一旦跟踪的资源有变化,informer就会调用callback。把关心的变更的Object放到workqueue里面。然后woker执行真正的业务逻辑,计算和比较workerqueue里items的当前状态和期望状态的差别,然后通过client-go向API server发送请求,直到驱动这个集群向用户要求的状态演化。
大家也看到了,写一套CRD和Controller绝对不是简单的事情,因此社区提供了相关框架去尽量简化复杂度,让使用者能够更方便地专注业务,甚至连业务都不用写,以下是几个主流框架:
Operator 是一种特殊的自定义 Controller,它的编写者,一定是某个“能力”对应的领域专家比如 TiDB 的开发人员,因为它包含了对这个领域的所有运维能力,如安装、升级、备份、恢复等。
这三个框架之间的关系有点「本是同根生相煎何太急」,将来有机会给大家整理下。