0. 概述
在 Kubernetes CRD 系列:Controller 中,我尝试用原生的 Code-Genarator 结合一个 Sample Controller 项目,演示了如何开发一个 Controller,但是,从那个指导中可以看出,通过这样的方式来开发 Controller 比较原始,需要做的工作比较多,总结一下有:
- 初始化项目结构(从 Sample Controller 中修改)
- 定义 CRD
- 生成代码
- 初始化 Controller
- 编写业务逻辑
其实从这里的步骤来看,有一些是每个 Controller 都会需要的,例如 1,3,4 就可以自动化掉,真正属于一个 Controller 独特的东西,可能就是 CRD 和 业务逻辑了,所以,为了实现这个目标,社区和官方都在不断得努力,于是就有了两个不错的工具选择:
这两个工具的使用难易度相当,其中 Operator Framework 是 CoreOS 公司开发和维护的,而 KubeBuilder 是 Kubernetes SIG 搞的,对于我来说,我更倾向于 Operator Framework,因为它是 CoreOS 公司维护的,所以具有更强的时效性,同时,因为 CoreOS 在 Kubernetes 领域积极的引领作用,所以多于 Controller 的支持会更强;相比较之下,因为 Kube Builder 是 SIG 小组来维护的,在迭代上没有公司来得即时和稳定,所以,在本文中,我将以 Operator Framework 为示例来介绍一下如何使用工具更简单得开发 Controller。
本着主要目的为介绍 Operator Framework 这个工具,所以我这里也不准备讲复杂的案例了,还是保持前几篇中的 Controller 的功能,在监听到资源的新增/修改和删除的时候分别打印一个日志。
1. 环境准备
要开始这个 Controller 之前,得做一些准备工作要做,第一个得有一个 K8S 环境,如果手上没有一个合适的集群的话,可以根据我之前的一篇介绍:CentOS 安装 K3S,快速安装一个 K3S 集群。
下一步就是安装 Operator SDK了,这里我安装目前得最新版本 v0.13.0:
[root@liqiang.io]# export RELEASE_VERSION=v0.13.0
[root@liqiang.io]# curl -LO https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu
[root@liqiang.io]# chmod +x operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu && sudo mkdir -p /usr/local/bin/ && sudo cp operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu /usr/local/bin/operator-sdk && rm operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu
这样,就准备完毕了,下一步就可以开始 Controller 的开发工作了。
2. 创建项目
通过 Operator Framework 可以简单创建出项目结构:
[root@liqiang.io]# operator-sdk new sample-controller --repo github.com/liqiangblogdemos/sample-controller
INFO[0000] Creating new Go operator 'envoy-controller'.
INFO[0000] Created go.mod
INFO[0000] Created tools.go
INFO[0000] Created cmd/manager/main.go
... ...
go: finding golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3
INFO[0275] Project validation successful.
INFO[0275] Project creation complete.
[root@liqiang.io]# cd sample-controller
[root@liqiang.io]# tree -L 2
.
├── build
│ ├── bin
│ └── Dockerfile
├── cmd
│ └── manager
├── deploy
│ ├── operator.yaml
│ ├── role_binding.yaml
│ ├── role.yaml
│ └── service_account.yaml
├── go.mod
├── go.sum
├── pkg
│ ├── apis
│ └── controller
├── tools.go
└── version
└── version.go
然后整体结构就出来了,下一步就是要创建属于我们的 CRD 了。
3. 创建 CRD
创建 CRD 只需要简单的一条命令,然后就可以在 pkg/apis 目录下看到生成的默认 CRD 的 Struct 了:
[root@liqiang.io]# operator-sdk add api --api-version=admin.liqiang.io/v1 --kind=Admin
[root@liqiang.io]# tree
.
├── apis
│ ├── addtoscheme_admin_v1.go
│ ├── admin
│ │ ├── group.go
│ │ └── v1
│ │ ├── admin_types.go
│ │ ├── doc.go
│ │ ├── register.go
│ │ └── zz_generated.deepcopy.go
│ └── apis.go
└── controller
└── controller.go
然后就根据我的业务修改 apis/admin/v1/appservice_types.go 这个文件里面的 CRD:
[root@liqiang.io]# cat apis/admin/v1.admin_types.go
... ...
// AdminSpec defines the desired state of Admin
type AdminSpec struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
}
... ...
其实这里一般情况下就添加一个 Spec 的信息就好了,其他信息都是可以不动的。
4. 添加 Controller
和 CRD 一样,Controller 也是差不多的添加方式:
[root@liqiang.io]# operator-sdk add controller --api-version=admin.liqiang.io/v1 --kind=Admin
INFO[0000] Generating controller version admin.liqiang.io/v1 for kind Admin.
INFO[0000] Created pkg/controller/admin/admin_controller.go
INFO[0000] Created pkg/controller/add_admin.go
INFO[0000] Controller generation complete.
然后就会在 pkg/controller 目录下生成 Controller 的代码框架了,这个时候就需要自己填充一下业务逻辑了,代码的逻辑写在 func (r *ReconcileAdmin) Reconcile(request reconcile.Request) (reconcile.Result, error)
中即可:
[root@liqiang.io]# cat pkg/controller/admin/admin_controller.go
... ...
func (r *ReconcileAdmin) Reconcile(request reconcile.Request) (reconcile.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.Info("Reconciling Admin")
// Fetch the Admin instance
instance := &adminv1.Admin{}
err := r.client.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
log4go.Info("admin: %s have beed deleted", request.Name)
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
log4go.Info("admin: %s created or updated", instance.Spec.Username)
return reconcile.Result{}, nil
}
... ...
这里需要注意的是,这没有使用我们熟悉的 clientset 的形式,而是使用的注册 schema 的形式来获取 CRD,使用方式也很简单:
instance := &adminv1.Admin{}
err := r.client.Get(context.TODO(), request.NamespacedName, instance)
这里的 Controller 逻辑是会在 CRD 状态变化的时候回调这个方法,但是,至于是删除还是新建需要自己来判断,这个很容易,但是,需要注意的是,如果没有其他辅助方式,你很难知道 CRD 是新建还是更新了,但是,CRD 都对应着一个资源的状态,你只需要判断 CRD 的对象和资源的状态是否一致即可,至于是新建和更新,大多数情况都不是那么重要。
5. 运行 Controoler
和之前一样,我们既可以通过打包运行在 K8S 集群的形式来运行 Controller,也可以像本地程序一样直接在开发环境运行,所以这里我还是以简单的本地运行的方式运行示例代码:
[root@liqiang.io]# export WATCH_NAMESPACE=default
[root@liqiang.io]# go run cmd/manager/main.go
{"level":"info","ts":1576399669.5472634,"logger":"cmd","msg":"Operator Version: 0.0.1"}
{"level":"info","ts":1576399669.5473356,"logger":"cmd","msg":"Go Version: go1.13.1"}
{"level":"info","ts":1576399669.5473542,"logger":"cmd","msg":"Go OS/Arch: linux/amd64"}
{"level":"info","ts":1576399669.5473707,"logger":"cmd","msg":"Version of operator-sdk: v0.13.0"}
{"level":"info","ts":1576399669.5510051,"logger":"leader","msg":"Trying to become the leader."}
{"level":"info","ts":1576399669.5511074,"logger":"leader","msg":"Skipping leader election; not running in a cluster."}
... ...
{"level":"info","ts":1576399781.498857,"logger":"controller_admin","msg":"Reconciling Admin","Request.Namespace":"default","Request.Name":"liuliqiang"}
2019/12/15 16:49:41 [INFO]admin: admin created or updated
可以看到,这里成功得感知到 CRD 的变更,并且对日志进行了打印。
6. 小结
本文的所有代码都可以在这个 Repo 中找到。
对比一下本文的内容和上一篇自己一步一步得写 Controller,可以发现通过工具确实方便了很多,甚至于只需要做两件事情:
- 编写 CRD:只需要添加 Spec
- 编写 Controller:只需要填充 Reconcile 函数
其他时间,都是在敲命令行,这样讲整个繁琐的过程简化为了一个结构体的定义以及一个函数的编写,带来了很大的生产力提高,值得推荐。