第 1 部分 故事
「你叫yann是吧,看来你对k8s 很了解,是么?」
我巴拉巴拉开始自我介绍。
「那你使用过CRD么?」
我委婉的表示:了解过,但是因为诸事繁忙,一直没机会去使用。
「好的,你回去等消息吧。」
凉凉。
以上为灰灰老师的经典小段子,借过来博君一笑。
第 2 部分 定义
定义
既然吃瘪了,就要好好复习,把这块缺陷补上。yann 没有说大话,之前确实看到过相关内容,不过一时没有想到具体用途,就没有多加留意,结果就踢到铁板了。
注:以下内容属于自行理解,如有不同见解欢迎留言交流。
简单来说,CRD 就是让用户自定义资源,资源这个说法比较抽象,但是Deployment,StatefulSet 都是资源。CRD 就是在做和这些组件类似的资源。
既然定义明白了,具体要怎样操作呢?这个过程和面向对象的开发过程有点像。面向对象的基本做法是,先定义一个类, 再把这个类实例化。CRD同样,先定义出一个类型来,再根据这个类型创建对象出来。没错,这个类型就是 Kubernetes 里面的 kind 项。
流程
既然知道了 CRD 的定义内容,那让我们把 Kubernetes 中CRD的使用流程也了解一下吧。
CRD仅仅是资源的定义,而 Controller 可以去监听CRD的CRUD事件来添加自定义业务逻辑。Controller 就像个调整器一样, 不停的把资源向预定状态调整。
当然,以上只是最简单的流程,具体还有相当的细节。例如 API中的对象的变化会存入一个队列, Controller是会消费队列的,遇到了再详细说明。
配置文件
既然了解了流程,让我们从头来开始。现在 yann 的环境是一台全新安装完毕的 Kubernetes 集群。版本比较老,是1.10的,不过 CRD 功能 在1.9 版本就存在了,所以可以演示。
首先做一个新的资源定义文件,让我们来看一下配置文件的内容:
这个配置文件虽然看起来很复杂,但是可以分成固定的四部分:
- apiVersion: api版本
- kind:对象的类别
- metadata:元数据
- spec:希望能达到的状态
基本上这四个部分,在每个配置文件上都会固定出现的。确定了版本和类别,再补充元数据和期望状态,一份配置文件就完成了。
在本例中,元数据只包含了 name 属性,而 spec 包含了组、版本、范围、names 等多个属性。也就是说, 大部分工作都是在 spec 里面定义的。
注意,该文件的类型 是CustomResourceDefinition,而我们定义出来的类型 Yann 是包含在 names 属性中的。
结果如图, 其他部分,明天接续。
第 3 部分 演示
Deployment
昨天提到的 Deployment 其实相当于订单或需求清单。比如说,yann 需要建立一个集群,里面有3个容器副本,就可以用 Deployment 来建立。
当然,这么定义是不严谨的。但是暂时可以这样理解:Deployment 包含了一个任务中的容器数量及所用的镜像,还有的其他一些属性。
yann 觉得先有概念,在逐步完善是一个很好的学习过程。yann 很不喜欢接受新事物的开始,就背负一堆负担。正常的学习路线应该是先搞懂原理,了解主要组件,然后再细化实践。而不是精雕细琢,记住每一个细碎知识点。结果半年以后还停在第1章,那就已经凉凉了。
演示
下面通过视频的方式给大家演示,通过 Deployment 创建一个集群的过程。大家只要有个概念:什么样的操作会产生什么样的结果就好了, 不必深究细节。
微信里的视频拿不出来,麻烦跳转观看.
视频有一分半, 中间电脑有点卡了,所以画面停顿,请多等一下。
看完视频只要体会了以下两个知识点就可以了:
- Deployment 类型的配置文件就像资源清单一样,yann 在里面定义需要3个 nginx 容器
- 通过 kubectl 命令 + 配置文件, 可以创造出容器集群
其实视频里面还定义了服务,并检验了容器和服务的可用性。不过这些目前不是很重要, 不了解也罢。
不过要注意,Deployment 并不是一份死板的资源清单,而是对资源动态的保证,例如一个 节点 node 宕机了, Deployment 的控制器只要发现定义数量不满足了,就要在存活节点上重新发起新的需求,以保证满足设定。
CRD
既然搞清楚了 Deployment 的用途,那 CRD 也就很容易说明了。Deployment 是一种资源定义清单, 定义了容器的副本数和数量。而CRD就是用户自己去自定义某些资源清单。
那这样就会有人问了,「我没事自己定义个资源清单干嘛? 」。这个问题问的好,昨天 yann 也说过,光是资源的定义没有任何意义,要配合控制器 controller 来处理这个清单。所以我们自定义了一个清单。自然也要自定义控制器去处理这个清单。而在控制器中,可以附加各种业务逻辑来完成相应的控制,这就是我们的目的。
搞清楚了前因后果,我们再来看一下 CRD 项目的具体的操作方式。昨天 yann 演示了一个 CRD 的一个配置文件,其实这只是第1步。后续还要补充另外一些文件,来构造成一个特定的工程。
然后我们会安装代码生成器,代码生成器检测工程文件的结构,来生成一些通用代码和组件,在此基础上我们再来编写控制器。最后把编译好的二进制控制器文件放到 k8s 服务器上, 再加上权限,指定配置文件就可以使用了。
为什么会这样操作?是因为k8s的处理过程还是相对复杂,厂商为了方便用户会自动生成一部分代码,只有部分比较灵活的代码由用户来完成。
第4部分 工程
官方示例
我们先看一下官方的实例程序。
网址如下:
CRD的官方的示例程序还是很规整的,中规中矩。如果学习Go语言的话,用类似的示例做出发点还是很好的,比选择个人一时兴趣创建的工程或者某个网页上随便找的Hello World要好很多。
很多同学不喜欢看官方的示例程序,认为又臭又长,内容复杂,版本还老。喜欢个人博客里面简化介绍,一条线结束,操作几下结果就出来了。平心而论,yann 也不喜欢看。但是为了认知的准确性,会勉强自己看。
个人项目
从上面的截图来看, 官方示例还是略微复杂了一点。我们从零开始创建一个更简化的项目。
还是先创建一个 crd.yaml,注意内容和文件路径,有些值等会用到。
位置:github.com/kubernetes/yann-controller/artifacts/examples/crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1kind: CustomResourceDefinitionmetadata:name: tests.yanncontroller.k8s.iospec:group: yanncontroller.k8s.ioversion: v1alpha1names: kind: Test plural: testsscope: Namespaced
在第一篇中我们说到的 apiVersion、kind、 metadata、spec 等几个固定的字段全员出现了。类似的情况在 k8s 中大量存在,所以我们只关注有变化部分就好了。
这次要关注的是 tests 和 yanncontroller ,分别为类型名的复数和分组名。在 metadata 里面可以看到。
另外注意,定义成类型的 Test,首字母T是大写的。
工程文件
最后 yann 带大家补全代码生成的全部文件。在本篇中,yann只会给大家展示基本的文件结构,代码具体内容和每个文件的用途,会在后面的篇章中逐渐说明。
我们只要看 pkg 目录下的文件就好。该目录包含2个子目录 apis、signals ,这两个目录是固定不变的,分别代表了 api 和信号量 相关的文件。
而 apis 下面的目录及子目录是自己定义的,分别对应 crd.yaml 配置文件中的分组名前缀 yanncontroller 和spec 定义的版本号 v1alpha1。
除了这2个目录外,其他文件的名字都是固定不变的。这样分析下来,应该好理解很多。
第 5 部分 控制器
yanncontroller
我们再把昨天的文件结构图,贴一下。
第一个文件是 yanncontroller 下面的 register.go ,请和截图比对。yanncontroller 是 yann 定义的 分组名,昨天文章有提到。
另外需要注意,下层子目前也有一个同名的文件,请不要弄混。
vi register.go
package yanncontroller
// 定义包名
const (
GroupName = "yanncontroller.k8s.io"
)
// 定义常量 GroupName
这个文件很简单, 让我们继续深入。
v1alpha1
在同一层目录会有个 v1alpha1 , 这个目录也是我们定义的,表示 api 版本。让我们再往下面看。
- doc.go
- register.go
- types.go
这三个文件也是固定出现的。但是,我说文件固定出现是指文件名不变, 在不同项目下,里面内容还是会变化的。
我们再来看 doc.go 文件
// +k8s:deepcopy-gen=package
// +groupName=yanncontroller.k8s.io
package v1alpha1
// 定义包名
注意,这个文件的特别之处是注释内容也是有用的,下同。所以 yann 把解释写在了代码框外面。
开头的两行注释,代码生成工具会用到。一个是告知deepcopy-gen生成deepcopy文件,另一个是声明了分组名,也就是前一个文件里的常量值。
register.go 文件的内容会稍微多一点, 我们要缩减了。
register.go
这个图真的是勉强截下来。不过没关系,我们在意的是结构,而不是内容。
选展开看一下导入文件
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
yanncontroller "github.com/kubernetes/yann-controller/pkg/apis/yanncontroller"
)
正常导入没什么好说的, metav1 属于别名。另外,导入的路径是相对 src 目录来描述的,如果不清楚自己的 src 目录位置,可以使用以下命令确认一下。
go env |grep PATH
# GOPATH="/root/go"
ls /root/go
# bin pkg src
如果,导入包成为困扰你的问题,也可以下载下来,构造成导入的路径放在 /root/go/src下,对于 yann 的环境。
同理, 对于自己项目的定义和官方示例就会有点不同,官方是 src 下 k8s.io 的文件夹,而 yann 的在 github.com 文件夹,所以调整修改了。
ls /root/go/src/
# github.com k8s.io
搞清楚,导入包的问题后,register.go 的其他内容可以稍微放一下, 毕竟我们不是专职讲 go 开发。
这边,稍微梳理一下:
- var SchemeGroupVersion
- func Kind(kind string) schema.GroupKind {}
- func Resource(resource string) schema.GroupResource {}
- var SchemeBuilder
- var AddToScheme
- func addKnownTypes(scheme *runtime.Scheme) error {}
其中 1、4、5 都是变量,其他是函数。
1号变量需要修改为自己的分组名。
6号函数是通过addKnownTypes方法使得client可以知道 Test 类型的API对象。
这一部分不理解也没有关系, 有个映象就可以了,修改的是1号和6号。
这个目录最后一个文件是 types.go
同样存在不可忽视的注释,除去注释以外, 内容还是挺简单的,就是定义了4个结构体而已。不理解结构体的同学, 可以想成 python 里的类 class,关注类名和类属性。以上4个结构体分别定义了自身结构、期望值、状态和list列表结构。
这个文件也没什么好操作的, 批量替换就好了, 具体看我的工程和官方示例的对比,把 FooXX 都改成了 TestXX 。
第 6 部分 依赖
依赖
在生成代码之前,我们要解决依赖问题。当然官方示例是自带构建好的代码的,这一部可以省去。但我们自己创建的工程是不能省的。
按照官方的说法,没有使用 go 1.11 模块的,要用下面的方式解决依赖
go get -d k8s.io/sample-controller
cd $GOPATH/src/k8s.io/sample-controller
godep restore
而使用 go 1.11 模块的,可以自动解决依赖问题
GO111MODULE=on
git clone https://github.com/kubernetes/sample-controller.git
cd sample-controller # ...
yann 使用了 go 1.11 模块,理论上没有什么问题。但是还是太天真,代码生成失败。
那我们关了以上模块, 用前一种 godep方法可以么?
还是被击败了。
这个地方花了 yann 一天时间,根据提示一点点排查。有报包缺失的,有报冗余函数的,还有报网络故障的。当然和这台机器是全新安装也有关系。
总之,综上所述,yann 觉得新手入门阶段, 解决依赖是最困难的。接受讨论,欢迎留言。
在这里,交互验证的优势显示出来了,如果没有官方示例项目,yann 就无法确认环境是否正常。
环境
生成代码的时候,提示当前 go 的版本不支持,正好演示一下 go 的环境部署。相对依赖问题而言,真的是简单太多了。
GO_VERSION=go1.12.14.linux-amd64
wget https://dl.google.com/go/$GO_VERSION.tar.gz
sudo tar -C /usr/local -xzf $GO_VERSION.tar.gz
# 配置环境变量
vi ~/.bashrc
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
source !$
附加操作
mkdir -p $HOME/go
cd !$
mkdir bin pkg src
export GO111MODULE=on
export GOPROXY=https://goproxy.cn
go version
# go version go1.12.14 linux/amd64
安装成功
成果
更新了版本后,代码正常生成, yann 给大家演示一下,命令如下
ROOT_PACKAGE="github.com/kubernetes/yann-controller"
CUSTOM_RESOURCE_NAME="yanncontroller"
CUSTOM_RESOURCE_VERSION="v1alpha1"
cd $GOPATH/src/k8s.io/code-generator
./generate-groups.sh all "$ROOT_PACKAGE/pkg/client" "$ROOT_PACKAGE/pkg/apis" "$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"
生成的文件
第 7 部分 生成文件
生成的文件
下图为我们昨天使用代码生成器生成的文件,可以稍微看一下
如图所示, 在我们的版本目录下多了一个文件,而 pkg 下面新增的 client 目录里面多了 3 个 子目录
clientset、informers、listers 。具体内容,我们涉及到的时候会进行描述,现在只要注意 生成文件的名字是和分组名及版本有关就好了。
导入梳理
观察一下官方示例你就会发现一个有趣的现象,异常多的导入。所以我们先从导入包开始分析。
别名 | 包名 | 组件名 |
---|---|---|
fmt | ||
time | ||
appsv1 | api/ | apps/v1 |
corev1 | api/ | core/v1 |
apimachinery/ | pkg/api/errors | |
metav1 | apimachinery/ | pkg/apis/meta/v1 |
utilruntime | apimachinery/ | pkg/util/runtime |
apimachinery/ | pkg/util/wait | |
appsinformers | client-go/ | informers/apps/v1 |
client-go/ | kubernetes | |
client-go/ | kubernetes/scheme | |
typedcorev1 | client-go/ | kubernetes/typed/core/v1 |
appslisters | client-go/ | listers/apps/v1 |
client-go/ | tools/cache | |
client-go/ | tools/record | |
client-go/ | util/workqueue | |
klog |
因为阅读效果的原因,我们做了缩减,自己导入时请参考源码。
别名 | 包名 | 组件名 |
---|---|---|
yannv1alpha1 | yann-controller/ | v1alpha1 |
clientset | yann-controller/ | clientset/versioned |
yannscheme | yann-controller/ | clientset/versioned/scheme |
informers | yann-controller/ | informers/externalversions/xx |
listers | yann-controller/ | listers/samplecontroller/v1alpha1 |
同样是导入内容,以上5条都是导入生成出来的代码。包名是自己取的,不必和我相同,同时别名也可以自己定义。
代码分析
我们先不进行细节分析,从整体上看一下代码分为哪几部分。我们要做的是一个控制器, 需要提供方法用来调用。所以声明一个结构体,并绑定方法。
方法 | 用途 |
---|---|
Controller | 声明的结构体 |
NewController | 初始化结构体 |
Run | Run 方法 |
runWorker | 长期运行调用其他函数 |
processNextWorkItem | 读 workqueue 里的项目 |
syncHandler | 状态比较发起更新 |
updateTestStatus | 状态修改 |
enqueueTest | 获取资源,转换为名称,放入队列 |
handleObject | 获取实现metav1.Object,查找test |
newDeployment | 调用test的name字段做了个新部署 |
第 8 部分 流程
工作流程
我们先贴一张 crd 的官方提供的工作流程:
鬼子画的图还是那么难懂,反正跟着数字看就好了。大意是 API 对象的变化会通过 Informer 存入 WorkQueue 队列。在Controller中会消费队列中的数据从而做出响应。
main文件
光看图上来说还是不容易理解,让我们实际操作一下吧。观察一下main文件。
变量 | 命令 |
---|---|
stopCh | signals.SetupSignalHandler() |
cfg | clientcmd.BuildConfigFromFlags(masterURL, kubeconfig) |
kubeClient | kubernetes.NewForConfig(cfg) |
exampleClient | clientset.NewForConfig(cfg) |
kubeInformerFactory | kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30) |
exampleInformerFactory | informers.NewSharedInformerFactory(exampleClient, time.Second*30) |
controller | NewController(kubeClient, exampleClient,…) |
kubeInformerFactory.Start(stopCh) | |
exampleInformerFactory.Start(stopCh) | |
controller.Run(2, stopCh) | |
init() {} | |
这个文件还是相对简单的,因为是守护进程,先处理下信号量。然后处理入参,定义 kubernetes 和 example 的 clientset 集。
之所以需要这些是用来构建 controller的 ,官方示例会比网上常见的示例多一个参数,给 test 程序使用。这也是官方示例的优点之一。
构建好 controller 后会启动 informer ,同样因为 test 的需要多启了一个。然后 controller 调用 Run 方法去处理信息。以上就是 main 文件的主要内容。 有余力的话,建议您把 test 相关的内容研究一下,对代码的规范化很有用途。
然后到 controller.go 文件下, 定义了结构体, 初始化结构体。
Run 方法会从 informer 同步缓存并启用 workers ,然后 runWorker 是一个长期运行的函数, 唯一用途就是调processNextWorkItem 方法 。
processNextWorkItem 从 workqueue 依次读项目,并调用 syncHandler 处理。
syncHandler 正是处理各种逻辑的函数,其他都是辅助它的, 缓存、队列操作,调用 test 操作,使用cdr信息构建一个 deployment 等。
成果展示
最后,老习惯, 还是实验成果的展示
如图, 修改自官方示例,建立了 crd 和 其对应的对象。通过查看 deplomyment 和 pods 可以确认代码运行到了最底部函数,实验成功。
另外说明一下,本次实验遇到了2个坑,一个是编译出的代码不能直接用, 一个是日志组件出现问题,我是 go 是12 版本的, 如果有人遇到类似情况可以交流下。
scheme.Codecs.WithoutConversion undefined (type serializer.CodecFactory has no field or method WithoutConversion)
panic: ./yann-controller flag redefined: log_dir
第 9 部分 自动生成
工具
一路跟下来的同学可能要说了,「你分享的这个东西太麻烦了,一个示例都这么复杂,感觉完全没有可用性。」笑,9012年了,难道真的还有人用原始的方式开发么? 前几天是为了让大家熟悉操作,现实场景中,我们是有工具的。下面就来分享 Kubebuilder。
先安装一下
curl -LO https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.0.0-beta.0/kubebuilder_2.0.0-beta.0_linux_amd64.tar.gz
tar xvf kubebuilder_2.0.0-beta.0_linux_amd64.tar.gz
sudo mv kubebuilder_2.0.0-beta.0_linux_amd64 /usr/local/kubebuilder
cat <<EOF >> ~/.bashrc
export PATH=$PATH:/usr/local/kubebuilder/bin
EOF
还有 kustomize
curl -LO https://github.com/kubernetes-sigs/kustomize/releases/download/v3.1.0/kustomize_3.1.0_linux_amd64
chmod +x kustomize_3.1.0_linux_amd64
sudo mv kustomize_3.1.0_linux_amd64 /usr/local/bin/kustomize
项目
工具安装好后, 我们再把之前示例的项目做一下。
初始化
在 GOPATH 的外边,创建一个项目
mkdir yann-controller2 && cd yann-controller2
go mod init github.com/yann/yann-controller2
生成项目代码
source ~/.bashrc
kubebuilder init --domain yann.com
创建 CRD
接下来的操作就似曾相识了
kubebuilder create api --group yanncontroller --version v1alpha2 --namespaced false --kind Test2
然后修改 types 文件 和 controller 文件
vi api/v1alpha2/test2_types.go
vi controllers/test2_controller.go
按照前几篇的内容,配置好这2个文件后,就可以启动项目了
启动项目
make install
make run
注意: Reconcile结构体聚合了Client接口, 所以 controller 的写法会有点变化, 但这是后篇的内容了。
第10 部分 总结
把复杂、困难的东西,讲的简洁明了, 确实挺困难的。yann 尽量放慢速度,并辅以图表和视频。尽量展现出其中的原理。学习不能贪图爽快,一定要搞清楚原理。不然很容易陷入一个脚本跑起来, 各种问题都奇怪的状况。crd 确实是个热点,哪怕现在用不到,也建议了解 一二。
本文由博客一文多发平台 OpenWrite 发布!
发布在平台的文章, 和原文存在格式差异, 阅读不便请见谅