使用 Operator SDK 為 Pod 標籤編寫Controller
關 注 微 信 公 眾 號 《 雲 原 生 C T O 》 更 多 雲 原 生 幹 貨 等 你 來 探 索
專 注 於 雲原生技術
分 享
提 供 優 質 雲原生開發
視 頻 技 術 培 訓
面試技巧
, 及 技術疑難問題
解答
雲 原 生 技 術 分 享 不 僅 僅 局 限 於 Go
、 Rust
、 Python
、 Istio
、 containerd
、 CoreDNS
、 Envoy
、 etcd
、 Fluentd
、 Harbor
、 Helm
、 Jaeger
、 Kubernetes
、 Open Policy Agent
、 Prometheus
、 Rook
、 TiKV
、 TUF
、 Vitess
、 Arg
o
、 Buildpacks
、 CloudEvents
、 CNI
、 Contour
、 Cortex
、 CRI-O
、 Falco
、 Flux
、 gRPC
、 KubeEdge
、 Linkerd
、 NATS
、 Notary
、 OpenTracing
、 Operator Framework
、 SPIFFE
、 SPIRE
和 Thanos
等
使用 Operator SDK 為 Pod 標籤編寫Controller
Operator
被證明是在 Kubernetes
中執行有狀態分散式應用程式的絕佳解決方案。 Operator SDK
等開源工具提供了構建可靠且可維護的 Operator
的方法,使擴充套件 Kubernetes
和實現自定義排程變得更加容易。
Kubernetes operator
在您的叢集內運行復雜的軟體。開源社群已經為 Prometheus
、 Elasticsearch
或 Argo CD
等分散式應用程式構建了許多 operator
。即使在開源之外,運維人員也可以幫助為您的 Kubernetes
叢集帶來新功能。
operator
是一組自定義資源和一組控制器。控制器監視 Kubernetes API
中特定資源的更改,並通過建立、更新或刪除資源做出反應。
Operator SDK
最適合構建功能齊全的 Operator
。儘管如此,您可以使用它來編寫單個控制器。這篇文章將引導您在 Go
中編寫一個 Kubernetes
控制器,該控制器將為 pod-name
具有特定註釋的 pod
新增標籤。
為什麼我們需要一個控制器呢?
我最近在一個專案中工作,我們需要建立一個服務,將流量路由到 ReplicaSet
中的特定 Pod
。問題是一個 Service
只能按標籤選擇 pod
,而 ReplicaSet
中的所有 pod
都具有相同的標籤。有兩種方法可以解決這個問題:
建立沒有選擇器的服務並直接管理該服務的端點或端點切片。我們需要編寫一個自定義控制器來將 Pod
的 IP
地址插入到這些資源中。
為 Pod
新增一個具有唯一值的標籤。然後我們可以在我們的服務選擇器中使用這個標籤。同樣,我們需要編寫一個自定義控制器來新增這個標籤。
控制器是跟蹤一個或多個 Kubernetes
資源型別的控制迴圈。上面選項 n°2
中的控制器只需要跟蹤 pod
,這使其更易於實現。這是我們將通過編寫一個 Kubernetes
控制器來 pod-name
為我們的 pod
新增標籤的選項。
StatefulSets
通過為集合中的每個 Pod
新增 標籤來本地執行此操作。 pod-name
但是如果我們不想或不能使用 StatefulSets
怎麼辦?
我們很少直接建立 pod
;大多數情況下,我們使用 Deployment
、 ReplicaSet
或其他高階資源。我們可以在 PodSpec
中指定要新增到每個 Pod
的標籤,但不能使用動態值,因此無法複製 StatefulSet
的 pod-name
標籤。
我們嘗試使用 mutating admission webhook
。當任何人建立 Pod
時, webhook
會使用包含 Pod
名稱的標籤來修補 Pod
。令人失望的是,這不起作用:並非所有 pod
在建立之前都有名稱。例如,當 ReplicaSet
控制器建立 Pod
時,它會向 namePrefixKubernetes API
伺服器傳送 a
而不是 name
. API
伺服器在將新 Pod
持久化到 etcd
之前生成一個唯一名稱,但僅在呼叫我們的 admission webhook
之後。所以在大多數情況下,我們無法通過 mutating webhook
知道 Pod
的名稱。
一旦 Pod
存在於 Kubernetes API
中,它大部分是不可變的,但我們仍然可以新增標籤。我們甚至可以從命令列這樣做:
kubectl label my-pod my-label-key=my-label-value
我們需要觀察 Kubernetes API
中任何 pod
的變化並新增我們想要的標籤。與其手動執行此操作,我們將編寫一個控制器來為我們執行此操作。
使用 Operator SDK 引導控制器
控制器是一個協調迴圈,它從 Kubernetes API
讀取資源的所需狀態,並採取措施使叢集的實際狀態更接近所需狀態。
為了儘快編寫此控制器,我們將使用 Operator SDK
。如果您沒有安裝它,請按照 官方文件。
$ operator-sdk version operator-sdk version: "v1.4.2", commit: "4b083393be65589358b3e0416573df04f4ae8d9b", kubernetes version: "v1.19.4", go version: "go1.15.8", GOOS: "darwin", GOARCH: "amd64"
讓我們建立一個新目錄來寫入我們的控制器:
mkdir label-operator && cd label-operator
接下來,讓我們初始化一個新的運算子,我們將向其中新增一個控制器。為此,您需要指定域和儲存庫。域作為您的自定義 Kubernetes
資源所屬的組的字首。因為我們不會定義自定義資源,所以域無關緊要。儲存庫將是我們要編寫的 Go
模組的名稱。按照慣例,這是您將儲存程式碼的儲存庫。
例如,這是我執行的命令:
# Feel free to change the domain and repo values. operator-sdk init --domain=padok.fr --repo=github.com/busser/label-operator
接下來,我們需要建立一個新的控制器。此控制器將處理 pod
而不是自定義資源,因此無需生成資原始碼。讓我們執行這個命令來搭建我們需要的程式碼:
operator-sdk create api --group=core --version=v1 --kind=Pod --controller=true --resource=false
我們現在有一個新檔案: controllers/pod_controller.go
. 該檔案包含一個 PodReconciler
型別,其中包含我們需要實現的兩個方法。第一個是 Reconcile
,現在看起來像這樣:
func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = r.Log.WithValues("pod", req.NamespacedName) // your logic here return ctrl.Result{}, nil }
Reconcile
每當建立、更新或刪除 Pod
時都會呼叫該方法。 Pod
的名稱和名稱空間在 ctrl.Request
作為引數接收的方法中。
第二種方法是 SetupWithManager
,現在它看起來像這樣:
func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument // For(). Complete(r) }
該 SetupWithManager
方法在 operator
啟動時被呼叫。它用於告訴 operator
框架我們 PodReconciler
需要觀察哪些型別。要使用 Kubernetes
內部使用的相同 Pod
型別,我們需要匯入它的一些程式碼。所有 Kubernetes
原始碼都是開源的,因此您可以在自己的 Go
程式碼中匯入您喜歡的任何部分。您可以在 Kubernetes
原始碼中或在 pkg.go.dev
上找到可用包的完整列表。要使用 pod
,我們需要 k8s.io/api/core/v1
包。
package controllers import ( // other imports... corev1 "k8s.io/api/core/v1" // other imports... )
讓我們使用 Podin
型別 SetupWithManager
告訴 operator
框架我們要監視 pod
:
func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&corev1.Pod{}). Complete(r) }
在繼續之前,我們應該設定控制器需要的 RBAC
許可權。在方法之上 Reconcile
,我們有一些預設許可權:
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch // +kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update
我們不需要所有這些。我們的控制器永遠不會與 Pod
的狀態或其終結器互動。它只需要讀取和更新 pod
。讓我們刪除不必要的許可權,只保留我們需要的:
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch
我們現在準備編寫控制器的協調邏輯。
協調邏輯
這是我們希望我們的 Reconcile
方法執行的操作:
-
使用
Pod
的名稱和名稱空間從ctrl.RequestKubernetes API
獲取Pod
。 -
如果
Pod
有add-pod-name-label
註解,pod-name
則給Pod
新增標籤;如果註釋丟失,請不要新增標籤。 -
更新
Kubernetes API
中的Pod
以保留所做的更改。 讓我們為註解和標籤定義一些常量:
const ( addPodNameLabelAnnotation = "padok.fr/add-pod-name-label" podNameLabel = "padok.fr/pod-name" )
我們協調功能的第一步是從 Kubernetes API
獲取我們正在處理的 Pod
:
// Reconcile handles a reconciliation request for a Pod. // If the Pod has the addPodNameLabelAnnotation annotation, then Reconcile // will make sure the podNameLabel label is present with the correct value. // If the annotation is absent, then Reconcile will make sure the label is too. func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("pod", req.NamespacedName) /* Step 0: Fetch the Pod from the Kubernetes API. */ var pod corev1.Pod if err := r.Get(ctx, req.NamespacedName, &pod); err != nil { log.Error(err, "unable to fetch Pod") return ctrl.Result{}, err } return ctrl.Result{}, nil }
Reconcile
建立、更新或刪除 Pod
時將呼叫我們的方法。在刪除的情況下,我們的呼叫 r.Get
將返回一個特定的錯誤。讓我們匯入定義此錯誤的包:
package controllers import ( // other imports... apierrors "k8s.io/apimachinery/pkg/api/errors" // other imports... )
我們現在可以處理這個特定的錯誤,並且——因為我們的控制器不關心已刪除的 pod
——明確地忽略它:
/* Step 0: Fetch the Pod from the Kubernetes API. */ var pod corev1.Pod if err := r.Get(ctx, req.NamespacedName, &pod); err != nil { if apierrors.IsNotFound(err) { // we'll ignore not-found errors, since we can get them on deleted requests. return ctrl.Result{}, nil } log.Error(err, "unable to fetch Pod") return ctrl.Result{}, err }
接下來,讓我們編輯我們的 Pod
,以便當且僅當我們的註解存在時,我們的動態標籤才存在:
/* Step 1: Add or remove the label. */ labelShouldBePresent := pod.Annotations[addPodNameLabelAnnotation] == "true" labelIsPresent := pod.Labels[podNameLabel] == pod.Name if labelShouldBePresent == labelIsPresent { // The desired state and actual state of the Pod are the same. // No further action is required by the operator at this moment. log.Info("no update required") return ctrl.Result{}, nil } if labelShouldBePresent { // If the label should be set but is not, set it. if pod.Labels == nil { pod.Labels = make(map[string]string) } pod.Labels[podNameLabel] = pod.Name log.Info("adding label") } else { // If the label should not be set but is, remove it. delete(pod.Labels, podNameLabel) log.Info("removing label") }
最後,讓我們將更新後的 Pod
推送到 Kubernetes API
:
/* Step 2: Update the Pod in the Kubernetes API. */ if err := r.Update(ctx, &pod); err != nil { log.Error(err, "unable to update Pod") return ctrl.Result{}, err }
將更新後的 Pod
寫入 Kubernetes API
時,存在自我們第一次讀取 Pod
以來已更新或刪除的風險。在編寫 Kubernetes
控制器時,我們應該記住,我們不是叢集中唯一的參與者。發生這種情況時,最好的辦法是通過重新排隊事件從頭開始協調。讓我們這樣做:
/* Step 2: Update the Pod in the Kubernetes API. */ if err := r.Update(ctx, &pod); err != nil { if apierrors.IsConflict(err) { // The Pod has been updated since we read it. // Requeue the Pod to try to reconciliate again. return ctrl.Result{Requeue: true}, nil } if apierrors.IsNotFound(err) { // The Pod has been deleted since we read it. // Requeue the Pod to try to reconciliate again. return ctrl.Result{Requeue: true}, nil } log.Error(err, "unable to update Pod") return ctrl.Result{}, err }
讓我們記住在方法結束時成功返回:
return ctrl.Result{}, nil }
就是這樣!我們現在準備在我們的叢集上執行控制器。
在叢集上執行控制器
要在您的叢集上執行我們的控制器,我們需要執行 operator
。為此,您只需要 kubectl
. 如果您手頭沒有 Kubernetes
叢集,我建議您使用 KinD (Kubernetes in Docker)
在本地啟動一個。
從您的機器上執行 operator
所需要的只是以下命令:
make run
幾秒鐘後,您應該會看到 operator
的日誌。請注意,我們的控制器 Reconcile
方法已為叢集中已經執行的所有 Pod
呼叫。
讓我們讓 oeprator
繼續執行,並在另一個終端中建立一個新 Pod
:
kubectl run --image=nginx my-nginx
operator
應該快速列印一些日誌,表明它對 Pod
的建立和隨後的狀態變化做出了反應:
INFO controllers.Pod no update required {"pod": "default/my-nginx"} INFO controllers.Pod no update required {"pod": "default/my-nginx"} INFO controllers.Pod no update required {"pod": "default/my-nginx"} INFO controllers.Pod no update required {"pod": "default/my-nginx"}
讓我們檢查 Pod
的標籤:
$ kubectl get pod my-nginx --show-labels NAME READY STATUS RESTARTS AGE LABELS my-nginx 1/1 Running 0 11m run=my-nginx
讓我們為 Pod
新增一個註解,以便我們的控制器知道向它新增我們的動態標籤:
kubectl annotate pod my-nginx padok.fr/add-pod-name-label=true
請注意,控制器立即做出反應並在其日誌中生成了一個新行:
INFO controllers.Pod adding label {"pod": "default/my-nginx"} $ kubectl get pod my-nginx --show-labels NAME READY STATUS RESTARTS AGE LABELS my-nginx 1/1 Running 0 13m padok.fr/pod-name=my-nginx,run=my-nginx
太棒了!您剛剛成功編寫了一個 Kubernetes
控制器,該控制器能夠為叢集中的資源新增具有動態值的標籤。
控制器和 operator
,無論大小,都可以成為您 Kubernetes
之旅的重要組成部分。現在編寫 operator
比以往任何時候都容易。可能性是無止境。
接下來是什麼?
如果您想更進一步,我建議您首先在叢集中部署控制器或 operator
。 Operator SDK
生成的 Makefile
將完成大部分工作。
將 operator
部署到生產環境時,實施穩健的測試始終是一個好主意。朝著這個方向邁出的第一步是編寫單元測試。 本文件將指導您為 operator
編寫測試。我為我們剛剛編寫的 operator
編寫了測試;你可以在這個 GitHub
儲存庫中找到我的所有程式碼。
http://github.com/busser/label-operator
如何學習更多?
Operator SDK
文件詳細介紹瞭如何進一步實現更復雜的 operator
。
Operator SDK
文件: http://sdk.operatorframework.io/docs/
在對更復雜的用例進行建模時,作用於內建 Kubernetes
型別的單個控制器可能還不夠。您可能需要使用自定義資源定義 (CRD)
和多個控制器構建更復雜的 operator
。 Operator SDK
是一個很好的工具,可以幫助您做到這一點。
如果您想討論構建 operator
,請加入 Kubernetes Slack
工作區中的 #kubernetes-operator
頻道!
http://slack.k8s.io/
http://kubernetes.slack.com/messages/kubernetes-operators
更多文件
請關注微信公眾號 雲原生CTO [1]
參考資料
參考地址: http://kubernetes.io/blog/2021/06/21/writing-a-controller-for-pod-labels/
- Go 中的構建器模式
- 讓我們使用 Go 實現基本的服務發現
- 雲原生下一步的發展方向是什麼?
- 用更雲原生的方式做診斷|大規模 K8s 叢集診斷利器深度解析
- 多個維度分析k8s多叢集管理工具,到底哪個才真正適合你
- 使用 Kube-capacity CLI 檢視 Kubernetes 資源請求、限制和利用率
- 使用 Go 在 Kubernetes 中構建自己的准入控制器
- 雲原生數倉如何破解大規模叢集的關聯查詢效能問題?
- 雲原生趨勢下的遷移與災備思考
- 2022 年不容錯過的六大雲原生趨勢!
- 使用 Prometheus 監控 Golang 應用程式
- 雲原生時代下的機遇與挑戰 DevOps如何破局
- 如何在雲原生格局中理解Kubernetes合規性和安全框架
- 設計雲原生應用程式的15條基本原則
- 使用 Operator SDK 為 Pod 標籤編寫Controller
- Kubernetes Visitor 模式
- 為什麼雲原生是第二次雲革命
- 構建雲原生安全的六個重要能力
- 擴充套件雲原生策略的步驟有哪些?
- 七個值得關注的開源雲原生工具