1. 為什么需要crd
- kubernetes默認支持deployment,configmap等資源抛虫,供我們以資源的形式管理云平臺上的服務(wù)松靡。
- 但當我們需要添加新的資源時,為了不對kubernetes原有代碼進行更改建椰,則需要kubernetes支持動態(tài)擴展新資源類型雕欺。
- 因此提供了自定義資源crd作為動態(tài)擴展的入口,通過crd可以聲明新資源類型棉姐,聲明后后就能創(chuàng)建該自定義資源的實例屠列,自定義資源對用的具體行為則由用戶創(chuàng)建的operator完成。
2. 整體結(jié)構(gòu)
如圖所示伞矩,其中黃色的部分是kubernetes默認的部分笛洛,而白色的模塊則是動態(tài)擴展的自定義資源及其組件。
- 在etcd中用戶聲明了自定義crd的結(jié)構(gòu)乃坤,聲明的自定義資源與deploymenet等默認資源使用方式一致
- 默認資源大部分由kubernetes中的controller-manager等組件管理撞蜂,而自定義資源則由部署的operator服務(wù)通過api-server管理。CRD資源只是一個抽象資源數(shù)據(jù)侥袜,資源對象對應(yīng)的實際服務(wù)是由operator提供。
譬如溉贿,聲明了訪問固定網(wǎng)頁的資源crd枫吧,那么當建立該資源對象時,operator就會獲取到該信息宇色,并根據(jù)資源對象中提供的地址去執(zhí)行訪問網(wǎng)頁的行為九杂。
3. 使用kubebuilder構(gòu)建operator
為什么使用kubebuilder? 因為當我們需要增加新資源時就需要聲明crd資源和構(gòu)建operator宣蠕。不同的opertor都需要連接apiserver同步自定義資源例隆,因此會有高度冗余的邏輯。
kubebuilder不僅會為我們生成opertor服務(wù)框架抢蚀,還能自動同步自定義資源的變化镀层。
用戶只需要定義CRD結(jié)構(gòu),和處理資源變動時的回調(diào)即可皿曲。
3.1 項目結(jié)構(gòu)
以使用kubebuilder創(chuàng)建的operator做結(jié)構(gòu)分析
如下圖所示唱逢,operator主要包含以下組件吴侦, 協(xié)同完成了對自定義資源的監(jiān)控和根據(jù)資源處理對應(yīng)的業(yè)務(wù)。
用戶只需要處理黃色部分的工作
- 定義好自定義資源結(jié)構(gòu)crd坞古,由框架注冊資源
- 定義回調(diào)函數(shù)controller备韧,資源變動時處理業(yè)務(wù)
以下部分是kubebuilder的框架性組件
- Cache
Kubebuilder 的核心組件,負責在 Controller 進程里面根據(jù) Scheme 同步 Api Server 中所有該 Controller 關(guān)心 GVKs 的 GVRs痪枫,其核心是 GVK -> Informer 的映射织堂,Informer 會負責監(jiān)聽對應(yīng) GVK 的 GVRs 的創(chuàng)建/刪除/更新操作,以觸發(fā) Controller 的 Reconcile 邏輯奶陈。
- Controller
Kubebuidler 為我們生成的腳手架文件易阳,我們只需要實現(xiàn) Reconcile 方法即可。
- Clients
在實現(xiàn) Controller 的時候不可避免地需要對某些資源類型進行創(chuàng)建/刪除/更新尿瞭,就是通過該 Clients 實現(xiàn)的闽烙,其中查詢功能實際查詢是本地的 Cache,寫操作直接訪問 Api Server声搁。
- Index
由于 Controller 經(jīng)常要對 Cache 進行查詢黑竞,Kubebuilder 提供 Index utility 給 Cache 加索引提升查詢效率。
- Finalizer
在一般情況下疏旨,如果資源被刪除之后很魂,我們雖然能夠被觸發(fā)刪除事件,但是這個時候從 Cache 里面無法讀取任何被刪除對象的信息檐涝,這樣一來遏匆,導致很多垃圾清理工作因為信息不足無法進行,K8s 的 Finalizer 字段用于處理這種情況谁榜。在 K8s 中幅聘,只要對象 ObjectMeta 里面的 Finalizers 不為空,對該對象的 delete 操作就會轉(zhuǎn)變?yōu)?update 操作窃植,具體說就是 update deletionTimestamp 字段帝蒿,其意義就是告訴 K8s 的 GC“在deletionTimestamp 這個時刻之后,只要 Finalizers 為空巷怜,就立馬刪除掉該對象”葛超。
所以一般的使用姿勢就是在創(chuàng)建對象時把 Finalizers 設(shè)置好(任意 string),然后處理 DeletionTimestamp 不為空的 update 操作(實際是 delete)延塑,根據(jù) Finalizers 的值執(zhí)行完所有的 pre-delete hook(此時可以在 Cache 里面讀取到被刪除對象的任何信息)之后將 Finalizers 置為空即可绣张。
- OwnerReference
K8s GC 在刪除一個對象時,任何 ownerReference 是該對象的對象都會被清除关带,與此同時侥涵,Kubebuidler 支持所有對象的變更都會觸發(fā) Owner 對象 controller 的 Reconcile 方法。
3.2 構(gòu)建operator
我們將使用kubebuilder擴展一個帶有replicas字段的資源 imoocpod。當創(chuàng)建該資源實例后独令,將會維持數(shù)量等于replicas, 名稱等于該實例前綴的一組pod端朵,
- kubebuilder 搭建與使用
Kubebuilder由Kubernetes特殊興趣組(SIG) API Machinery 擁有和維護,能夠幫助開發(fā)者創(chuàng)建 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
首先通過以下命令創(chuàng)建 CR 的GVK(Group招狸、Version敬拓、Kind):
# 定義 crd 所屬的 domain,這個指令會幫助你生成一個工程裙戏。
$ kubebuilder init --domain xiyanxiyan10
# 創(chuàng)建 crd 在 golang 工程中的結(jié)構(gòu)體乘凸,以及其所需的 controller 邏輯
$ kubebuilder create api --group batch --version v1alpha1 --kind ImoocPod
執(zhí)行結(jié)束后,目錄結(jié)構(gòu)如下:
.
├── api ## 這里定義了 sample 的結(jié)構(gòu)體累榜,以及所需的 deepcopy 實現(xiàn)
│ └── v1alpha1
│ ├── groupversion_info.go
│ ├── imoocpod_types.go # 本例中需要二次開發(fā) 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 # 本例中需要二次開發(fā) 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 以及相關(guān)字段, 我們改造生成的ImoocPodSpec, ImoocPodStatus 兩個結(jié)構(gòu)葛作,引入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"` // 這里二次開發(fā)猖凛,加入計數(shù)字段
}
// 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"` //這里二次開發(fā)加入計數(shù)和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
實現(xiàn) controller 整個處理流程赂蠢,在此工程中,controller 對資源的監(jiān)聽依賴于 Informer
機制辨泳,細節(jié)詳見K8s中 controller & infromer機制虱岂。controller-runtime
在此機制上又封裝了一層,其整體流程入下圖
其中 Informer 已經(jīng)由kubebuilder和contorller-runtime 實現(xiàn)菠红,監(jiān)聽到的資源的事件(創(chuàng)建第岖、刪除、更新试溯、webhock)都會放在 Informer 中绍傲。然后這個事件會經(jīng)過 predict()方法進行過濾,經(jīng)過interface enqueue進行處理耍共,最終放入 workqueue中。我們創(chuàng)建的 controller 則會依次從workqueue中拿取事件猎塞,并調(diào)用我們自己實現(xiàn)的 Recontile() 方法進行業(yè)務(wù)處理试读。
在controllers/imoocpod_controller.go中,有函數(shù)
// +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
}
我們改造他荠耽,實現(xiàn)ImoocPod資源對應(yīng)的具體邏輯
// +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 對應(yīng)的所有的 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
在項目主目錄下執(zhí)行make install南片,會自動調(diào)用 kustomize 創(chuàng)建部署 crd 的yml并部署吕粹,我們也可以從 config/crd/bases/下找到對應(yīng)的 crd yaml文件。
然后執(zhí)行 make run 則在本地啟動 operator 主程序务蝠,日志如下,可見已經(jīng)在監(jiān)聽資源
$ 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 構(gòu)建鏡像馏段,使用config/manager/manager.yml 部署。
- 資源創(chuàng)建實例
新建文件demo.yaml, 并使用命令部署 kubectl create -f ./demo.yaml
apiVersion: batch.xiyanxiyan10/v1alpha1
kind: ImoocPod
metadata:
name: demo
spec:
replicas: 2
- 確認結(jié)果
可以看到践瓷,在kubernetes已經(jīng)根據(jù)自定義資源實例創(chuàng)建了對應(yīng)的pod對象