Kubernetes Operator 提供了擴充套件 Kubernetes API 的機制,讓開發者能以宣告式的方式管理複雜應用程式。本文詳細說明如何使用 Kubebuilder 工具來建立一個 Operator,並定義自訂資源 EGApp,包含 Spec 和 Status 結構的設計與驗證規則設定。文章也解析了 CRD 檔案的結構,以及如何生成和安裝 CRD。此外,更進一步說明 Controller 的核心邏輯,包含 Reconcile 函式的運作流程、Deployment 的建立與更新,以及如何將 Pod 狀態同步到自訂資源的 Status 中,提供完整的 Operator 開發流程參考。

Kubernetes Operator 開發:API 定義與自訂資源

Kubernetes Operator 是擴充套件 Kubernetes 功能的重要工具,允許開發者建立自訂資源並自動化複雜應用程式的管理。本文將介紹如何使用 Kubebuilder 建立一個簡單的 Operator,並定義自訂 API 資源。

建立 Operator 專案

首先,使用 Kubebuilder 建立新的 Operator 專案:

$ kubebuilder init --domain platform.evillgenius.com --repo platformapp

這將建立基本的專案結構,包括 main.goMakefile 和其他必要檔案。

建立 API 定義

接下來,建立新的 API 定義:

$ kubebuilder create api --group egplatform --version v1alpha1 --kind EGApp

這將建立 api/v1alpha1/egapp_types.gocontrollers/egapp_controller.go 檔案,分別定義了自訂資源的結構和控制器邏輯。

修改 API 定義

api/v1alpha1/egapp_types.go 中,修改 EGAppSpecEGAppStatus 結構以滿足應用程式需求:

type EGAppSpec struct {
    // AppId 是內部目錄系統的唯一匹配 ID
    AppId string `json:"appId,omitempty"`
    // +kubebuilder:validation:Enum=java;python;go
    Framework string `json:"framework"`
    // +kubebuilder:validation:Optional
    // +kubebuilder:validation:Enum=lowMem;highMem;highCPU;balanced
    // +kubebuilder:default="lowMem"
    InstanceType string `json:"instanceType"`
    // +kubebuilder:validation:Enum=dev;stage;prod
    Environment string `json:"environment"`
    // +kubebuilder:validation:Optional
    // +kubebuilder:default:=1
    ReplicaCount int32 `json:"replicaCount"`
}

type EGAppStatus struct {
    // Pod 清單
    Pods []string `json:"pods"`
}

內容解密:

  • EGAppSpec 定義了自訂資源的期望狀態,包括應用程式 ID、框架型別、例項型別、環境和副本數量。
  • EGAppStatus 定義了自訂資源的觀察狀態,包含目前執行的 Pod 清單。
  • 使用 Kubebuilder 的 marker comments(如 +kubebuilder:validation:Enum)來驗證欄位值。

生成 CRD 和清單

修改完成後,執行以下命令生成 CRD 和其他清單:

$ make generate
$ make manifests

這將更新 config/crd/bases 下的 CRD 檔案。

CRD 結構解析

生成的 CRD 檔案定義了自訂資源的結構,以下是部分內容:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: egapps.egplatform.platform.evillgenius.com
spec:
  group: egplatform.platform.evillgenius.com
  names:
    kind: EGApp
    plural: egapps
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              appId:
                type: string
              environment:
                enum:
                - dev
                - stage
                - prod
                type: string

圖表翻譯:

此圖示呈現了 CRD 的結構,包括 API 群組、版本、名稱和結構定義。使用 OpenAPI V3 Schema 驗證自訂資源的內容。

圖表翻譯: 此圖表展示了 CRD 的組成部分,包括 API 群組、版本、名稱和結構定義。結構定義進一步分為 Spec 和 Status,其中 Spec 包含應用程式的詳細設定,如 appId、environment 和 framework。

自訂資源定義與 Operator 實作詳解

在 Kubernetes 中,Operator 是一種用於管理和維護特定應用程式或資源的自訂控制器。本章節將探討如何使用 Kubebuilder 建立自訂資源定義(CRD)以及實作 Operator 的核心邏輯。

建立自訂資源定義

首先,我們使用 Kubebuilder 建立一個名為 EGApp 的自訂資源。EGApp 的結構定義如下:

type: object
properties:
  environment:
    type: string
  framework:
    type: string
  status:
    type: object
    properties:
      pods:
        type: array
        items:
          type: string
    required:
      - pods

程式碼解密:

  • type: object 定義了 EGApp 是一個物件型別的資源。
  • properties 欄位定義了資源的屬性,包括 environmentframeworkstatus
  • status 欄位是一個物件,包含了 pods 屬性,代表與該 EGApp 相關的 Pod 清單。
  • required 欄位確保了 status 中必須包含 pods 屬性。

Kubebuilder 不僅生成了基本的 CRD 結構,還加入了 OpenAPI 驗證資訊,以確保自訂資源符合特定的規範。

安裝自訂資源定義

執行以下指令,可以將 EGApp CRD 安裝到 Kubernetes 叢集中:

$ make install

該指令會使用 Kubebuilder 生成必要的組態檔案,並透過 kubectl apply 將 CRD 安裝到叢集中。

程式碼解密:

  • make install 指令會呼叫預先定義的 Makefile 規則,生成 CRD 組態檔案並安裝到叢集中。
  • /home/eddiejv/dev/projects/operators/platformapp/bin/controller-gen 是用於生成 CRD 和 Webhook 組態的工具。
  • kubectl apply -f - 將生成的 CRD 組態套用到叢集中。

驗證自訂資源

安裝完成後,可以使用以下指令檢視 EGApp 資源的結構和說明:

$ kubectl explain egapp --recursive

程式碼解密:

  • kubectl explain 指令用於顯示 Kubernetes 資源的詳細說明。
  • --recursive 引數表示遞迴顯示所有巢狀欄位的說明。

Controller 調和邏輯

Operator 的核心是調和(Reconciliation)邏輯,用於確保自訂資源的實際狀態與預期狀態一致。調和邏輯主要實作在 controllers/<kind>_controller.go 檔案中的 Reconcile 方法。

調和邏輯的基本流程如下圖所示:

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Kubernetes Operator 開發 API 與自訂資源

package "Kubernetes Cluster" {
    package "Control Plane" {
        component [API Server] as api
        component [Controller Manager] as cm
        component [Scheduler] as sched
        database [etcd] as etcd
    }

    package "Worker Nodes" {
        component [Kubelet] as kubelet
        component [Kube-proxy] as proxy
        package "Pods" {
            component [Container 1] as c1
            component [Container 2] as c2
        }
    }
}

api --> etcd : 儲存狀態
api --> cm : 控制迴圈
api --> sched : 調度決策
api --> kubelet : 指令下達
kubelet --> c1
kubelet --> c2
proxy --> c1 : 網路代理
proxy --> c2

note right of api
  核心 API 入口
  所有操作經由此處
end note

@enduml

圖表翻譯:

此圖示展示了 Operator 的調和邏輯流程。首先檢查是否存在自訂資源,若存在則進行驗證。若驗證透過,則檢查是否需要變更狀態,並進行相應的變更。最後結束調和流程。

資源驗證

資源驗證是 Operator 中非常重要的一環。驗證邏輯可以分為多個層級:

  1. OpenAPI 驗證:在 CRD 定義中指定驗證規則,防止無效資源進入叢集狀態。
  2. 驗證 Webhook:透過 Webhook 在 API 伺服器層級進行額外的驗證。
  3. 調和迴圈中的驗證:在調和邏輯中實作額外的驗證邏輯,以處理已存在資源的驗證。
func (r *EGAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 取得 EGApp 資源
    var egapp egplatformv1alpha1.EGApp
    if err := r.Get(ctx, req.NamespacedName, &egapp); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 驗證資源
    if !egapp.IsValid() {
        return ctrl.Result{}, errors.New("invalid EGApp resource")
    }

    // 調和邏輯...
}

程式碼解密:

  • Reconcile 方法是 Operator 調和邏輯的入口點。
  • 首先透過 r.Get 方法取得指定的 EGApp 資源。
  • 若資源不存在,則忽略錯誤並結束調和。
  • 若資源存在,則呼叫 IsValid 方法進行驗證。若驗證失敗,則傳回錯誤。
  • 若驗證透過,則繼續執行調和邏輯。

Controller 實作詳解

在 Kubernetes Operator 的開發過程中,Controller 是核心元件,負責監控自訂資源(Custom Resource, CR)的狀態並確保叢集的實際狀態與預期狀態一致。本文將探討 Controller 的實作細節,並解析其關鍵程式碼。

Reconcile 函式解析

Reconcile 是 Controller 的核心函式,負責調節叢集狀態使其符合自訂資源的定義。以下為關鍵實作:

func (r *EGAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := log.Log.WithValues("EGApp", req.NamespacedName)
    logger.Info("EGApp Reconcile started...")

    // 取得 EGApp CR 例項
    egApp := &egplatformv1alpha1.EGApp{}
    err := r.Get(ctx, req.NamespacedName, egApp)
    if err != nil {
        // 處理資源不存在的情況
        if errors.IsNotFound(err) {
            logger.Info("EGApp resource not found. Object must be deleted")
            return ctrl.Result{}, nil
        }
        logger.Error(err, "Failed to get EGApp")
        return ctrl.Result{}, err
    }

    // 檢查 Deployment 是否存在
    found := &appsv1.Deployment{}
    err = r.Get(ctx, types.NamespacedName{Name: egApp.Name, Namespace: egApp.Namespace}, found)
    if err != nil {
        // 建立新的 Deployment
        dep := r.deploymentForEGApp(egApp)
        logger.Info("Creating a new deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            logger.Error(err, "Failed to create new deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
            return ctrl.Result{}, err
        }
        return ctrl.Result{}, nil
    }

    // 確保 Deployment 的副本數與規格一致
    replicas := egApp.Spec.ReplicaCount
    if *found.Spec.Replicas != replicas {
        found.Spec.Replicas = &replicas
        err = r.Update(ctx, found)
        if err != nil {
            logger.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{}, err
        }
        return ctrl.Result{Requeue: true}, nil
    }

    // 更新 EGApp 狀態中的 Pod 名稱
    podList := &corev1.PodList{}
    listOpts := []client.ListOption{
        client.InNamespace(egApp.Namespace),
        client.MatchingLabels(egApp.GetLabels()),
    }
    if err = r.List(ctx, podList, listOpts...); err != nil {
        logger.Error(err, "Failed to list pods", "egApp.Namespace", egApp.Namespace, "egApp.Name", egApp.Name)
        return ctrl.Result{}, err
    }
    podNames := getPodNames(podList.Items)
    
    if !reflect.DeepEqual(podNames, egApp.Status.Pods) {
        egApp.Status.Pods = podNames
        err := r.Status().Update(ctx, egApp)
        if err != nil {
            logger.Error(err, "Failed to update egApp status")
            return ctrl.Result{}, err
        }
    }

    return ctrl.Result{}, nil
}

內容解密:

  1. Reconcile 流程

    • 首先透過 req.NamespacedName 取得對應的 EGApp 資源例項。
    • 若資源不存在則直接傳回,表示資源已被刪除。
    • 若資源存在,則檢查對應的 Deployment 是否存在。
  2. Deployment 管理

    • 若 Deployment 不存在,則建立新的 Deployment。
    • 若已存在,則檢查其副本數是否與 EGApp 規格中的 ReplicaCount 一致,不一致則更新。
  3. 狀態更新

    • 取得與 Deployment 相關聯的 Pod 清單。
    • 將 Pod 名稱更新至 EGApp 的 Status 子資源中。

Deployment 建立邏輯

func (r *EGAppReconciler) deploymentForEGApp(m *egplatformv1alpha1.EGApp) *appsv1.Deployment {
    ls := m.GetLabels()
    replicas := m.Spec.ReplicaCount
    
    deploy := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      m.Name,
            Namespace: m.Namespace,
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: &replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: ls,
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: ls,
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{{
                        Image: "gcr.io/kuar-demo/kuard-amd64:1",
                        Name:  m.Spec.AppId,
                        Ports: []corev1.ContainerPort{{
                            ContainerPort: 8080,
                            Name:          "http",
                        }},
                    }},
                },
            },
        },
    }
    
    ctrl.SetControllerReference(m, deploy, r.Scheme)
    return deploy
}

內容解密:

  1. Deployment 組態

    • 使用 EGApp 的標籤和規格建立 Deployment。
    • 設定容器映像和埠號組態。
    • 使用 ctrl.SetControllerReference 建立 CR 與 Deployment 的從屬關係。
  2. 重要設計考量

    • 這種從屬關係確保當 EGApp 被刪除時,相關的 Deployment 也會被清理。
    • 未來可將硬編碼的映像檔改為動態組態。

本地測試與佈署

完成 Controller 邏輯後,可以透過以下指令進行本地測試:

$ make run

該指令會:

  1. 生成必要的 CRD 和 RBAC 設定
  2. 編譯並執行 Controller 程式碼
  3. 在本地啟動 metrics 和 health probe 服務