k8s 中 CRD controller 开发教程
By admin
- 8 minutes read - 1508 words本文主要介绍 crd controller
的基本开发过程,让每一个刚接触k8s开发的同学都可以轻松开发自己的控制器。
kubebuilder 简介
kubebuilder
是一个帮助开发者快速开发 kubernetes API
的脚手架命令行工具,其依赖 controller-tools
和 controller-runtime
两个库。其中 controller-runtime
简化 kubernetes controller
的开发,并且对 kubernetes
的几个常用库进行了二次封装, 以简化开发工程。而 controller-tool
主要功能是代码生成。
下图是使用 kubebuilder
的工作流程图:
安装 kubebuilder
# download kubebuilder and install locally.
➜ curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
➜ chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
确认安装成功
➜ kubebuilder version
Version: main.version{KubeBuilderVersion:"3.11.1", KubernetesVendor:"1.27.1", GitCommit:"1dc8ed95f7cc55fef3151f749d3d541bec3423c9", BuildDate:"2023-07-03T13:10:56Z", GoOs:"darwin", GoArch:"amd64"}
相关命令
➜ kubebuilder --help
CLI tool for building Kubernetes extensions and tools.
Usage:
kubebuilder [flags]
kubebuilder [command]
Examples:
The first step is to initialize your project:
kubebuilder init [--plugins=<PLUGIN KEYS> [--project-version=<PROJECT VERSION>]]
<PLUGIN KEYS> is a comma-separated list of plugin keys from the following table
and <PROJECT VERSION> a supported project version for these plugins.
Plugin keys | Supported project versions
-----------------------------------------+----------------------------
base.go.kubebuilder.io/v3 | 3
base.go.kubebuilder.io/v4 | 3
declarative.go.kubebuilder.io/v1 | 2, 3
deploy-image.go.kubebuilder.io/v1-alpha | 3
go.kubebuilder.io/v2 | 2, 3
go.kubebuilder.io/v3 | 3
go.kubebuilder.io/v4 | 3
grafana.kubebuilder.io/v1-alpha | 3
kustomize.common.kubebuilder.io/v1 | 3
kustomize.common.kubebuilder.io/v2 | 3
For more specific help for the init command of a certain plugins and project version
configuration please run:
kubebuilder init --help --plugins=<PLUGIN KEYS> [--project-version=<PROJECT VERSION>]
Default plugin keys: "go.kubebuilder.io/v4"
Default project version: "3"
Available Commands:
alpha Alpha-stage subcommands
completion Load completions for the specified shell
create Scaffold a Kubernetes API or webhook
edit Update the project configuration
help Help about any command
init Initialize a new project
version Print the kubebuilder version
Flags:
-h, --help help for kubebuilder
--plugins strings plugin keys to be used for this subcommand execution
--project-version string project version (default "3")
Use "kubebuilder [command] --help" for more information about a command.
初始化项目使用 init
子命令,创建脚手架使用 create
子命令。
下面我们开始正式开始一个项目的开发流程
CRD controller 开发
这里我们创建一个控制器项目
➜ mkdir demo
➜ cd demo
➜ kubebuilder init --domain example.io --repo github.com/cfanbo/sample --owner "sxf"
此时打印内容为
Writing kustomize manifests for you to edit…
Writing scaffold for you to edit…
Get controller runtime:
$ go get sigs.k8s.io/[email protected]
Update dependencies:
$ go mod tidy
Next: define a resource with:
$ kubebuilder create api
常用的几个参数:
domain
表示当前组的域(默认为 my.domain
)
repo
表示当前Go项目模块名
owner
表示 copyright 所有者
license
协议,必须为 apache2
、none
其中的一个,不指定则默认为 apache2
project-name
项目名称,默认以当前目录为项目名称
project-version
项目版本号,默认为 3
此时项目新的结构为
go.mod
: 我们的项目的 Go mod 配置文件,记录依赖库信息,对应的是 --repo
参数。这里指定模块名为 github.com/cfanbo/sample
Makefile
: 用于控制器构建和部署的 Makefile
文件
PROJECT
: 用于生成组件的 Kubebuilder
元数据
cmd/main.go
是整个项目入口文件
目录 config
表示项目启动配置,包含在集群上启动控制器所需的 [Kustomize](https://sigs.k8s.io/kustomize)
YAML 定义,但一旦我们开始编写控制器,它还将包含我们的 CustomResourceDefinitions(CRD)
、RBAC
配置和 WebhookConfigurations
。
config/default
在标准配置中包含 Kustomize base
,它用于启动控制器。
其他每个目录都包含一个不同的配置,重构为自己的基础。
config/manager
: 在集群中以 pod 的形式启动控制器config/rbac
: 在自己的账户下运行控制器所需的权限config/promethus
:定义 prometheus 服务
对于命令用法可参考 kubebuilder init -h 查看
项目基本结构介绍见: https://book.kubebuilder.io/cronjob-tutorial/basic-project
kubebuilder create
命令可以用来创建 api
和 webhook
脚手架,这里我们创建一个 api
➜ kubebuilder create api --group tech --version v1 --kind SuperService
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v1/superservice_types.go
api/v1/groupversion_info.go
internal/controller/suite_test.go
internal/controller/superservice_controller.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
mkdir -p /Users/sxf/workspace/demo/bin
test -s /Users/sxf/workspace/demo/bin/controller-gen && /Users/sxf/workspace/demo/bin/controller-gen --version | grep -q v0.12.0 ||
GOBIN=/Users/sxf/workspace/demo/bin go install sigs.k8s.io/controller-tools/cmd/[email protected]
/Users/sxf/workspace/demo/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
这里指定 group
、version
和 kind
三个参数,对应的正是k8s中的 gvk
这个概念。生成的CRD apiVersion
格式为 [group].[domain]/[version]
。
现在我们再看一下目录结构,都有哪些变化
这时会发现多出一些 bin
、api
、config/samples
和 internal
等目录。
- 其中
bin
目录存放的是一个[controller-gen](https://github.com/kubernetes-sigs/controller-tools)
二进制,它是一个常用代码生成工具, 主要用来生成 CRD 和 controller 相关的代码。如生成 CRD 定义、生成RBAC规则,策略值定义,控制器框架 或 生成 marker 接口等等 api
目录主要存放的是我们开发的自定义资源结构定义(CRD
)config/samples
是声明CR
的示例internal/controller
是实现控制器(controller
)逻辑的地方,一般只需要在Reconcile()
函数里实现控制逻辑
实现功能
下面我们开发一个自定义控制器,要实现部署一个webserver 服务到k8s集群中时,自动使用指定的镜像通过 deployment 来实现服务部署,同时通过 service
将此服务向外暴露,其中服务类型为 NodePort
方式,yaml格式为
apiVersion: tech.example.io/v1
kind: SuperService
metadata:
name: nginx-app
spec:
size: 2
image: nginx:1.23-alpine
envs:
- name: yourname
value: sxf
ports:
- port: 80
targetPort: 80
nodePort: 30002
这里的 apiVersion
字段格式为 [group].[domain]/[version]
实现原理:
- 首先根据这个指定的镜像一副本个数,动态创建一个deployment 对象
- 然后再创建一个 Service 对象,并指定endpoint为刚刚通过 deployment 创建的Pod
好了,现在让我们正式开始实现这个控制器吧。
CRD资源结构体定义
首先我们需要对kubebuilder 工具自动生成的资源数据结构进行一些调整,文件为 api/v1beta1/frigate_types.go
,在本示例中只需要对 SuperServiceSpec
和 SuperServiceStatus
两个重要结构体添加一些字段即可。
// SuperServiceSpec defines the desired state of SuperService
type SuperServiceSpec 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 SuperService. Edit superservice_types.go to remove/update
//Foo string `json:"foo,omitempty"`
Size *int32 `json:"size"`
Image string `json:"image"`
Envs []corev1.EnvVar `json:"envs,omitempty"`
Ports []corev1.ServicePort `json:"ports,omitempty"`
}
// SuperServiceStatus defines the observed state of SuperService
type SuperServiceStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
appsv1.DeploymentStatus `json:",inline"`
Count int32 `json:"count"`
}
数据结构 SuperServiceSpec
表示资源期望的状态
这里 Size
表示 Pod
的副本个数,对应的是 deployment
中的 Deployment.Spec.Replicas
字段;
Image
对应的是 Container.Image
字段;
Envs
对应的是 Pod 中的 Envs
字段 Container.Envs
字段;
Ports
对应的是 Service.Spec.Ports
与 Container.Ports
字段
而数据结构 SuperServiceStatus
则表示资源实际的状态,但在本文示例中只用它来存储资源变更的次数,其内嵌的数据结构我们暂时未使用。
CRD控制器实现
实现逻辑:
- 当收到一个
SuperService
资源变更时,则通过APIServer
查找其对应的deployment
对象。如果找不到则直接创建一个deployment
对象(对于pod
我们不用管它,其由内置的deployment
控制器进行管理) - 然后再通过 APIServer 查找 Service 对象,处理逻辑同上
- 然后对比前后两次
SuperService
实例的实际状态
与期望状态
是否一致,如果不一致,则进行SuperService
同步并保存。这里期望状态使用instance.Spec
表示,而实际状态用instance.Annotations["spec"]
来表示,只需要判断是否一致即可(不推荐这样使用,建议还是使用Spec
和Status
两个字段作对比,或根据不同需求实际状态通过查询APIServer 获取也可以)。
在文件 internal/controller/superservice_controller.go
里的调谐函数 Reconcile
添加我们自己的逻辑。
一、 RABC 设置
由于我们的控制器需要操作 deployment
和 service
对象,因此需要首先设置 RBAC
权限
//+kubebuilder:rbac:groups=tech.example.io,resources=superservices,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=tech.example.io,resources=superservices/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=tech.example.io,resources=superservices/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;update;patch;get;list;watch
//+kubebuilder:rbac:groups=*,resources=services,verbs=create;update;patch;get;list;watch
最后面两行是我们自己添加的 RBAC
权限配置。
这几行注解会在执行 make manifests
命令时生成相应的配置文件(/config/rabc/rule.yaml
) 内容,用来声明控制器权限。
二、控制器逻辑
对于控制器实现代码比较多,这里不再贴出,给出的代码里有相应的注释信息,参考: https://github.com/cfanbo/k8s-crd-demo/blob/main/internal/controller/superservice_controller.go
生成CRD清单
生成 CRD 清单
➜ make manifests
内部调用 bin/controller-gen
命令动态生成一些控制器配置信息(会自动从 /config/default/
读取全局配置,然后修改一些固定位置的配置,最终生成yaml文件),如rabc之类的yaml文件, 见 ./config/
目录内容。
安装CRD
在本地集群测试 controller 前,我们需要先安装CRD。
➜ make install
/Users/sxf/workspace/demo/bin/controller-gen rbac:roleName=manager-role crd webhook paths=”./…” output:crd:artifacts:config=config/crd/bases
test -s /Users/sxf/workspace/demo/bin/kustomize || GOBIN=/Users/sxf/workspace/demo/bin GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/[email protected]
/Users/sxf/workspace/demo/bin/kustomize build config/crd | kubectl apply -f –
customresourcedefinition.apiextensions.k8s.io/superservices.tech.example.io created
从输出结果中可以看到CRD安装成功,可以在k8s集群中查看注册的crd资源以及相关配置,如 RBAC。
➜ kubectl api-resources --api-group=tech.example.io
NAME SHORTNAMES APIVERSION NAMESPACED KIND
superservices tech.example.io/v1 true SuperService
也可以使用查看命令
➜ kubectl get crd superservices.tech.example.io
如果要从集群卸载CRD的话,可执行命令
➜ make uninstall
运行控制器
这时我们新开一个终端并执行以下命令在本地开发环境运行控制器服务,注意观察控制器打印日志(不需要手动对 cmd/main.go
文件做任何修改,kubebuilder
工具已做好了一切准备工作)。
➜ make run
此时生成代码并运行,自动读取 ~/.kube/config
配置文件
test -s /Users/sxf/workspace/demo/bin/controller-gen && /Users/sxf/workspace/demo/bin/controller-gen –version | grep -q v0.12.0 ||
GOBIN=/Users/sxf/workspace/demo/bin go install sigs.k8s.io/controller-tools/cmd/[email protected]
/Users/sxf/workspace/demo/bin/controller-gen rbac:roleName=manager-role crd webhook paths=”./…” output:crd:artifacts:config=config/crd/bases
/Users/sxf/workspace/demo/bin/controller-gen object:headerFile=”hack/boilerplate.go.txt” paths=”./…”
go fmt ./…
go vet ./…
go run ./cmd/main.go
2023-08-19T15:16:53+08:00 INFO controller-runtime.metrics Metrics server is starting to listen {“addr”: “:8080”}
2023-08-19T15:16:53+08:00 INFO setup starting manager
2023-08-19T15:16:53+08:00 INFO starting server {“path”: “/metrics”, “kind”: “metrics”, “addr”: “[::]:8080”}
2023-08-19T15:16:53+08:00 INFO Starting server {“kind”: “health probe”, “addr”: “[::]:8081”}
2023-08-19T15:16:53+08:00 INFO Starting EventSource {“controller”: “superservice”, “controllerGroup”: “tech.example.io”, “controllerKind”: “SuperService”, “source”: “kind source: *v1.SuperService”}
2023-08-19T15:16:53+08:00 INFO Starting Controller {“controller”: “superservice”, “controllerGroup”: “tech.example.io”, “controllerKind”: “SuperService”}
2023-08-19T15:16:53+08:00 INFO Starting workers {“controller”: “superservice”, “controllerGroup”: “tech.example.io”, “controllerKind”: “SuperService”, “worker count”: 1}
从打印日志中可以看到控制器服务启动成功,这里指定的线程个数 worker count
为 1 个。
另外也可以手动执行
go run ./cmd/main.go
命令,这样就可以指定一些配置参数。
测试控制器
控制器服务已经运行成功,现在我们创建一个CR
资源, 文件为 test.yaml
apiVersion: tech.example.io/v1
kind: SuperService
metadata:
name: nginx-app
spec:
size: 2
image: nginx:1.23-alpine
envs:
- name: yourname
value: sxf
- name: sex
value: man
ports:
- port: 80
targetPort: 80
nodePort: 30002
➜ kubectl apply -f test.yaml
superservice.tech.example.io/nginx-app created
此时在刚才开启的终端里可以会看到一些控制器输出日志。
现在我们查看一下控制器生成的一些资源对象来确认控制器服务是否正常。
➜ kubectl get superservices
NAME AGE
nginx-app 38s
➜ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nginx-app 2/2 2 2 61s
➜ kubectl get pod -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
default nginx-app-586589cfbb-9zkhw 1/1 Running 0 97s 10.244.1.52 kind-worker <none> <none>
default nginx-app-586589cfbb-jgshg 1/1 Running 0 97s 10.244.1.51 kind-worker <none> <none>
➜ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-app NodePort 10.96.57.24 <none> 80:30002/TCP 2m16s
可以看到与这个crd
相关的资源,如 Deployment
、Pod
和 Service
对象全部创建成功,我们再确认查看一下 service
与 pod
的关系
➜ kubectl get ep nginx-app
NAME ENDPOINTS AGE
nginx-app 10.244.1.51:80,10.244.1.52:80 2m28s
通过 ENDPOINTS
字段可知它们之间的映射关系是ok的,到此说明我们控制器运行成功了。
这里顺便抛出一个小问题。那就是如果现在删除一个pod 会是什么情况呢?我们这里测试一下
➜ kubectl delete pod nginx-app-586589cfbb-9zkhw
pod "nginx-app-586589cfbb-9zkhw" deleted
➜ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-app-586589cfbb-jgshg 1/1 Running 0 3m19s 10.244.1.51 kind-worker <none> <none>
nginx-app-586589cfbb-pvsjq 1/1 Running 0 15s 10.244.1.53 kind-worker <none> <none>
➜ kubectl get ep nginx-app
NAME ENDPOINTS AGE
nginx-app 10.244.1.51:80,10.244.1.53:80 3m59s
可以看到原来的 Pod 被删除后,内置的 Deployment
将立即创建一个新的 Pod 出来,同时并更新 Service 的 Endpoints 映射关系,而这一切都是由内置控制器来完成的,不需要我们参与,是不是很方便呢。
因此对于开发者来讲,只需要关心上层操作逻辑即可,灵活用好这些内置的资源对象就可以了。
如果手动删除了 Deployment 或 Service 的话,控制器是无法感知到这个操作的,这是因为我们控制器 watch 的是 SuperService 对象,因此被删除的对象无法自动创建。除非对 CR 资源进行了变更,以便CRD控制器进行资源对象重建。
我们试着多次修复副本数量SuperServiceSpec.Size
字段后,再次查看 CR 资源信息
➜ kubectl desc superservices nginx-app
Name: nginx-app
Namespace: default
Labels: <none>
Annotations: spec:
{"size":4,"image":"nginx:1.23-alpine","envs":[{"name":"yourname","value":"sxf"},{"name":"sex","value":"man"}],"ports":[{"protocol":"TCP","...
API Version: tech.example.io/v1
Kind: SuperService
Metadata:
Creation Timestamp: 2023-08-19T11:13:43Z
Generation: 8
Resource Version: 554662
UID: 03acb3fd-53f4-4960-9725-df3ed09c8dc2
Spec:
Envs:
Name: yourname
Value: sxf
Name: sex
Value: man
Image: nginx:1.23-alpine
Ports:
Node Port: 30002
Port: 80
Protocol: TCP
Target Port: 80
Size: 4
Status:
Count: 2
Events: <none>
可以看到 Status.Count:2
表示 superservices
资源发生了了2
次变更。
好了,到现在测试已完成,我们进行一些清理工作,将这个 cd 资源删除
➜ kubectl delete superservices nginx-app
superservice.tech.example.io "nginx-app" deleted
也可以直接调用
➜ kubectl delete -f test.yaml
superservice.tech.example.io "nginx-app" deleted
再次查询会发现 CR
及其对应的 Deployment
、Pod
和 Service
也一同被删除。
CRD镜像制作与测试
下面我们先制作一个控制器镜像,然后在本地再测试这个镜像,在此之前先停止 make run
命令。
镜像制作
自定义控制器开发完毕,为了在生产中部署方便,一般将控制器以镜像的形式发布出去。
➜ make docker-build docker-push IMG=<some-registry>/<project-name>:tag
如果一切正常的话,在本地会生成一个对应的控制器镜像,同时会将镜像上传到远程仓库。
如果想制作多平台镜像,则可以使用 make docker-buildx 命令
镜像测试
我这里使用的 kind 集群,不需要把镜像推送到远程容器仓库。直接加载本地的镜像到 kind 集群节点:
➜ kind load docker-image <some-registry>/<project-name>:tag
现在我们将自定义控制器部署在本地集群(此时会自动读取 .kube/config
配置文件)
➜ make deploy IMG=<some-registry>/<project-name>:tag
这里的 make deploy
命令是调用 KUSTOMIZE
命令对 config/manager/kustomize.yaml
文件进行镜像的替换,并在集群中应用此配置。
最后确认控制器安装是否成功
➜ kubectl get deployment -A
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
demo-system demo-controller-manager 1/1 1 1 6s
可以看到控制器已通过镜像部署在集群成功。
控制器部署所在的 namespace 定义在
config/default/kustomization.yaml
文件,这是一个全局配置文件
查看控制器日志
➜ kubectl logs -f deploy/demo-controller-manager -n demo-system
Found 2 pods, using pod/demo-controller-manager-67c8bfc7d-6lk9f
2023-08-19T08:58:28Z INFO controller-runtime.metrics Metrics server is starting to listen {“addr”: “127.0.0.1:8080”}
2023-08-19T08:58:28Z INFO setup starting manager
2023-08-19T08:58:28Z INFO starting server {“path”: “/metrics”, “kind”: “metrics”, “addr”: “127.0.0.1:8080”}
I0819 08:58:28.394566 1 leaderelection.go:245] attempting to acquire leader lease demo-system/a01d9552.example.io…
2023-08-19T08:58:28Z INFO Starting server {“kind”: “health probe”, “addr”: “[::]:8081”}
重新创建一个CR资源,确认一下对应的 deployment
和 Pod
是否一切正常。
➜ kubectl apply -f test.yaml
重复上在的查询命令,不过这时需要指定名字空间,
➜ kubectl get superservices
➜ kubectl get pods -A
➜ kubectl get svc -A
➜ kubectl get deployment -A
发现直接调度模式下运行控制器与镜像方式运行控制器,有些资源会使用不同的namespace, 待了解。
卸载
一切测试完毕后,我们将注册的 crd 和 控制器从本了集群中卸载
卸载CRD
➜ make uninstall
卸载控制器
➜ make undeploy
总结
- 对开发过程中为了方便排查问题方便,需要关注一下控制器的打印日志
- CRD 支持使用 OpenAPI v3 schema 在
validation
段中进行声明式验证。本文未对声明字段进行验证,如果需要请参考 - 本文未对控制器的 webhook 做介绍,可参考官方文档 https://book.kubebuilder.io/cronjob-tutorial/webhook-implementation 和 https://book.kubebuilder.io/cronjob-tutorial/running-webhook。对于 webhook 本地没有办法完进行充分的调试,会存在一些webbook资源无法在本地集群安装的问题,因此只能通过镜像的方式进行调度(可以自行分析一下目前官方提供的 make deploy 命令)。
- 对于高级用法,如如何使用 Finalizers:https://cloudnative.to/kubebuilder/reference/using-finalizers.html
- 对于单元测试,请参考 https://cloudnative.to/kubebuilder/cronjob-tutorial/writing-tests.html
参考资料
- https://book.kubebuilder.io/quick-start
- https://blog.happyhack.io/2020/10/12/kubernetes-crd-day1/
- https://github.com/kubernetes-sigs/kubebuilder/tree/master/docs/book/src/cronjob-tutorial/testdata/project
- https://book.kubebuilder.io/
- https://github.com/cnych/opdemo