使用 Operator SDK 為 Pod 標籤編寫Controller

語言: CN / TW / HK

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 儲存庫中找到我的所有程式碼。

https://github.com/busser/label-operator

如何學習更多?

Operator SDK 文件詳細介紹瞭如何進一步實現更復雜的 operator

Operator SDK 文件: https://sdk.operatorframework.io/docs/

在對更復雜的用例進行建模時,作用於內建 Kubernetes 型別的單個控制器可能還不夠。您可能需要使用自定義資源定義 (CRD) 和多個控制器構建更復雜的 operator Operator SDK 是一個很好的工具,可以幫助您做到這一點。

如果您想討論構建 operator ,請加入 Kubernetes Slack 工作區中的 #kubernetes-operator 頻道!

https://slack.k8s.io/
https://kubernetes.slack.com/messages/kubernetes-operators

更多文件

請關注微信公眾號 雲原生CTO [1]

參考資料

[1]

參考地址: https://kubernetes.io/blog/2021/06/21/writing-a-controller-for-pod-labels/