自定义资源是 Kubernetes API 的扩展,本文将讨什么时候应该向 Kubernetes 集群添加自定义资源以及何时使用独立服务。它描述了添加自定义资源的两种方法以及如何在它们之间进行选择。
Customer Resource #
概述 #
资源就是 Kubernetes API 集合中的某个的对象。例如,内置 pods 资源包含 Pod 对象的集合。
自定义资源是扩展了 Kubernetes API,但在默认的 API 集合中是不可用的,因为没有对应的 controller 处理业务逻辑。使用自定义资源,不仅可以解耦大多数的自研功能,还可用使得 Kubernetes 更加模块化。
自定义资源可以通过动态注册的方法,于正在 running 的集群中创建、删除和更新,并且与集群本身的资源是相互独立的。当自定义资源被创建,就可以通过 kubectl 创建和访问对象,和对内置资源的操作完全一致。
是否需要自定义资源 #
在创建新的 API 时,应该考虑与 Kubernetes 的 API 聚合,还是让 API 独立运行。
API 聚合 | API 独立 |
---|---|
声明式 | 非声明式 |
kubectl 可读可写 | 不需要 kubectl 支持 |
接受 k8s 的 REST 限制 | 特定 API 路径 |
可限定为 cluster 或者 namespace | 不适用 namespace |
重用 k8s API 支持的功能 | 不需要这些功能 |
声明式 API VS 命令式 API #
在声明式 API,通过具有以下特性:
- 对象颗粒度小
- 基础结构的定义
- 读写多,更新少,操作主要为 CRUD
- API 表期望状态
命令式 API,通过具有以下特性:
- 同步响应
- RPC 调用
- 大量数据存储(大对象或多对象)
- 高带宽访问
- 操作非 CRUD
- API 不易抽象
Customer Resource Definition #
Kubernetes 提供了两种向集群添加自定义资源的方法:
- 创建自定义 API server 并聚合到 API 中
- CRDs
Customer Resource Definition 介绍 #
kubernetes: v1.19.0
通过 CRD 创建自定义资源,只要符合 CRD 结构体定义,均可以被 Kubernetes 所接收,CRD 的定义和 k8s 原生资源的定义非常类似,都是由 4 个子资源组成:TypeMeta、ObjectMeta、Spec、Status,具体代码如下:
// staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go:354
type CustomResourceDefinition struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec CustomResourceDefinitionSpec
Status CustomResourceDefinitionStatus
}
下面主要来看下 Spec 和 Status 字段:
type CustomResourceDefinitionSpec struct {
Group string
// 定义复数、单数,简称,Kind,ListKind,Categories
Names CustomResourceDefinitionNames
// 表示集群级别或 namespace 级别
Scope ResourceScope
// 定义 CR 所有支持的版本号
Versions []CustomResourceDefinitionVersion
// 定义不同版本之间的转换方法
Conversion *CustomResourceConversion
// 是否保留未知字段,apiVersion、kind、metadata 等总是保留
PreserveUnknownFields *bool
}
type CustomResourceDefinitionStatus struct {
// 描述资源的当前状态,即 status.conditions
Conditions []CustomResourceDefinitionCondition
// 与 spec.names 类似,即 status.acceptNames
AcceptedNames CustomResourceDefinitionNames
// 资源曾经存在的版本,可在 etcd 中存储,供迁移用
StoredVersions []string
}
CRD 定义示例 #
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# 名称必须符合下面的格式:<plural>.<group>
name: foos.samplecontroller.io
spec:
# REST API 使用的组名称:/apis/<group>/<version>
group: samplecontroller.io
# REST API 使用的版本号:/apis/<group>/<version>
versions:
- name: v1alpha1
served: true
storage: true
schema:
# 校验方法
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
#类型校验
replicas:
type: integer
deploymentName:
type: string
# Namespaced 或 Cluster
scope: Namespaced
names:
# URL 中使用的复数名称:/apis/<group>/<version>/<plural>
plural: foos
# CLI 中使用的单数名称
singular: foo
# CamelCased 格式的单数类型。在清单文件中使用
kind: Foo
# 资源的 List 类型,默认是 “`kind`List”
listKind: FooList
# CLI 中使用的资源简称
shortNames:
- fo
# 分组
categories:
- all
Customer Controller #
只定义资源,不实现资源控制器是没有任何意义的。自定义控制器能够完成业务逻辑,最主要是依赖 client-go 库的各个组件的交互。下图展示它们之间的关系:
通过图示,可以看到几个核心组件的交互流程,蓝色表示 client-go,黄色是自定义 controller,各组件作用介绍如下:
client-go 组件 #
- Reflector:reflector 用来 watch 特定的 k8s API 资源。具体的实现是通过 ListAndWatch 的方法,watch 可以是 k8s 内建的资源或者是自定义的资源。当 reflector 通过 watch API 接收到有关新资源实例存在的通知时,它使用相应的列表 API 获取新创建的对象,并将其放入 watchHandler 函数内的 Delta FIFO 队列中。
- Informer:informer 从 Delta FIFO 队列中弹出对象。执行此操作的功能是 processLoop。base controller 的作用是保存对象以供以后检索,并调用我们的控制器将对象传递给它。
- Indexer:索引器提供对象的索引功能。典型的索引用例是基于对象标签创建索引。 Indexer 可以根据多个索引函数维护索引。Indexer 使用线程安全的数据存储来存储对象及其键。 在 Store 中定义了一个名为 MetaNamespaceKeyFunc 的默认函数,该函数生成对象的键作为该对象的 namespace/name 组合。
自定义 controller 组件 #
- Informer reference:指的是 Informer 实例的引用,定义如何使用自定义资源对象。自定义控制器代码需要创建对应的 Informer。
- Indexer reference:自定义控制器对 Indexer 实例的引用。自定义控制器需要创建对应的 Indexer。
- Resource Event Handlers:资源事件回调函数,当它想要将对象传递给控制器时,它将被调用。编写这些函数的典型模式是获取调度对象的 key,并将该 key 排入工作队列以进行进一步处理。
- Work queue:任务队列。编写资源事件处理程序函数以提取传递的对象的 key 并将其添加到任务队列。
- Process Item:处理任务队列中对象的函数,这些函数通常使用 Indexer 引用或 Listing 包装器来重试与该 key 对应的对象。
简单的说,整个处理流程大概为:Reflector 通过检测 Kubernetes API 来跟踪该扩展资源类型的变化,一旦发现有变化,就将该 Object 存储队列中,Informer 循环取出该 Object 并将其存入 Indexer 进行检索,同时触发 Callback 回调函数,并将变更的 Object Key 信息放入到工作队列中,此时自定义 Controller 里面的 Process Item 就会获取工作队列里面的 Key,并从 Indexer 中获取 Key 对应的 Object,从而进行相关的业务处理。
sample-controller 使用 #
资源准备 #
foo.crd.yaml #
使用 2.2 节的示例。
example.foo.yaml #
创建 foo 资源类型的对象:
apiVersion: samplecontroller.io/v1alpha1
kind: Foo
metadata:
name: example-foo
spec:
deploymentName: example-foo
replicas: 1
sample-controller 部署 #
代码 #
获取示例代码:
$ git clone https://github.com/kubernetes/sample-controller.git
$ cd sample-controller
编译 #
在编译二进制之前,要修改 pkg/apis/samplecontroller/register.go:21
GroupName 的定义,k8s.io 和 kubernetes.io 后缀都是官方保留字段,不建议 CRD 使用。所以将其修改为 samplecontroller.io
。
$ go build -o sample-controller .
启动 #
使用节点上的 kubeconfig 文件启动 controller:
$ ./sample-controller -kubeconfig=$HOME/.kube/config
启动时,会提示 watch 不到资源,因为还没创建定义:
I1026 23:40:56.529233 57978 controller.go:115] Setting up event handlers
I1026 23:40:56.529444 57978 controller.go:156] Starting Foo controller
I1026 23:40:56.529450 57978 controller.go:159] Waiting for informer caches to sync
E1026 23:40:56.554564 57978 reflector.go:138] k8s.io/sample-controller/pkg/generated/informers/externalversions/factory.go:117: Failed to watch *v1alpha1.Foo: failed to list *v1alpha1.Foo: the server could not find the requested resource (get foos.samplecontroller.io)
E1026 23:40:57.841194 57978 reflector.go:138] k8s.io/sample-controller/pkg/generated/informers/externalversions/factory.go:117: Failed to watch *v1alpha1.Foo: failed to list *v1alpha1.Foo: the server could not find the requested resource (get foos.samplecontroller.io)
创建资源 #
$ kubectl create -f foos.crd.yaml
customresourcedefinition.apiextensions.k8s.io/foos.samplecontroller.io created
$ kubectl create -f example.foos.yaml
foo.samplecontroller.io/example-foo created
创建完成,可以使用 kubectl get crd
查看资源定义和对象:
$ kubectl get crd
NAME CREATED AT
clusterversions.cfe.io 2020-10-26T14:43:27Z
foos.samplecontroller.io 2020-10-26T15:41:52Z
$ kubectl get foos
NAME AGE
example-foo 19m
sample-controller 会 watch 到新对象 default/example-foo:
I1026 23:42:03.232577 57978 controller.go:164] Starting workers
I1026 23:42:03.232607 57978 controller.go:170] Started workers
I1026 23:42:03.515970 57978 controller.go:228] Successfully synced 'default/example-foo'
I1026 23:42:03.516099 57978 event.go:291] "Event occurred" object="default/example-foo" kind="Foo" apiVersion="samplecontroller.io/v1alpha1" type="Normal" reason="Synced" message="Foo synced successfully"
校验 #
检查 controller 创建 Deployment 对象:
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
example-foo 1/1 1 1 3m1s
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
example-foo-54dc4db9fc-4r8pp 1/1 Running 0 3m8s
shell-demo 1/1 Running 9 43d
web-0 1/1 Running 16 78d
web-1 1/1 Running 16 78d