1. 为什么需要crd
- kubernetes默认支持deployment,configmap等资源,供我们以资源的形式管理云平台上的服务。
- 但当我们需要添加新的资源时,为了不对kubernetes原有代码进行更改,则需要kubernetes支持动态扩展新资源类型。
- 因此提供了自定义资源crd作为动态扩展的入口,通过crd可以声明新资源类型,声明后后就能创建该自定义资源的实例,自定义资源对用的具体行为则由用户创建的operator完成。
2. 整体结构
如图所示,其中黄色的部分是kubernetes默认的部分,而白色的模块则是动态扩展的自定义资源及其组件。
- 在etcd中用户声明了自定义crd的结构,声明的自定义资源与deploymenet等默认资源使用方式一致
- 默认资源大部分由kubernetes中的controller-manager等组件管理,而自定义资源则由部署的operator服务通过api-server管理。CRD资源只是一个抽象资源数据,资源对象对应的实际服务是由operator提供。
譬如,声明了访问固定网页的资源crd,那么当建立该资源对象时,operator就会获取到该信息,并根据资源对象中提供的地址去执行访问网页的行为。
3. 使用kubebuilder构建operator
为什么使用kubebuilder? 因为当我们需要增加新资源时就需要声明crd资源和构建operator。不同的opertor都需要连接apiserver同步自定义资源,因此会有高度冗余的逻辑。
kubebuilder不仅会为我们生成opertor服务框架,还能自动同步自定义资源的变化。
用户只需要定义CRD结构,和处理资源变动时的回调即可。
3.1 项目结构
以使用kubebuilder创建的operator做结构分析
如下图所示,operator主要包含以下组件, 协同完成了对自定义资源的监控和根据资源处理对应的业务。
用户只需要处理黄色部分的工作
- 定义好自定义资源结构crd,由框架注册资源
- 定义回调函数controller,资源变动时处理业务
以下部分是kubebuilder的框架性组件
- Cache
Kubebuilder 的核心组件,负责在 Controller 进程里面根据 Scheme 同步 Api Server 中所有该 Controller 关心 GVKs 的 GVRs,其核心是 GVK -> Informer 的映射,Informer 会负责监听对应 GVK 的 GVRs 的创建/删除/更新操作,以触发 Controller 的 Reconcile 逻辑。
- Controller
Kubebuidler 为我们生成的脚手架文件,我们只需要实现 Reconcile 方法即可。
- Clients
在实现 Controller 的时候不可避免地需要对某些资源类型进行创建/删除/更新,就是通过该 Clients 实现的,其中查询功能实际查询是本地的 Cache,写操作直接访问 Api Server。
- Index
由于 Controller 经常要对 Cache 进行查询,Kubebuilder 提供 Index utility 给 Cache 加索引提升查询效率。
- Finalizer
在一般情况下,如果资源被删除之后,我们虽然能够被触发删除事件,但是这个时候从 Cache 里面无法读取任何被删除对象的信息,这样一来,导致很多垃圾清理工作因为信息不足无法进行,K8s 的 Finalizer 字段用于处理这种情况。在 K8s 中,只要对象 ObjectMeta 里面的 Finalizers 不为空,对该对象的 delete 操作就会转变为 update 操作,具体说就是 update deletionTimestamp 字段,其意义就是告诉 K8s 的 GC“在deletionTimestamp 这个时刻之后,只要 Finalizers 为空,就立马删除掉该对象”。
所以一般的使用姿势就是在创建对象时把 Finalizers 设置好(任意 string),然后处理 DeletionTimestamp 不为空的 update 操作(实际是 delete),根据 Finalizers 的值执行完所有的 pre-delete hook(此时可以在 Cache 里面读取到被删除对象的任何信息)之后将 Finalizers 置为空即可。
- OwnerReference
K8s GC 在删除一个对象时,任何 ownerReference 是该对象的对象都会被清除,与此同时,Kubebuidler 支持所有对象的变更都会触发 Owner 对象 controller 的 Reconcile 方法。
3.2 构建operator
我们将使用kubebuilder扩展一个带有replicas字段的资源 imoocpod。当创建该资源实例后,将会维持数量等于replicas, 名称等于该实例前缀的一组pod,
- kubebuilder 搭建与使用
Kubebuilder由Kubernetes特殊兴趣组(SIG) API Machinery 拥有和维护,能够帮助开发者创建 CRD 并生成 controller 脚手架。
- 安装kubebuilder
kubebuilder 使用起来比较简单,首先我们需要安装 kubebuilder 和它依赖的 kustomize。
os=$(go env GOOS)
arch=$(go env GOARCH)
# download kubebuilder and extract it to tmp
curl -sL https://go.kubebuilder.io/dl/2.3.0/${os}/${arch} | tar -xz -C /tmp/
# move to a long-term location and put it on your path
# (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else)
mv /tmp/kubebuilder_2.3.0_${os}_${arch} /usr/local/kubebuilder
export PATH=$PATH:/usr/local/kubebuilder/bin
- 初始化operator
首先通过以下命令创建 CR 的GVK(Group、Version、Kind):
# 定义 crd 所属的 domain,这个指令会帮助你生成一个工程。
$ kubebuilder init --domain xiyanxiyan10
# 创建 crd 在 golang 工程中的结构体,以及其所需的 controller 逻辑
$ kubebuilder create api --group batch --version v1alpha1 --kind ImoocPod
执行结束后,目录结构如下:
.
├── api ## 这里定义了 sample 的结构体,以及所需的 deepcopy 实现
│ └── v1alpha1
│ ├── groupversion_info.go
│ ├── imoocpod_types.go # 本例中需要二次开发 crd定义
│ └── zz_generated.deepcopy.go
├── bin
│ └── manager ## controller 编译后的 二进制文件
├── config ## 包含了我们在使用 crd 是可能需要的 yml 文件,包括rbac、controller的deployment 等
│ ├── certmanager
│ ├── crd
│ ├── default
│ ├── manager
│ ├── prometheus
│ ├── rbac
│ ├── samples
│ └── webhook
├── controllers ## 我们的controller 逻辑就放在这里,
│ ├── imoocpod_controller.go # 本例中需要二次开发 controller
│ └── suite_test.go
├── Dockerfile
├── go.mod
├── go.sum
├── hack
│ └── boilerplate.go.txt
├── main.go
├── Makefile
└── PROJECT
自定义CRD字段
在api/v1/imoocpod_types.go中,包含了 kubebuilder 为我们生成的 ImoocPodSpec 以及相关字段, 我们改造生成的ImoocPodSpec, ImoocPodStatus 两个结构,引入Replicas, PodNames 两个字段完成demo功能。
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// ImoocPodSpec defines the desired state of ImoocPod
type ImoocPodSpec 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 ImoocPod. Edit ImoocPod_types.go to remove/update
Replicas int `json:"replicas"` // 这里二次开发,加入计数字段
}
// ImoocPodStatus defines the observed state of ImoocPod
type ImoocPodStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Replicas int `json:"replicas"`
PodNames []string `json:"podNames"` //这里二次开发加入计数和pod列表
}
// +kubebuilder:object:root=true
// ImoocPod is the Schema for the imoocpods API
type ImoocPod struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ImoocPodSpec `json:"spec,omitempty"`
Status ImoocPodStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// ImoocPodList contains a list of ImoocPod
type ImoocPodList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ImoocPod `json:"items"`
}
- 完善 controller 逻辑
kubebuilder 依赖于 controller-runtime
实现 controller 整个处理流程,在此工程中,controller 对资源的监听依赖于 Informer
机制,细节详见K8s中 controller & infromer机制。controller-runtime
在此机制上又封装了一层,其整体流程入下图
其中 Informer 已经由kubebuilder和contorller-runtime 实现,监听到的资源的事件(创建、删除、更新、webhock)都会放在 Informer 中。然后这个事件会经过 predict()方法进行过滤,经过interface enqueue进行处理,最终放入 workqueue中。我们创建的 controller 则会依次从workqueue中拿取事件,并调用我们自己实现的 Recontile() 方法进行业务处理。
在controllers/imoocpod_controller.go中,有函数
// +kubebuilder:rbac:groups=sample.sample.io,resources=samples,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=sample.sample.io,resources=samples/status,verbs=get;update;patch
func (r *PlaybookReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
_ = context.Background()
_ = r.Log.WithValues("imoocpod", req.NamespacedName)
// your logic here
return ctrl.Result{}, nil
}
我们改造他,实现ImoocPod资源对应的具体逻辑
// +kubebuilder:rbac:groups=batch.xiyanxiyan10,resources=imoocpods,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch.xiyanxiyan10,resources=imoocpods/status,verbs=get;update;patch
func (r *ImoocPodReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
logger := r.Log.WithValues("imoocpod", req.NamespacedName)
logger.Info("start reconcile")
// fetch the ImoocPod instance
instance := &batchv1alpha1.ImoocPod{}
if err := r.Client.Get(ctx, req.NamespacedName, instance); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// 1. 获取 name 对应的所有的 pod 的列表
lbls := labels.Set{"app": instance.Name}
existingPods := &corev1.PodList{}
if err := r.Client.List(ctx, existingPods, &client.ListOptions{
Namespace: req.Namespace, LabelSelector: labels.SelectorFromSet(lbls)}); err != nil {
logger.Error(err, "fetching existing pods failed")
return ctrl.Result{}, err
}
// 2. 获取 pod 列表中的 pod name
var existingPodNames []string
for _, pod := range existingPods.Items {
if pod.GetObjectMeta().GetDeletionTimestamp() != nil {
continue
}
if pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodPending {
existingPodNames = append(existingPodNames, pod.GetObjectMeta().GetName())
}
}
// 4. pod.Spec.Replicas > 运行中的 len(pod.replicas),比期望值小,需要 scale up create
if instance.Spec.Replicas > len(existingPodNames) {
logger.Info(fmt.Sprintf("creating pod, current and expected num: %d %d", len(existingPodNames), instance.Spec.Replicas))
pod := newPodForCR(instance)
if err := controllerutil.SetControllerReference(instance, pod, r.Scheme); err != nil {
logger.Error(err, "scale up failed: SetControllerReference")
return ctrl.Result{}, err
}
// 5. pod.Spec.Replicas < 运行中的 len(pod.replicas),比期望值大,需要 scale down delete
if instance.Spec.Replicas < len(existingPodNames) {
logger.Info(fmt.Sprintf("deleting pod, current and expected num: %d %d", len(existingPodNames), instance.Spec.Replicas))
pod := existingPods.Items[0]
existingPods.Items = existingPods.Items[1:]
if err := r.Client.Delete(ctx, &pod); err != nil {
logger.Error(err, "scale down faled")
return ctrl.Result{}, err
}
}
return ctrl.Result{Requeue: true}, nil
}
func newPodForCR(cr *batchv1alpha1.ImoocPod) *corev1.Pod {
labels := map[string]string{"app": cr.Name}
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: cr.Name + "-pod",
Namespace: cr.Namespace,
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "busybox",
Image: "busybox",
Command: []string{"sleep", "3600"},
},
},
},
}
}
- 部署 crd 和 controller
在项目主目录下执行make install,会自动调用 kustomize 创建部署 crd 的yml并部署,我们也可以从 config/crd/bases/下找到对应的 crd yaml文件。
然后执行 make run 则在本地启动 operator 主程序,日志如下,可见已经在监听资源
$ make run
...
go vet ./...
/home/xiyanxiyan10/project/app/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
go run ./main.go
2021-07-16T19:51:29.959+0800 INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"}
2021-07-16T19:51:29.959+0800 INFO setup starting manager
2021-07-16T19:51:29.959+0800 INFO controller-runtime.manager starting metrics server {"path": "/metrics"}
2021-07-16T19:51:29.959+0800 INFO controller-runtime.controller Starting EventSource {"controller": "imoocpod", "source": "kind source: /, Kind="}
2021-07-16T19:51:30.060+0800 INFO controller-runtime.controller Starting Controller {"controller": "imoocpod"}
2021-07-16T19:51:30.060+0800 INFO controller-runtime.controller Starting workers {"controller": "imoocpod", "worker count": 1}
2021-07-16T19:51:30.060+0800 INFO controllers.ImoocPod start reconcile {"imoocpod": "default/demo"}
当我们想以deployment 方式部署controller时,可以使用 Dockerfile 构建镜像,使用config/manager/manager.yml 部署。
- 资源创建实例
新建文件demo.yaml, 并使用命令部署 kubectl create -f ./demo.yaml
apiVersion: batch.xiyanxiyan10/v1alpha1
kind: ImoocPod
metadata:
name: demo
spec:
replicas: 2
- 确认结果
可以看到,在kubernetes已经根据自定义资源实例创建了对应的pod对象