使用 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