本文已超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
Pod 標籤控制器編寫指南
Operator 證明是運行 Kubernetes 中具狀態分散式應用程式的絕佳解決方案。像是 Operator SDK 等開源工具,提供了建構可靠且可維護 Operator 的方法,使其更容易擴展 Kubernetes 並實作自訂排程。
Kubernetes Operator 在您的叢集中運行複雜的軟體。開源社群已經為 Prometheus、Elasticsearch 或 Argo CD 等分散式應用程式建構了許多 Operator。即使在開源之外,Operator 也可以幫助為您的 Kubernetes 叢集帶來新功能。
Operator 是一組自訂資源和一組控制器。控制器監控 Kubernetes API 中特定資源的變更,並透過建立、更新或刪除資源來做出反應。
Operator SDK 最適合用於建構功能完善的 Operator。儘管如此,您可以使用它來編寫單一控制器。這篇文章將引導您逐步完成使用 Go 編寫 Kubernetes 控制器的過程,該控制器將 pod-name
標籤新增到具有特定註解的 Pod。
為什麼我們需要控制器來執行此操作?
我最近在一個專案中工作,我們需要建立一個 Service,將流量路由到 ReplicaSet 中的特定 Pod。問題在於 Service 只能透過標籤選擇 Pod,而 ReplicaSet 中的所有 Pod 都具有相同的標籤。有兩種方法可以解決這個問題
- 建立一個沒有選擇器的 Service,並直接管理該 Service 的 Endpoints 或 EndpointSlices。我們需要編寫一個自訂控制器,將 Pod 的 IP 位址插入到這些資源中。
- 將具有唯一值的標籤新增到 Pod。然後我們可以在 Service 的選擇器中使用這個標籤。同樣,我們需要編寫一個自訂控制器來新增這個標籤。
控制器是一個控制迴路,追蹤一個或多個 Kubernetes 資源類型。選項 2 中的控制器只需要追蹤 Pod,這使其更容易實作。這就是我們要逐步完成的選項,透過編寫一個 Kubernetes 控制器,將 pod-name
標籤新增到我們的 Pod。
StatefulSets 透過將 pod-name
標籤新增到集合中的每個 Pod,原生執行此操作。但是,如果我們不想或無法使用 StatefulSets 呢?
我們很少直接建立 Pod;大多數時候,我們使用 Deployment、ReplicaSet 或其他高階資源。我們可以在 PodSpec 中指定要新增到每個 Pod 的標籤,但不能使用動態值,因此無法複製 StatefulSet 的 pod-name
標籤。
我們嘗試使用 mutating admission webhook。當任何人建立 Pod 時,webhook 會使用包含 Pod 名稱的標籤修補 Pod。令人失望的是,這不起作用:並非所有 Pod 在建立之前都有名稱。例如,當 ReplicaSet 控制器建立 Pod 時,它會將 namePrefix
發送到 Kubernetes API 伺服器,而不是 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
接下來,讓我們初始化一個新的 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
}
每當建立、更新或刪除 Pod 時,就會呼叫 Reconcile
方法。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)
}
當 Operator 啟動時,會呼叫 SetupWithManager
方法。它的作用是告訴 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...
)
讓我們在 SetupWithManager
中使用 Pod
類型,告訴 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 的狀態或其 finalizers 互動。它只需要讀取和更新 Pod。讓我們刪除不必要的權限,只保留我們需要的權限
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch
我們現在準備好編寫控制器的協調邏輯。
實作協調
以下是我們希望 Reconcile
方法執行的操作
- 使用
ctrl.Request
中的 Pod 名稱和命名空間,從 Kubernetes 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
}
當建立、更新或刪除 Pod 時,將呼叫我們的 Reconcile
方法。在刪除的情況下,我們對 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 呼叫。
讓我們讓 Operator 保持運行,並在另一個終端中建立一個新的 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 儲存庫 中找到我的所有程式碼。
如何了解更多資訊?
Operator SDK 文件詳細介紹了如何更進一步並實作更複雜的 Operator。
在建模更複雜的使用案例時,單一控制器作用於內建 Kubernetes 類型可能不夠。您可能需要使用 自訂資源定義 (CRD) 和多個控制器來建構更複雜的 Operator。Operator SDK 是一個很棒的工具,可以幫助您做到這一點。
如果您想討論建構 Operator,請加入 #kubernetes-operator 頻道中的 Kubernetes Slack 工作區!