本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
為 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 新增一個 `pod-name` 標籤。
為什麼我們需要一個控制器來做這件事?
我最近在一個專案中工作,我們需要建立一個 Service,將流量路由到 ReplicaSet 中的特定 Pod。問題是 Service 只能透過標籤選擇 Pod,而 ReplicaSet 中的所有 Pod 都具有相同的標籤。有兩種方法可以解決這個問題:
- 建立一個沒有選擇器的 Service,並直接管理該 Service 的 Endpoint 或 EndpointSlice。我們需要編寫一個自定義控制器來將 Pod 的 IP 地址插入到這些資源中。
- 為 Pod 新增一個具有唯一值的標籤。然後我們可以在 Service 的選擇器中使用這個標籤。同樣,我們需要編寫一個自定義控制器來新增這個標籤。
控制器是一個控制迴圈,它跟蹤一個或多個 Kubernetes 資源型別。上面選項 2 中的控制器只需要跟蹤 Pod,這使得實現起來更簡單。這就是我們將透過編寫一個 Kubernetes 控制器來實現的選項,該控制器將為我們的 Pod 新增一個 `pod-name` 標籤。
StatefulSet 原生支援透過為集合中的每個 Pod 新增 `pod-name` 標籤來實現此功能。但是,如果我們不想或不能使用 StatefulSet 呢?
我們很少直接建立 Pod;大多數情況下,我們使用 Deployment、ReplicaSet 或其他高階資源。我們可以在 PodSpec 中指定要新增到每個 Pod 的標籤,但不能使用動態值,因此無法複製 StatefulSet 的 `pod-name` 標籤。
我們嘗試過使用可變准入 Webhook。當任何人建立 Pod 時,Webhook 會用包含 Pod 名稱的標籤來修補 Pod。令人失望的是,這不起作用:並非所有 Pod 在建立前都有名稱。例如,當 ReplicaSet 控制器建立 Pod 時,它會向 Kubernetes API 伺服器傳送 `namePrefix` 而不是 `name`。API 伺服器在將新 Pod 持久化到 etcd 之前,但僅在呼叫我們的准入 Webhook 之後,才會生成一個唯一的名稱。因此,在大多數情況下,我們無法透過可變 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
}
`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...
)
讓我們在 `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 的狀態或其終結器互動。它只需要讀取和更新 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 新增 `pod-name` 標籤;如果缺少該註解,則不新增標籤。
- 在 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 Slack 工作區中的 #kubernetes-operator 頻道!