如何從零開發KubernetesOperator?(文末課程福利)

也許能夠將應用熟練的部署到Kubernetes上,但你知道什麼是Operator嗎?Operator是如何工作的?如何構建Operator?這是一個複雜的課題,但幸運的是,自2016年發明以來,已經開發了許多相關工具,可以簡化工程師的生活。
這些工具允許我們將自定義邏輯加入Kubernetes,從而自動化大量任務,而這已經超出了軟體本身功能的範圍。
閒話少說,讓我們深入瞭解更多關於Operator的知識吧!
1
什麼是Operator?
等一下,你知道Kubernetes(或k8s)嗎?簡單介紹一下,這是由谷歌雲開發的“可以在任何地方部署、擴充套件和管理容器應用程式的開源系統”。
大多數人使用Kubernetes的方式是使用原生資源(如Pod、Deployment、Service等)部署應用程式。但是,也可以擴充套件Kubernetes的功能,從而新增滿足特定需求的新業務邏輯,這就是Operator的作用。
Operator的主要目標是將工程師的邏輯轉換為程式碼,以便實現原生Kubernetes無法完成的某些任務的自動化。
負責開發應用程式或服務的工程師對系統應該如何執行、如何部署以及如何在出現問題時做出反應有很深的瞭解。將這些技術知識封裝在程式碼中並自動化操作的能力意味著在可以花費更少的時間處理重複任務,而在重要問題上可以投入更多時間。
例如,可以想象Operator在Kubernetes中部署和維護MySQL、Elasticsearch或Gitlab runner等工具,Operator可以配置這些工具,根據事件調整系統狀態,並對故障做出反應。
聽起來很有趣不是嗎?讓我們動手幹吧。
2
構建Operator
可以使用Kubernetes開發的controller-runtime專案從頭構建Operator,也可以使用最流行的框架之一加速開發週期並降低複雜性(Kubebuilder或OperatorSDK)。因為Kubebuilder框架非常容易使用,文件也很容易閱讀,而且久經考驗,因此我選擇基於Kubebuilder構建。
不管怎樣,這兩個專案目前正在合併為單獨的專案。
設定開發環境
開發Operator需要以下必備工具:
  • Go v1.17.9+
  • Docker 17.03+
  • kubectl v1.11.3+
  • 訪問Kubernetes v1.11.3+叢集(強烈建議使用kind設定自己的本地叢集,它非常容易使用!)
然後安裝kubebuilder:
$ curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH) && chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
如果一切正常,應該會看到類似輸出(版本可能會隨時間發生變化):
$ kubebuilder version

Version: main.version{KubeBuilderVersion:

"3.4.1"

, KubernetesVendor:

"1.23.5"

, GitCommit:

"d59d7882ce95ce5de10238e135ddff31d8ede026"

, BuildDate:

"2022-05-06T13:58:56Z"

, GoOs:

"darwin"

, GoArch:

"amd64"

}
太棒了,現在可以開始了!
構建簡單的Operator
接下來做個小練習,構建一個簡單的foo operator,除了演示Operator的功能之外,沒有實際用處。
執行以下命令初始化新專案,該命令將下載controller-runtime二進位制檔案,併為我們準備好專案。
$ kubebuilder init –domain my.domain –repo my.domain/tutorial
Writing kustomize manifests 

for

 you to edit…
Writing scaffold 

for

 you to edit…
Get controller runtime:
$ go get sigs.k8s.io/[email protected]
go: downloading sigs.k8s.io/controller-runtime v0.11.2
Update dependencies:
$ go mod tidy
go: downloading github.com/onsi/gomega v1.17.0
下面是專案結構(注意這是一個Go專案):
$ ls -a

-rw——-   1 leovct  staff    129 Jun 30 16:08 .dockerignore

-rw——-   1 leovct  staff    367 Jun 30 16:08 .gitignore

-rw——-   1 leovct  staff    776 Jun 30 16:08 Dockerfile

-rw——-   1 leovct  staff   5029 Jun 30 16:08 Makefile

-rw——-   1 leovct  staff    104 Jun 30 16:08 PROJECT

-rw——-   1 leovct  staff   2718 Jun 30 16:08 README.md

drwx——   6 leovct  staff    192 Jun 30 16:08 config

-rw——-   1 leovct  staff   3218 Jun 30 16:08 go.mod

-rw-r–r–   1 leovct  staff  94801 Jun 30 16:08 go.sum

drwx——   3 leovct  staff     96 Jun 30 16:08 hack

-rw——-   1 leovct  staff   2780 Jun 30 16:08 main.go
我們來看看這個Operator最重要的組成部分:
  • main.go是專案入口,負責設定並執行管理器。
  • config/包含在Kubernetes中部署Operator的manifest。
  • Dockerfile是用於構建管理器映象的容器檔案。
等等,這個管理器元件是什麼玩意兒?
這涉及到部分理論知識,我們稍後再說!
Operator由兩個元件組成,自定義資源定義(CRD,Custom Resource Definition)和控制器(Controller)。
CRD是“Kubernetes自定義型別”或資源藍圖,用於描述其規範和狀態。我們可以定義CRD的例項,稱為自定義資源(CR,Custom Resource)。

圖1. 自定義資源定義(CRD)和自定義資源(CR)
控制器(也稱為控制迴圈)持續監視叢集狀態,並根據事件做出變更,目標是將資源的當前狀態變為使用者在自定義資源規範中定義的期望狀態。

圖2. 控制器操作概要圖示
一般來說,控制器是特定於某種型別的資源的,但也可以對一組不同的資源執行CRUD(建立、讀取、更新和刪除)操作。
在Kubernetes的文件中舉了一個控制器的例子:恆溫器。當我們設定溫度時,告訴恆溫器所需的狀態,房間的實際溫度就是當前的實際狀態,恆溫器透過開啟或關閉空調,使實際狀態更接近預期狀態。
那管理器(manager)呢?該元件的目標是啟動所有控制器,並使控制迴圈共存。假設專案中有兩個CRD,同時有兩個控制器,每個CRD對應一個控制器,管理器將啟動這兩個控制器並使它們共存。
現在我們知道了Operator是如何工作的,可以開始使用Kubebuilder框架建立一個Operator,我們從建立新的API(組/版本)和新的Kind(CRD)開始,當提示建立CRD和控制器時,按yes。
$ kubebuilder create api –group tutorial –version v1 –kind Foo

Create Resource [y/n] y

Create Controller [y/n] y

Writing kustomize manifests 

for

 you to edit…

Writing scaffold 

for

 you to edit…

api/v1/foo_types.go

controllers/foo_controller.go

Update dependencies:

$ go mod tidy

Running make:

$ make generate

mkdir -p /Users/leovct/Documents/tutorial/bin

GOBIN=/Users/leovct/Documents/tutorial/bin go install sigs.k8s.io/controller-tools/cmd/[email protected]

/Users/leovct/Documents/tutorial/bin/controller-gen object:headerFile=

"hack/boilerplate.go.txt"

 paths=

"./…"

接下來是最有意思的部分!我們將定製CRD和控制器來滿足需求,注意看已經建立了兩個新資料夾:
  • api/v1包含Foo CRD
  • controllers包含Foo控制器
自定義CRD和Controller
接下來定製我們可愛的Foo CRD。正如前面所說,這個CRD沒有任何目的,只是簡單展示如何使用Operator在Kubernetes中執行簡單的任務。
Foo CRD在其定義中有name欄位,該欄位指的是Foo正在尋找的朋友的名稱。如果Foo找到了一個朋友(一個和朋友同名的Pod),happy狀態將被設定為true。
package v1
import (

 metav1 

"k8s.io/apimachinery/pkg/apis/meta/v1"

)
// FooSpec defines the desired state of Foo

type

 FooSpec struct {

 // Name of the friend Foo is looking 

for

 Name string `json:

"name"

`

}
// FooStatus defines the observed state of Foo

type

 FooStatus struct {

 // Happy will be 

set

 to 

trueif

 Foo found a friend

 Happy bool `json:

"happy,omitempty"

`

}
//+kubebuilder:object:root=

true

//+kubebuilder:subresource:status
// Foo is the Schema 

for

 the foos API

type

 Foo struct {

 metav1.TypeMeta   `json:

",inline"

`

 metav1.ObjectMeta `json:

"metadata,omitempty"

`
 Spec   FooSpec   `json:

"spec,omitempty"

`

 Status FooStatus `json:

"status,omitempty"

`

}
//+kubebuilder:object:root=

true

// FooList contains a list of Foo

type

 FooList struct {

 metav1.TypeMeta `json:

",inline"

`

 metav1.ListMeta `json:

"metadata,omitempty"

`

 Items           []Foo `json:

"items"

`

}
func 

init

() {

 SchemeBuilder.Register(&Foo{}, &FooList{})

}
接下來實現控制器邏輯。沒什麼複雜的,透過觸發reconciliation請求獲取Foo資源,從而得到Foo的朋友的名稱。然後,列出所有和Foo的朋友同名的Pod。如果找到一個或多個,將Foo的happy狀態更新為true,否則設定為false。
注意,控制器也會對Pod事件做出反應。實際上,如果建立了一個新的Pod,我們希望Foo資源能夠相應更新其狀態。這個方法將在每次發生Pod事件時被觸發(建立、更新或刪除)。然後,只有當Pod名稱是叢集中部署的某個Foo自定義資源的“朋友”時,才觸發Foo控制器的reconciliation迴圈。
package controllers
import (

"context"

 corev1 

"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"

 ctrl 

"sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"

 tutorialv1 

"my.domain/tutorial/api/v1"

)
// FooReconciler reconciles a Foo object

type

 FooReconciler struct {

 client.Client

 Scheme *runtime.Scheme

}
// RBAC permissions to monitor foo custom resources

//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos,verbs=get;list;watch;create;update;patch;delete

//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos/status,verbs=get;update;patch

//+kubebuilder:rbac:groups=tutorial.my.domain,resources=foos/finalizers,verbs=update
// RBAC permissions to monitor pods

//+kubebuilder:rbac:groups=

""

,resources=pods,verbs=get;list;watch
// Reconcile is part of the main kubernetes reconciliation loop 

which

 aims to

// move the current state of the cluster closer to the desired state.

func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

log

 := log.FromContext(ctx)

 log.Info(

"reconciling foo custom resource"

)
 // Get the Foo resource that triggered the reconciliation request

 var foo tutorialv1.Foo

if

 err := r.Get(ctx, req.NamespacedName, &foo); err != nil {

  log.Error(err, 

"unable to fetch Foo"

)

return

 ctrl.Result{}, client.IgnoreNotFound(err)

 }
 // Get pods with the same name as Foo

's friend

 var podList corev1.PodList

 var friendFound bool

 if err := r.List(ctx, &podList); err != nil {

  log.Error(err, "unable to list pods")

 } else {

  for _, item := range podList.Items {

   if item.GetName() == foo.Spec.Name {

    log.Info("pod linked to a foo custom resource found", "name", item.GetName())

    friendFound = true

   }

  }

 }
 // Update Foo'

 happy status

 foo.Status.Happy = friendFound

if

 err := r.Status().Update(ctx, &foo); err != nil {

  log.Error(err, 

"unable to update foo's happy status"

"status"

, friendFound)

return

 ctrl.Result{}, err

 }

 log.Info(

"foo's happy status updated"

"status"

, friendFound)
 log.Info(

"foo custom resource reconciled"

)

return

 ctrl.Result{}, nil

}
// SetupWithManager sets up the controller with the Manager.

func (r *FooReconciler) SetupWithManager(mgr ctrl.Manager) error {

return

 ctrl.NewControllerManagedBy(mgr).

  For(&tutorialv1.Foo{}).

  Watches(

   &source.Kind{Type: &corev1.Pod{}},

   handler.EnqueueRequestsFromMapFunc(r.mapPodsReqToFooReq),

  ).

  Complete(r)

}
func (r *FooReconciler) mapPodsReqToFooReq(obj client.Object) []reconcile.Request {

 ctx := context.Background()

log

 := log.FromContext(ctx)
 // List all the Foo custom resource

 req := []reconcile.Request{}

 var list tutorialv1.FooList

if

 err := r.Client.List(context.TODO(), &list); err != nil {

  log.Error(err, 

"unable to list foo custom resources"

)

 } 

else

 {

  // Only keep Foo custom resources related to the Pod that triggered the reconciliation request

for

 _, item := range list.Items {

if

 item.Spec.Name == obj.

GetName

() {

    req = append(req, reconcile.Request{

     NamespacedName: types.NamespacedName{Name: item.Name, Namespace: item.Namespace},

    })

    log.Info(

"pod linked to a foo custom resource issued an event"

"name"

, obj.GetName())

   }

  }

 }

return

 req

}
我們已經完成了對API定義和控制器的編輯,可以執行以下命令來更新Operator manifest。
$ make manifests

/Users/leovct/Documents/tutorial/bin/controller-gen rbac:roleName=manager-role crd webhook paths=

"./…"

 output:crd:artifacts:config=config/crd/bases
執行Controller
我們使用Kind設定本地Kubernetes叢集,它很容易使用。
首先將CRD安裝到叢集中。
$ make install

/Users/leovct/Documents/tutorial/bin/controller-gen rbac:roleName=manager-role crd webhook paths=

"./…"

 output:crd:artifacts:config=config/crd/bases

kubectl apply -k config/crd

customresourcedefinition.apiextensions.k8s.io/foos.tutorial.my.domain created
可以看到Foo CRD已經建立好了。
$ kubectl get crds

NAME                               CREATED AT

foos.tutorial.my.domain            2022-06-30T17:02:45Z
然後終端中執行控制器。請記住,也可以將其部署為Kubernetes叢集中的deployment。
$ make run

/Users/leovct/Documents/tutorial/bin/controller-gen rbac:roleName=manager-role crd webhook paths=

"./…"

 output:crd:artifacts:config=config/crd/bases

/Users/leovct/Documents/tutorial/bin/controller-gen object:headerFile=

"hack/boilerplate.go.txt"

 paths=

"./…"

go fmt ./…

go vet ./…

go run ./main.go

INFO controller-runtime.metrics Metrics server is starting to listen {

"addr"

":8080"

}

INFO setup starting manager

INFO Starting server {

"path"

"/metrics"

"kind"

"metrics"

"addr"

"[::]:8080"

}

INFO Starting server {

"kind"

"health probe"

"addr"

"[::]:8081"

}

INFO controller.foo Starting EventSource {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"source"

"kind source: *v1.Foo"

}

INFO controller.foo Starting EventSource {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"source"

"kind source: *v1.Pod"

}

INFO controller.foo Starting Controller {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

}

INFO controller.foo Starting workers {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"worker count"

: 1}
如你所見,管理器啟動了,然後Foo控制器也啟動了,控制器現在正在執行並監聽事件!
測試控制器
為了測試是否一切工作正常,我們建立兩個Foo自定義資源以及一些pod,觀察控制器的行為。
首先,在config/samples中建立Foo自定義資源清單,執行以下命令在本地Kubernetes叢集中建立資源。
apiVersion: tutorial.my.domain/v1

kind: Foo

metadata:

  name: foo-01

spec:

  name: jack

apiVersion: tutorial.my.domain/v1

kind: Foo

metadata:

  name: foo-02

spec:

  name: joe

$ kubectl apply -f config/samples

foo.tutorial.my.domain/foo-1 created

foo.tutorial.my.domain/foo-2 created
可以看到控制器為每個Foo自定義資源建立事件觸發了reconciliation迴圈。
INFO controller.foo reconciling foo custom resource {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"name"

"foo-1"

"namespace"

"default"

}

INFO controller.foo foo

's happy status updated {"reconciler group": "tutorial.my.domain", "reconciler kind": "Foo", "name": "foo-1", "namespace": "default", "status": "false"}

INFO controller.foo foo custom resource reconciled {"reconciler group": "tutorial.my.domain", "reconciler kind": "Foo", "name": "foo-1", "namespace": "default"}

INFO controller.foo reconciling foo custom resource {"reconciler group": "tutorial.my.domain", "reconciler kind": "Foo", "name": "foo-2", "namespace": "default"}

INFO controller.foo foo'

s happy status updated {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"name"

"foo-2"

"namespace"

"default"

"status"

"false"

}

INFO controller.foo foo custom resource reconciled {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"name"

"foo-2"

"namespace"

"default"

}
如果檢查Foo自定義資源狀態,可以看到狀態為空,這正是所期望的,目前為止一切正常!
$ kubectl describe foos

Name:         foo-1

Namespace:    default

API Version:  tutorial.my.domain/v1

Kind:         Foo

Metadata:     …

Spec:

  Name:       jack

Status:

Name:         foo-2

Namespace:    default

API Version:  tutorial.my.domain/v1

Kind:         Foo

Metadata:     …

Spec:

  Name:       joe

Status:
接下來我們部署一個叫jack的Pod來觀察系統的反應。
apiVersion: v1

kind: Pod

metadata:

  name: jack

spec:

  containers:

  – name: ubuntu

    image: ubuntu:latest

# Just sleep forever
command

: [ 

"sleep"

 ]

    args: [ 

"infinity"

 ]
Pod部署完成後,應該可以看到控制器對Pod建立事件作出響應,然後按照預期更新第一個Foo自定義資源狀態,可以透過describe Foo自定義資源來驗證。
INFO pod linked to a foo custom resource issued an event {

"name"

"jack"

}

INFO controller.foo reconciling foo custom resource {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"name"

"foo-1"

"namespace"

"default"

}

INFO controller.foo pod linked to a foo custom resource found {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"name"

"foo-1"

"namespace"

"default"

"name"

"jack"

}

INFO controller.foo foo

's happy status updated {"reconciler group": "tutorial.my.domain", "reconciler kind": "Foo", "name": "foo-1", "namespace": "default", "status": true}

INFO controller.foo foo custom resource reconciled {"reconciler group": "tutorial.my.domain", "reconciler kind": "Foo", "name": "foo-1", "namespace": "default"}

我們更新第二個Foo自定義資源規範,將其name欄位的值從joe更改為jack,控制器應該捕獲更新事件並觸發reconciliation迴圈。
INFO controller.foo pod linked to a foo custom resource found {

"reconciler group"

"tutorial.my.domain"

"reconciler kind"

"Foo"

"name"

"foo-2"

"namespace"

"default"

"name"

"jack"

}

INFO controller.foo foo

's happy status updated {"reconciler group": "tutorial.my.domain", "reconciler kind": "Foo", "name": "foo-2", "namespace": "default", "status": true}

INFO controller.foo foo custom resource reconciled {"reconciler group": "tutorial.my.domain", "reconciler kind": "Foo", "name": "foo-2", "namespace": "default"}

Yeah,成功了!我們已經做了足夠多的實驗,你應該明白這是怎麼回事了!如果刪除名為jack的pod,自定義資源的happy狀態將被設定為false。
我們可以確認Operator是正常工作的!最好再編寫一些單元測試和端到端測試,但本文不會覆蓋相關內容。
為自己感到驕傲吧,你已經設計、部署並測試了第一個Operator!恭喜!!
如果需要瀏覽完整程式碼,請訪問:
https://github.com/leovct/kubernetes-operator-tutorial
來源:分散式實驗室,版權歸原作者所有,侵刪
最後給大家準備了內部福利免費公開課【Kubernetes Operator開發
新年倒計時,上硬核乾貨:
💡Operator使用場景
💡開發環境準備
💡kubebuilder 建立項
💡CRD的開發與部署
💡Crontroller開發與部署
今晚20點,線上直播↓↓
帶你從理論到實踐,全面掌握 Operator 開發技能
凡是我的粉絲,皆可預約免費聽
掃描下方二維碼預約
(是掃碼聽課,不是在公眾號後臺回覆,
別看錯了哦)

相關文章