使用 Kubebuilder 构建 Kubernetes CRD Controller

2019/08/01 00:48 上午 posted in  Kubernetes

前一篇文章(《如何使用 CRD 拓展 Kubernetes 集群》)通过一个 Demo 讲解 CRD 是什么,以及可以提供什么能力,本文继续基于这个 Demo(https://github.com/Coderhypo/KubeService ),来讲解一下如何构建一个 CRD Controller。

CRD Controller

对于 CRD(CustomResourceDefinition)本身来说,你把它理解为只是一个 OpenApi 的 Schema 一点也不过分,因为这也是它唯一的能力和作用,而对于广义的说法:“利用 CRD 实现了 xx 功能”,真正承担功能实现的,其实说的是 CRD Controller。

Kubernetes 本身就自带了一堆 Controller,Master 节点上的三大核心组件之一:Controller Manager,其实就是一堆 Controller 的集合。

Controller Manger 里有不少 Controller 和我们要实现的 CRD Controller 本质上做的事其实是一样,就是对特定资源进行管理。

而且 Kubernetes 中不同的 Controller 间的通讯方式也非常有意思,以通过 Deployment 创建 Pod 举例:

用户通过 Kubectl 创建 Deployment,APIServer 会对该请求进行权限、准入认证,然后将 Deployment 的资源存储到 ETCD 中,因为 Kubernetes 通过 ETCD 实现了 List-Watch 机制,因此对 Deployment 相关事件感兴趣的 Deployment Controller 会受到资源的 ADD 事件并处理,即为该 Deployment 创建 RS。

RS 创建请求在被 APIServer 接收之后,RS 的 ADD 事件将被发布,因此 ReplicaSet Controller 将会接收到该事件,并进行后续处理:即创建 Pod。

因此可以看到,得益于 Kubernetes 基于事件的工作方式,创建受 Deployment 管理的 Pod 这个动作,Deployment Controller 和 ReplicaSet Controller 都参与执行,但是这两个 Controller 之间并没有直接通讯。

也正是因为其基于事件的工作方式,我们可以自定义 Controller 来处理感兴趣的事件,包括但不局限于 CR 的创建、修改等。

Kubebuilder 和 Operator-SDK

对于 CRD Controller 的构建,有几个主流的工具,一个是 coreOS 开源的 Operator-SDK(https://github.com/operator-framework/operator-sdk ),另一个是 K8s 兴趣小组维护的 Kubebuilder(https://github.com/kubernetes-sigs/kubebuilder )。

Operator-SDK 是 Operator 框架的一部分,Operator 社区比较成熟而且活跃,甚至还有自己的 Hub(https://operatorhub.io/ ) 来让大家探索、分享有趣的 Operator。

Kubebuilder 与其说是个 SDK 不如说是一个代码生成器,通过符合格式的注释和数据结构来生成一个可运行的 Controller。在使用下来最大的感觉是基础设施还不是很完善,比如文档,在写这个 Demo 很多场景还是靠翻代码才找到怎么解决。

Kubebuilder quick start

可能得益于 Kubebuilder 偏代码生成器,所以使用 Kubebuilder 从零到一创建一个 Controller 是非常简单,想必会有很多文章或 topic 会以 “x 分钟创建 CRD Controller” 为题吧。

官方入门文档可见:https://book.kubebuilder.io/quick-start.html

创建项目

首先通过 Kubebuilder init 命令初始化项目,--domain flag arg 来指定 api group。

kubebuilder init --domain o0w0o.cn --owner "Hypo"

当项目创建好之后,会提醒你是否下载依赖,然后你会发现大半个 Kubernetes 的代码已经在你 GOPATH 里了 ┑( ̄Д  ̄)┍。

创建 Api

当项目创建好就可以创建 api:

kubebuilder create api --group app --version v1 --kind App
kubebuilder create api --group app --version v1 --kind MicroService

创建 api 的同时你会发现 Kubebuilder 会帮你创建一些目录和源代码文件:

  1. pkg.apis 里面包含了资源 AppMicroService 的默认数据结构
  2. pkg.controller 里面 AppMicroService 的两个默认 Controller

resource type

Kubebuilder 已经帮你创建和默认的结构:

// MicroService is the Schema for the microservices API
// +k8s:openapi-gen=true
type MicroService struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   MicroServiceSpec   `json:"spec,omitempty"`
    Status MicroServiceStatus `json:"status,omitempty"`
}

你要做的只是拓展,以 MicroService 举例:

type Canary struct {
    // +kubebuilder:validation:Maximum=100
    // +kubebuilder:validation:Minimum=1
    Weight int `json:"weight"`

    // +optional
    CanaryIngressName string `json:"canaryIngressName,omitempty"`

    // +optional
    Header string `json:"header,omitempty"`

    // +optional
    HeaderValue string `json:"headerValue,omitempty"`

    // +optional
    Cookie string `json:"cookie,omitempty"`
}

type DeployVersion struct {
    Name     string                `json:"name"`
    Template appsv1.DeploymentSpec `json:"template"`

    // +optional
    ServiceName string `json:"serviceName,omitempty"`

    // +optional
    Canary *Canary `json:"canary,omitempty"`
}

type ServiceLoadBalance struct {
    Name string             `json:"name"`
    Spec corev1.ServiceSpec `json:"spec"`
}

type IngressLoadBalance struct {
    Name string                        `json:"name"`
    Spec extensionsv1beta1.IngressSpec `json:"spec"`
}

type LoadBalance struct {
    // +optional
    Service *ServiceLoadBalance `json:"service,omitempty"`
    // +optional
    Ingress *IngressLoadBalance `json:"ingress,omitempty"`
}

// MicroServiceSpec defines the desired state of MicroService
type MicroServiceSpec struct {
    // +optional
    LoadBalance        *LoadBalance    `json:"loadBalance,omitempty"`
    Versions           []DeployVersion `json:"versions"`
    CurrentVersionName string          `json:"currentVersionName"`
}

完整的代码见:https://github.com/Coderhypo/KubeService/blob/master/pkg/apis/app/v1/microservice_types.go

controller 逻辑

如果之前没有了解过 Kubernetes 控制器的代码,可能会对默认生成的 Controller 很奇怪,默认 MicroService Controller 名为 ReconcileMicroService,其只有一个主要方法就是:

func (r *ReconcileMicroService) Reconcile(request reconcile.Request) (reconcile.Result, error)

在使这个 Controller Work 之前,需要 pkg.controller.microservice.micriservice_controller.go 中的 add 方法中注册要关注的事件,当任何感兴趣的事件发生时,Reconcile 便会被调用,这个函数的职责,就像 Deployment Controller 的 syncHandler 一样,当有事件发生时,去比对当前资源的状态和预期的状态是否一致,如果不一致,就去矫正。

比如 MicroService 通过 Deployment 管理版本,Reconcile 就要判断每个版本的 Deployment 是否存在,是否符合预期,不过不存在,就去创建,如果不符合,就去纠正。

具体 ReconcileMicroService 代码可见:https://github.com/Coderhypo/KubeService/blob/master/pkg/controller/microservice/microservice_controller.go

运行

当 CR 的结构已经确定和 Controller 代码完成之后,便可以尝试试运行一下。Kubebuilder 可以通过本地 kubeconfig 配置的集群试运行(对于快速创建一个开发集群推荐 minikube)。

首先记得在 main.go 中的 init 方法中添加 schema:

func init() {
    _ = corev1.AddToScheme(scheme)
    _ = appsv1.AddToScheme(scheme)
    _ = extensionsv1beta1.AddToScheme(scheme)
    _ = apis.AddToScheme(scheme)
    // +kubebuilder:scaffold:scheme
}

然后使 Kubebuilder 重新生成代码:

make

然后将 config/crd 下的 CRD yaml 应用到当前集群:

make install

在本地运行 CRD Controller(直接执行 main 函数也可以):

make run