使用 operator-sdk 在 Kubernetes 中實作 CRD 以及 Controller
目標
使用 operator-sdk 建立一個 CustomResourceDefinition,類似於 ReplicaSet 的功能,在這裡筆者將他稱為 PodSet。
PodSet 可以自動的創建 Replicas 的數量個 Pods,並且在 Pods 被新增、刪除,或是 PodSet 的 Spec 被修改時自動增減 Pods 的數量。
先附上 PodSet 的 Spec:
1  | apiVersion: k8stest.justin0u0.com/v1alpha1  | 
程式碼的部分在:https://github.com/justin0u0/podset-operator
Prerequisites
筆者的環境如下:
1  | - Minikube v1.16.0  | 
Installation
operator-sdk
1  | brew install operator-sdk  | 
golang
1  | brew install golang-go  | 
Create new project
1  | mkdir podset-operator && cd podset-operator  | 
Create new API and Controller
我們 Create 一個新的 Resource 其中 group=k8stest,version=v1alpha1,kind=PodSet。
1  | operator-sdk create api --group=k8stest --version=v1alpha1 --kind=PodSet  | 
完成後目錄結構應該如下:
1  | .  | 
其中比較重要的幾個部分有 main.go,api/podset_types.go 以及 controllers/podset_controller.go。
Define API
在 api/podset_types.go 中,會看到這樣的一段 Code:
1  | // PodSetSpec defines the desired state of PodSet  | 
其中 PodSetSpec 是用來定義 User 給定的 Yaml 中,Spec 的部分要長成什麼樣子。因此我們將 Foo 刪除加上 Replicas。在 PodSetSpec 中,可以透過 +kubebuilder:validation 的 Marker 來驗證欄位。
1  | type PodSetSpec struct {  | 
而 PodSetStatus 是用來紀錄 Runtime 時的狀態,比如說可以紀錄屬於這個 PodSet 的 Pod 有哪些,以及真正的 Replicas 的值。另外,在 PodSetStatus 上加上 +kubebuilder:subresource:status,更新主資源不會修改到 Status,同樣地,更新 Status 不會更新到主資源。
1  | // +kubebuilder:subresource:status  | 
若不清楚 Spec 與 Status 的關係,可以參考 Spec & Status。
修改 API 後,要記得執行 make generate 以更新 generated code。
Generate CRD Manifests
完成 PodSetSpec 後,operator-sdk 可以根據 PodSetSpec 產生出部署用的 yaml file。
執行 make manifests,會看到 config/crd/bases/k8stest.justin0u0.com_podsets.yaml 的生成,這個 Config 是給 Kubernetes 用來驗證你寫的 PodSet 是否為正確的用的。
Implement the Controller
Controller 負責處理整個 CRD 的邏輯,在 controllers/podset_controller.go 中,有兩個重要的部分。
SetupWithManager
1  | // SetupWithManager sets up the controller with the Manager.  | 
在 SetupWithManager 中,會看到這樣一段程式碼,其中 For(&k8stestv1alpha1.PodSet) 代表最主要要觀察的 Resource 是 PodSet 這個 Resource,所謂觀察也就是只當有任何 PodSet 的 Create, Update, Delete 事件,都會送出一個 Reconcile Request,而 Reconcile 正是等一下會實作的 Controller Logic。
不過不只是在 PodSet Create, Update, Delete 時需要 Reconcile,在我們想要做的 PodSet 例子中,當由 PodSet 管理的 Pod 有 Create, Update, Delete 的事件時,也應該要做出對應的處理。
因此可以將 SetupWithManager 函數改成以下:
1  | // SetupWithManager sets up the controller with the Manager.  | 
其中的 Owns(&corev1.Pod{}) 代表 PodSet 會 Own 類別為 corev1.Pod 的 resource,並且,在 corev1.Pod 這種 resource 被 Create, Update, Delete 時,會送一個 Reconcile Request 給他的 Owner,也就是 PodSet。
Reconcile
1  | // +kubebuilder:rbac:groups=k8stest.justin0u0.com,resources=podsets,verbs=get;list;watch;create;update;patch;delete  | 
首先,Reconcile 的回傳值有 ctrl.Result 與 error 兩個。當 ctrl.Result 回傳 {Requeue: true} 或是 error != nil 的話,Controller 會 requeue reconcile 的請求以重新執行一遍 Reconcile。
再來,看到 Reconcile 上方的註解,代表 Controller 所擁有的 RBAC 權限。可以看到預設的 RBAC 權限包含對 PodSet 資源的 get, list, watch, create…。在 PodSet 中,需要權限來新增、刪除、取得 Pods,觀察 Pod 的刪減…權限,因此加上 Pod 的權限:
1  | // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;create;delete;watch  | 
接個開始實踐 PodSet 邏輯。首先,應該先找到發起請求的 PodSet:
1  | // Fetch podset instance  | 
r.Get 其實也等於 r.client.Get,r 是一個 PodSetReconciler,看一下 PodSetReconciler 的定義:
1  | // PodSetReconciler reconciles a PodSet object  | 
可以看到包含一個 client.Client,這是 Golang struct 中的 promoted field,因此可以直接用 client.Client 內的 method,例如 r.Get, r.List…等等。
再來,可以先預想一下生成由 PodSet 管理、生成出來的 Pod 要有怎麼樣的 Spec,以方便管理,因此筆者先實作 podForPodSet 函數,給定一個 PodSet,podForPodSet 函數回傳一個 Pod。
1  | func (r *PodSetReconciler) podForPodSet(podSet *k8stestv1alpha1.PodSet) *corev1.Pod {  | 
可以看到,產生出來的 Pod 有 labels {"app": "podset", "podset_cr": podSet.Name} 這樣的標籤,這個標籤可以幫助不同的 PodSet 區分屬於自己所創建(管理)的 Pods 有哪些。
最後一行的 ctrl.SetControllerReference 將 pod 的 Owner 綁定成傳入的 podSet。
接著,返回 Reconcile 函數的部分,先取得現在 PodSet 所擁有的,並且正在運行的 Pods:
1  | // Get all pods with label app=podSet.Name  | 
可以看到,利用 labelSet := labels.Set{"app": "podset", "podset_cr": podSet.name},再使用 r.List 的 opts LabelSelector,可以幫助我們篩選出屬於這個 podSet 所擁有的 pods。
取得的 podList 中,其中有一些 Pod 可能正在被 terminated(因為 k8s 中,被 elegant deleted by user 的 Pod 只會先將 DeletionTimestamp 設上數值而已),因此,利用 Pod 的 Phase 以及 ObjectMeta,篩選出真正還在運行的 Pod 有哪些。
1  | // Get all running pods  | 
如果不做 pod.ObjectMeta.DeletionTimstamp 的檢查,雖然 Pod 在被刪除的那個剎那就會呼叫一次 Reconcile,但是我們會以為這個 Pod 還在運行中,因爲其 Status.Phase 還是 Running。
取得 runningPods 以及 runningPodNames 後,首先更新 PodSet 的 Status:
1  | // Update status if needed  | 
更新 podSet.Status 可以讓我們在其 yaml 中看到 PodSet 目前的真實狀態。
注意這裡的 r.Status().Update 並不等於 r.Update,r.Update 更新的是某個 resource 的 spec,而 r.Status().Update 是透過 StatusWriter 的 Update 來更新 resource 的 status。
最後,根據 runningPods 的數量,決定要增加或是刪除 Pods。
若 runningPods 數量過多,刪除一個 Pod:
1  | // Scale down pods  | 
刪除 Pods 的方法採用一次刪除一個,並且回傳 ctrl.Result{Requeue: true},因此下一次的 Reconcile 就會再刪除一個 pod,如果需要的話。
若 runningPods 數量不足,增加一個 Pod:
1  | // Scale up pods  | 
使用 podForPodSet 以及 r.Create 函數來創建一個新的 Pod。
最後完成的程式碼可以在最上面附上的 Github 連結查看。
Build and run the operator
完成程式碼後,首先:
1  | make install  | 
將 CRD 部署到 K8s cluster 中,因此要確認 k8s 這時已經開啟。(如果是使用 Minikube 的話,用 minikube status 確認)。
1  | kubectl get crds  | 
這時應該可以看到 CRD 已經被部署到 k8s cluster 中。
測試 Controller 時,可以使用以下兩種方法:
Run controller locally outside k8s cluster
1  | make run ENABLE_WEBHOOKS=false  | 
可以看到 Controller 已經 Run 在 local。
Run controller as Deployment inside k8s cluster
要將 controller 變成 Deployment 資源跑在 k8s 內,需要將 operator 打包成 Image。
若是使用 DockerHub 的話,首先確認已經 Login 到 Docker 帳號中:
1  | docker login  | 
接著打包 Image:
1  | export USERNAME=<your-docker-username>  | 
完成後可以看到 image 已經被創建並且打上 tag:
1  | docker images | grep "podset-operator"  | 
接著,將 Image push 到 container registry 中:
1  | make docker-push IMG=$USERNAME/podset-operator:v0.0.1  | 
最後 deploy 到 k8s cluster 中:
1  | make deploy IMG=$USERNAME/podset-operator:v0.0.1  | 
1  | kubectl get all -n podset-operator-system  | 
Test Controller
首先將 config/samples/k8stest_v1alpha1_podset.yaml 改成以下:
1  | apiVersion: k8stest.justin0u0.com/v1alpha1  | 
並將 PodSet resource 部署到 k8s cluster:
1  | kubectl -k ./config/samples/.  | 
完成後,一個 name=podset-sample 的 PodSet 資源會被部署到 default 的 namespace 中,並且應該可以看到兩個 Pod 已經在 default namespace 中被創建中:
1  | NAME READY STATUS RESTARTS AGE  | 
可以嘗試將其中一個 pod 刪除、將 PodSet Spec 的 replicas 修改並 Apply…,應該可以看到 podset-sample 的 pods 也會動態的增減。
或是嘗試將 PodSet 刪除,可以看到所有的 pods 也會跟著被刪除。
Cleanup
若是將 controller 部署在 k8s-cluster 中,可以執行:
1  | make undeploy  | 
來刪除所有部署的資源。
References
https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/
https://pkg.go.dev/github.com/kubernetes-sigs/controller-runtime