
1. 概述
我们将 CRD, Controller, Webhook 三者合起来叫 Operator。一个 Operator 工程一般必须包含 CRD 和 Controller,Admission 是可选的。如果说 Kubernetes 是 "操作系统" 的话,Operator 是 Kubernetes 的第一层应用,它部署在 Kubernetes 里,使用 Kubernetes "扩展资源" 接口的方式向更上层用户提供服务。
Operator的实现方式主要包括OperatorSDK和KubeBuilder,目前KubeBuilder在阿里使用的比较多。
KubeBuilder
https://github.com/kubernetes-sigs/kubebuilder
OperatorSDK
https://github.com/operator-framework/operator-sdk
下文的接入流程中,我们主要选择KubeBuilder进行介绍。
2. 名词解释
GVKs&GVRs:
GVK = GroupVersionKind,GVR = GroupVersionResource
API Group & Versions(GV):
API Group是相关API功能的集合,每个Group拥有一或多个Versions,用于接口的演进。
Kinds & Resources:
每个GV都包含多个API类型,称为Kinds,在不同的Versions之间同一个Kind定义可能不同, Resource是Kind的对象标识
(resource type)
https://kubernetes.io/docs/reference/kubectl/overview/#resource-types,一般来说Kinds和Resources是1:1的,比如pods Resource对应Pod Kind,但是有时候相同的Kind可能对应多个Resources,比如Scale Kind可能对应很多Resources:deployments/scale,replicasets/scale,对于CRD来说,只会是1:1的关系。
每一个GVK都关联着一个package中给定的root Go type,比如apps/v1/Deployment就关联着K8s源码里面k8s.io/api/apps/v1 package里面的Deployment struct,我们提交的各类资源定义YAML文件都需要写:
-
apiVersion
这个就是GV -
kind
这个就是K
{"kind": "CronJob","apiVersion": "batch.tutorial.kubebuilder.io/v1",...}
-
负责运行所有的Controllers -
初始化共享caches,包含listAndWatch功能 -
初始化clients用于与Api Server通信;
3. List-Watch机制
-
更快地返回 List/Get 请求,减少对 Kubenetes API 的直接调用
-
依赖 Kubernetes List/Watch API
-
可监听事件并触发回调函数
-
二级缓存
4. CRD规范
4.1 命名规范
-
CRD 的全名一般要符合如下的命名规范: {Kind}.{Group}.{Organization}.{Domain}。其中: -
{Kind} 即为 CRD 真正的短名字,用精简的单个或多个英文单词的拼接来命名真正的 CRD 短名字。如 AdvancedDeployment,NoteBook 等。使用大驼峰命名法(首字母也是大写,UpperCamelCase)。 -
{Group} 必须是一种功能类别,如 ops, apps, auth 等。尽量用精简的单个英语单词的方式传达你的 CRD 属于的“类别”。组成的字母必须都是小写 -
{Organization} 为仓库的 git Group,即 团队名英文简称。 -
{Domain},即 Company Name Domain,例如alibaba-inc.com。 -
目前对于 CRD 版本转化不太友好,统一使用 v1
4.2 Spec, Status 规范
-
在上一节用命令在 apis 包下生成 CRD Types 之后,请不要随意修改 apis 里的结构体、命名规则、以及注释。 -
只能、也只需要修改 {Kind}_types.go 文件里 Spec 和 StatusSpec 结构体里的内容。 -
Spec 和 StatusSpec 里的字段都必须是 Public 的,也就是字段名首字母是大写。 -
每个字段,都应该写上 JSON Tag,JSON Tag 必须使用 小驼峰命名法,即 LowerCamelCase。 -
如果字段允许为空,JSON Tag 记得带上 omitempty。StatusSpec 的字段一般都是允许为空的!例子:
type MySpec struct {// FiledA 允许为空FieldA string `json:"fieldA,omitempty"`// FiledB 不允许为空FieldB string `json:"fieldB"`}
5. 快速接入
5.1 安装环境
-
go https://golang.org/dl/
1.13+ -
docker https://docs.docker.com/install/
17.03+ -
kubectl https://kubernetes.io/docs/tasks/tools/install-kubectl/
1.11.3+:
brew install kubectl
-
helm https://github.com/helm/helm
3.2.2+:
brew install helm
-
kustomize https://sigs.k8s.io/kustomize/docs/INSTALL.md
3.1.0+
如果kubectl版本为1.14+,则无需安装,使用kubectl kustomize即可
-
controller-tools https://github.com/kubernetes-sigs/controller-tools/releases
0.3.0+
go get https://github.com/kubernetes-sigs/controller-tools.git
-
kubebuilder https://book.kubebuilder.io/quick-start.html#installation
2.3.1+
下载压缩包:kubebuilder_2.3.1_${os}_${arch}解压并复制到:/usr/local/kubebuilder添加环境变量:export PATH=$PATH:/usr/local/kubebuilder/bin
-
设置环境变量
打开配置文件:open ~/.bash_profile添加环境变量:export GOBIN=$GOPATH/binexport PATH=$PATH:$GOBINexport GOPATH=/Users/yunzheng/goexport GOROOT=/usr/local/goexport GO111MODULE=onexport GOPROXY=https://goproxy.cn执行修改的文件:source ~/.bash_profile
-
必须开启go mod,GO111MODULE=on -
代理地址推荐:
-
七牛云:https://goproxy.cn 推荐,可解决依赖问题 -
阿里云:https://mirrors.aliyun.com/goproxy/,不推荐,某些包依然无法下载 -
腾讯:https://goproxy.io,不推荐,有些包无法下载
-
开发工具推荐:
-
goland:推荐,但是收费 -
idea:可以装go插件,但是可能没有goland适配的好 -
vscode:比较轻量级,但是开发不太方便
5.2 创建工程
-
创建脚手架工程
mkdir myapp-operatorcd myapp-operator/kubebuilder init --domain my.alibaba-inc.com
-
创建 API,生成CRD和Controller
kubebuilder create api --group apps --version v1alpha1 --kind Myapp注意:1)group参数表示组的概念2)version定义版本3)kind定义自定义资源类型4)以上参数组成 自定义yaml 的 apiVersion和kind
-
如果需要在Myapp CRUD 时进行合法性检查, 可以生成webhook:
kubebuilder create webhook
-
初始化基础的依赖包信息
go mod init
5.3 编写代码
5.3.1 定义 CRD
// MyappSpec defines the desired state of Myapptype MyappSpec struct {// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster// Important: Run "make" to regenerate code after modifying this file// Foo is an example field of Myapp. Edit Myapp_types.go to remove/updateFoo string `json:"foo,omitempty"`appsv1.DeploymentSpec `json:",inline"`}// MyappStatus defines the observed state of Myapptype MyappStatus struct {// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster// Important: Run "make" to regenerate code after modifying this fileappsv1.DeploymentStatus `json:",inline"`Phase MyappPhase `json:"phase"`}// +kubebuilder:object:root=true// +kubebuilder:subresource:status// Myapp is the Schema for the myapps APItype Myapp struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec MyappSpec `json:"spec,omitempty"`Status MyappStatus `json:"status,omitempty"`}
5.3.2 编写Reconcile逻辑
func (r *MyappReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {ctx := context.Background()log := r.Log.WithValues("myapp", req.NamespacedName)// your logic herelog.Info("start Reconcile" + req.Name)...}
5.3.3 编写断言
var oldSpec interface{}var oldObjectMeta metav1.ObjectMetavar newSpec interface{}var newObjectMeta metav1.ObjectMetaswitch e.MetaOld.(type) {case *appsv1alpha1.Myapp:oldSpec = e.MetaOld.(*appsv1alpha1.Myapp).SpecoldObjectMeta = e.MetaOld.(*appsv1alpha1.Myapp).ObjectMetanewSpec = e.MetaNew.(*appsv1alpha1.Myapp).SpecnewObjectMeta = e.MetaNew.(*appsv1alpha1.Myapp).ObjectMetacase *appsv1.Deployment:oldSpec = e.MetaOld.(*appsv1.Deployment).SpecoldObjectMeta = e.MetaOld.(*appsv1.Deployment).ObjectMetanewSpec = e.MetaNew.(*appsv1.Deployment).SpecnewObjectMeta = e.MetaNew.(*appsv1.Deployment).ObjectMetacase *corev1.Pod:oldSpec = e.MetaOld.(*corev1.Pod).SpecoldObjectMeta = e.MetaOld.(*corev1.Pod).ObjectMetanewSpec = e.MetaNew.(*corev1.Pod).SpecnewObjectMeta = e.MetaNew.(*corev1.Pod).ObjectMeta}if !reflect.DeepEqual(oldSpec, newSpec) ||oldObjectMeta.DeletionTimestamp != nil ||newObjectMeta.DeletionTimestamp != nil {log.Info("Update event has new metadata", "event")return true}
5.3.4 修改Webhook
func (r *Myapp) ValidateCreate() errorfunc (r *Myapp) ValidateUpdate(old runtime.Object) errorfunc (r *Myapp) Default()
5.3.5 修改main入口
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{Scheme: scheme,MetricsBindAddress: metricsAddr,LeaderElection: enableLeaderElection,LeaderElectionID: "myapp-operator",Port: 9443,LeaderElectionNamespace: os.Getenv("POD_NAMESPACE"),Namespace: os.Getenv("POD_NAMESPACE"),})
-
更多详情查看demo例子
5.4 调试工程
-
配置Makefile
注意 kustomize build都改为kubectl kustomize
-
生产CRD:make manifests -
编译工程:make -
部署CRD:make install -
部署RBAC相关Yaml -
运行CR:make run
-
部署CR
kubectl apply -f config/samples/apps_v1alpha1_myapp.yaml
-
在idea或者goland中打断点调试
5.5 打包构建
-
配置Dockerfile
# Build the manager binaryFROM golang:1.12.5 as builderWORKDIR /workspace# Copy the Go Modules manifestsCOPY go.mod go.modCOPY go.sum go.sum# cache deps before building and copying source so that we don't need to re-download as much# and so that source changes don't invalidate our downloaded layer# 设置代理RUN GO111MODULE=on GOPROXY=https://goproxy.cn go mod download# Copy the go sourceCOPY main.go main.goCOPY api/ api/COPY controllers/ controllers/# BuildRUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go# Use distroless as minimal base image to package the manager binary# Refer to https://github.com/GoogleContainerTools/distroless for more details# 设置基础镜像FROM reg.docker.alibaba-inc.com/alibase/alios7u2-min:latestWORKDIR /COPY --from=builder /workspace/manager .USER admin:adminENTRYPOINT ["/manager"]
-
设置代理
RUN GO111MODULE=on GOPROXY=https://goproxy.cn go mod download
-
设置基础镜像
FROM reg.docker.alibaba-inc.com/alibase/alios7u2-min:latest
-
打镜像
cd myapp-operatormake docker-build
5.6 上传镜像
-
创建命名空间
-
创建镜像仓库
-
上传镜像
$ sudo docker login --username=[username] registry.cn-hangzhou.aliyuncs.com$ sudo docker tag [ImageId] registry.cn-hangzhou.aliyuncs.com/amwp/myapp-operator:[镜像版本号]$ sudo docker push registry.cn-hangzhou.aliyuncs.com/amwp/myapp-operator:[镜像版本号]
5.7 Helm部署
6. 源码解读
6.1 KubeBuilder对Controller的逻辑封装
-
KubeBuilder引入了Manager这个概念,一个Manager可以管理多个Controller,而这些Controller会共享Manager的Client; -
如果manager挂掉或者停止了,所有的controller也会随之停止; -
kubebuilder使用一个map[GroupVersionKind]informer来管理这些controller,所以每个controller还是拥有其独立的workQueue,deltaFIFO,并且kubebuilder也已经帮我们实现了这部分代码; -
我们主要需要做的开发,就是写Reconcile中的逻辑。 -
Manager通过map[GroupVersionKind]informer启动所有Controller:
6.2 Predicate和Controller中接收事件的区别
-
初始化share_informer后,会注册 eventHandler(pkg/kubelet/kubelietconfig/watch.go) -
接下来开始执行shared_informer的run方法
-
调用handler中的OnUpdate方法
-
在OnUpdate方法中会根据predicate过滤器来进行事件的过滤。此时事件类型还 是event.UpdateEvent
-
接下来会将该事件入队,此时事件类型转换为了 reconcile.Request(handler/enqueue.go)
-
controller启动后(internal/controller/controller.go),会启动worker goroutine
-
不停的从队列里面取事件,进行扔到reconcileHandler中进行处理
-
将事件传递到reconcile逻辑中,此时reconcile入参类型ctrl.Request,该类型是和reconcile.Request是同一个东西
7. 常见问题
7.1 the server could not find the requested resource
// +kubebuilder:object:root=true// +kubebuilder:subresource:status// Myapp is the Schema for the myapps APItype Myapp struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec MyappSpec `json:"spec,omitempty"`Status MyappStatus `json:"status,omitempty"`}
7.2 删除回收器Finalizer
time.Sleep(time.Second * 10)if err := r.Delete(ctx, vm); err != nil {log.Error(err, "unable to delete vm ", "vm", vm)}
vm.ObjectMeta.Finalizers = append(vm.ObjectMeta.Finalizers, "app.kubeone.alibaba-inc.com")
如果 DeleteionTimestamp不存在如果没有Finalizers加上Finalizers,并更新CRD要不然,说明是要被删除的如果存在Finalizers,删除Finalizers,并更新CRD
if cronJob.ObjectMeta.DeletionTimestamp.IsZero() {if !containsString(cronJob.ObjectMeta.Finalizers, myFinalizerName) {cronJob.ObjectMeta.Finalizers = append(cronJob.ObjectMeta.Finalizers, myFinalizerName)if err := r.Update(context.Background(), cronJob); err != nil {return ctrl.Result{}, err}}} else {if containsString(cronJob.ObjectMeta.Finalizers, myFinalizerName) {if err := r.deleteExternalResources(cronJob); err != nil {return ctrl.Result{}, err}cronJob.ObjectMeta.Finalizers = removeString(cronJob.ObjectMeta.Finalizers, myFinalizerName)if err := r.Update(context.Background(), cronJob); err != nil {return ctrl.Result{}, err}}}
8. 参考资料
-
kubebuilder2.0学习笔记——搭建和使用 https://segmentfault.com/a/1190000020338350 -
kubebuilder2.0学习笔记——进阶使用 https://segmentfault.com/a/1190000020359577 -
Operator开发规范 https://zhuanlan.zhihu.com/p/73036278 -
Metacontroller 入门指南 https://zhuanlan.zhihu.com/p/73036278
更多精彩

识别二维码观看直播
走出舒适圈,从来都不简单
Bilibili资深运维工程师:DCDN在游戏应用加速中的实践





