云原生技术通过方法论、工具集和最佳实践重塑着整个软件技术栈和生命周期,云原生对云计算服务方式与互联网架构进行整体性升级, 深刻改变着整个 IT 领域。云原生的核心是 kubernetes,围绕 kubernetes 构建满足自身需求的 PaaS 平台(应用中心)是绝大数企业的诉求, 但是不同企业自身场景往往存在一定的差异,Operator 是最常见 kubernetes 拓展方式。本片博文,我将会给大家理清 Operator 的来龙去脉, 同时介绍如何通过 kubebuilder 快速开发一个简单的 Operator。

Operator 诞生的背景

kubernetes 无法做到真正意义的开箱即用的,它与传统的 PaaS 平台不同,它仅仅只提供核心的基础设施功能,但是还无法满足用户的最终需求,这里用户主要指业务开发和业务运维, 比如说业务开发需要 CI/CD 工具实现 Devops 的功能,原生 kubernetes 是不提供支持的,但是我们可以通过tekton这一个第三方工具实现 DevOps 相关功能, 这也正是 kubernetes 区别传统PaaS平台的真正强大之处,其提供完善的扩展机制以及基于此而发展出来的海量的第三方工具和丰富的生态。

Operator pattern首先由 CoreOS 提出,通过结合 CRD 和 custom controller 将特定应用的运维知识转换为代码,实现应用运维的自动化和智能化。 Operator 允许 kubernetes 来管理复杂的,有状态的分布式应用程序,并由 kubernetes 对其进行自动化管理,例如,etcd operator 能够创建并管理一组 etcd 集群, 定制化的 controller 组件了解这些资源,知道如何维护这些特定的应用。

随着 kubernetes 的功能越来越复杂,其需要管理的资源在高速增长,对应的 API 和 controller 的数量也愈发无法控制, kubernetes 变得很臃肿,很多不必要的 API 和功能将出现在每次安装的集群中。

为了解决这个问题,CRD 应运而生,CRD 由 TPR(Third Part Resource v1.2 版本引入)演化而来,v1.7 进入 beta,v1.8 进入稳定, 通过 CRD,kubernetes 可以动态的添加并管理资源。CRD 解决了结构化数据存储的问题,Controller 则用来跟踪这些资源, 保证资源的状态满足期望值。CRD+Controller=decalartive API,声明式 API 设计是 kubernetes 重要的设计思想, 该设计保证能够动态扩展 kubernetes API,这种模式也正是 Operator pattern。

kubernetes 本身也在通过 CRD 添加新功能,我们有什么理由不使用呢?

使用场景总结及举例

CRD+custom controller 已经被广泛地使用,按使用场景可划分为以下两种:

  1. 通用型 controller: 这种场景和 kubernetes 内置的apps controller类似,主要解决特定通用类型应用的管理
  2. Operator: 该场景用于解决一个特定应用的自动化管理

通用型 controller 往往是 kubernetes 平台侧用户,如各大云厂商和 kubernetes 服务提供商,Operator 则是各种软件服务提供商, 他们设计时面向单一应用,很多开源的应用的 operator 可以在 operator hub 中获取。我列举一些示例供大家参考:

通用型 Controller

  1. 阿里的 cafedeploymentcontroller 解决金融场景下分布式应用特殊需求。
  2. oam-kubernetes-runtime 实现了 Application Model (OAM),以系统可持续的方式拓展 kubernetes

Operator

  1. etcd operator
  2. Prometheus operator

通用型Controller与kubernetes自带的几个controller类似,旨在解决一些通用的应用模型,而Operator则更加面向单个特定应用, 这两者没有本质的区别。

如何开发 CRD

作为kubernetes开发者,如何开发 CRD+Custom cntroller 呢?其实官方提供示例项目sample-controller供开发者参考,开发流程大致有以下几个过程:

  1. 初始化项目结构(可根据 sample controller 修改)
  2. 定义 CRD
  3. 生成代码
  4. 初始化 controller
  5. 实现 controller 具体逻辑

其中步骤 2,5 是核心业务逻辑,其余步骤完全可以通过自动生成的方式省略,到目前,社区有两个成熟的脚手架工具用于简化开发,一个是有 kube-sig 维护的 kubebuilder, 另一个是由 redhat 维护的 operator-sdk,这两个工具都是基于 controller-runtime 项目而实现,用户可自行选择,笔者用的是 kubebuilder。 使用 kubebuilder 能够帮助我们节省以下工作:

如果你想要快速构建 CRD 和 Custom controller,脚手架工具是个不错的选择,如果是学习目的,建议结合 sample-controller 和 kubernetes controller 相关 源码。

kubebuilder 详解

kubebuilder 是一个帮助开发者快速开发 kubernetes API 的脚手架命令行工具,其依赖库 controller-tools 和 controller-runtime, controller-runtime 简化 kubernetes controller 的开发,并且对 kubernetes 的几个常用库进行了二次封装, 以简化开发者使用。controller-tool 主要功能是代码生成。下图是使用 kubebuilder 的工作流程图:

文章后面会结合一个简单示例来介绍开发流程。

kubebuilder 有非常良好的文档,包括一个从零开始的示例,您应该以文档为主。

使用 kubebuilder 开发一个 CRD

本次示例创建一个通用的Application资源,Application 包含一个子资源 Deployment 以及一个 Count 资源, 每当 Application 进行被重新协调Reconcil,Count 会进行自增。

全部代码请参考代码仓

前提(你需要提前了解的)

  1. Golang 开发者,kubernetes 大量使用Code Generate这一功能来自动生成重复性代码
  2. 阅读 kubernetes controller 的代码
  3. 阅读 kubebuilder 的文档
  4. 了解 kustomize

开发步骤及主要代码展示

首先,根据你的开发环境安装 kubebuilder 工具,mac 下通过 homebrew 安装命令如下:

1
2
3
➜  ~ brew install kubebuilder
➜ ~ kubebuilder version
Version: version.Version{KubeBuilderVersion:"unknown", KubernetesVendor:"unknown", GitCommit:"$Format:%H$", BuildDate:"1970-01-01T00:00:00Z", GoOs:"unknown", GoArch:"unknown"}

安装完毕后,首先创建项目目录custom-controller并使用go mod初始化项目

1
2
3
cd custom-controllers
➜ ls
➜ go mod init controllers.happyhack.io

接着,使用 kubebuilder 初始化项目,生成相关文件和目录,并创建 CRD 资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 使用kubebuilder初始化项目
➜ custom-controllers kubebuilder init --domain controller.daocloud.io --license apache2 --owner "Holder"
# 创建CRD资源
➜ custom-controllers kubebuilder create api --group controller --version v1 --kind Application
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
api/v1/application_types.go
controllers/application_controller.go
Running make:
$ make
/Users/donggang/Documents/Code/golang/bin/controller-gen object:headerFile="hack/huilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

到目前,该项目就已经能够运行了,不过我们需要添加我们自己业务代码,主要包括修改 CRD 定义和添加 controller 逻辑两部分。 首先修改 API 资源定义即 CRD 定义,Application 包含一个 Deployment,我们可以参考 kubernetes Deployment 与 Pod 这两个类型之间的关系设计 Application 和 Deployment, Deployment 通过字段spec.template来描述如何创建 Pod,DeploymentTemplateSpec描述了该如何创建 Deployment,

1
2
3
4
5
6
7
8
9
10
11
// PodTemplateSpec describes the data a deployment should have when created from a template
type DeploymentTemplateSpec struct {
// Standard object's metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`

// Specification of the desired behavior of the pod.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
Spec v12.DeploymentSpec `json:"spec,omitempty"`
}

API 描述定义完成了,接下来需要我们来进行具体业务逻辑实现了,编写具体 controller 实现,首先我们简单梳理 controller 的主要逻辑

  • 当一个 application 被创建时,需要创建对应的 deployment,当 application 被删除或更新时,对应 Deployment 也需要被删除或更新
  • 当 application 对应的子资源 deployment 被其他客户端删除或更新时,controller 需要重建或恢复它
  • 最后一步更新 application 的 status,这里即 count 加 1

我们在方法func(r *ApplicationReconciler) Reconcile(req ctrl.Request)(ctrl.Result,error)实现相关逻辑, 当然当业务逻辑比较复杂时,可以拆分为多个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func (r *ApplicationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log := r.Log.WithValues("application", req.NamespacedName)

var app controllerv1.Application
if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
log.Error(err, "unable to fetch app")
return ctrl.Result{}, client.IgnoreNotFound(err)
}

selector, err := metav1.LabelSelectorAsSelector(app.Spec.Selector)
if err != nil {
log.Error(err, "unable to convert label selector")
return ctrl.Result{}, err
}

var deploys v12.DeploymentList
if err := r.List(ctx, &deploys, client.InNamespace(req.Namespace), client.MatchingLabelsSelector{Selector: selector}); err != nil {
if errors.IsNotFound(err) {
deploy, err := r.constructDeploymentForApplication(&app)
if err != nil {
log.Error(err, "unable to construct deployment")
return ctrl.Result{
RequeueAfter: time.Second * 1,
}, err
}
if err = r.Create(ctx, deploy); err != nil {
return ctrl.Result{RequeueAfter: time.Second * 1}, err
}
}
}
...
}

完成Reconcile方法后,我们可以修改config目录的示例yaml,来进行本地测试了。

官方开发自定义 Controller 的指导

kubernetes开箱自带了多个controller,这些controller在我们开发时具有非常重要的参考价值,同时社区也总结了的 controller 开发所需要遵循十一条原则, 但是请大家结合实际场景灵活运用这些原则:

总结及展望

本文简单介绍了 CRD 以及如何使用脚手架工具 kubebuilder 帮助我们开发自定义 controller,当然这个 controller 示例的逻辑比较简单, 在实际场景中,我们会遇到很多的挑战,比如controller 的逻辑会比较复杂、需要通过多个controller等。作为kubernetes开发者, Controller开发是一项必不可少的技能。

参考