你也許能夠將應用熟練的部署到 Kubernetes 上,但你知道什么是 Operator 嗎?Operator 是如何工作的?如何構建 Operator?這是一個復雜的課題,但幸運的是,自 2016 年發明以來,已經開發了許多相關工具,可以簡化工程師的生活。
這些工具允許我們將自定義邏輯加入 Kubernetes,從而自動化大量任務,而這已經超出了軟件本身功能的范圍。
閑話少說,讓我們深入了解更多關于 Operator 的知識吧!
什么是 Operator?
等一下,你知道 Kubernetes(或 k8s)嗎?簡單介紹一下,這是由谷歌云開發的“可以在任何地方部署、擴展和管理容器應用程序的開源系統”。
大多數人使用 Kubernetes 的方式是使用原生資源(如 Pod、Deployment、Service 等)部署應用程序。但是,也可以擴展 Kubernetes 的功能,從而添加滿足特定需求的新業務邏輯,這就是 Operator 的作用。Operator 的主要目標是將工程師的邏輯轉換為代碼,以便實現原生 Kubernetes 無法完成的某些任務的自動化。
負責開發應用程序或服務的工程師對系統應該如何運行、如何部署以及如何在出現問題時做出反應有很深的了解。將這些技術知識封裝在代碼中并自動化操作的能力意味著在可以花費更少的時間處理重復任務,而在重要問題上可以投入更多時間。
例如,可以想象 Operator 在 Kubernetes 中部署和維護 MySQL、Elasticsearch 或 Gitlab runner 等工具,Operator 可以配置這些工具,根據事件調整系統狀態,并對故障做出反應。
聽起來很有趣不是嗎?讓我們動手干吧。
構建 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-okubebuilderhttps://go.kubebuilder.io/dl/latest/$(goenvGOOS)/$(goenvGOARCH)&&chmod+xkubebuilder&&mvkubebuilder/usr/local/bin/
如果一切正常,應該會看到類似輸出(版本可能會隨時間發生變化):
$kubebuilderversion Version:main.version{KubeBuilderVersion:"3.4.1",KubernetesVendor:"1.23.5",GitCommit:"d59d7882ce95ce5de10238e135ddff31d8ede026",BuildDate:"2022-05-06T1356Z",GoOs:"darwin",GoArch:"amd64"}
太棒了,現在可以開始了!
「構建簡單的 Operator」
接下來做個小練習,構建一個簡單的 foo operator,除了演示 Operator 的功能之外,沒有實際用處。運行以下命令初始化新項目,該命令將下載 controller-runtime 二進制文件,并為我們準備好項目。
$kubebuilderinit--domainmy.domain--repomy.domain/tutorial Writingkustomizemanifestsforyoutoedit... Writingscaffoldforyoutoedit... Getcontrollerruntime: $gogetsigs.k8s.io/controller-runtime@v0.11.2 go:downloadingsigs.k8s.io/controller-runtimev0.11.2 ... Updatedependencies: $gomodtidy go:downloadinggithub.com/onsi/gomegav1.17.0 ...
下面是項目結構(注意這是一個 Go 項目):
$ls-a -rw-------1leovctstaff129Jun3016:08.dockerignore -rw-------1leovctstaff367Jun3016:08.gitignore -rw-------1leovctstaff776Jun3016:08Dockerfile -rw-------1leovctstaff5029Jun3016:08Makefile -rw-------1leovctstaff104Jun3016:08PROJECT -rw-------1leovctstaff2718Jun3016:08README.md drwx------6leovctstaff192Jun3016:08config -rw-------1leovctstaff3218Jun3016:08go.mod -rw-r--r--1leovctstaff94801Jun3016:08go.sum drwx------3leovctstaff96Jun3016:08hack -rw-------1leovctstaff2780Jun3016:08main.go
我們來看看這個 Operator 最重要的組成部分:
main.go 是項目入口,負責設置并運行管理器。
config/包含在 Kubernetes 中部署 Operator 的 manifest。
Dockerfile 是用于構建管理器鏡像的容器文件。
等等,這個管理器組件是什么玩意兒?
這涉及到部分理論知識,我們稍后再說!
Operator 由兩個組件組成,自定義資源定義(CRD,Custom Resource Definition)和控制器(Controller)。
CRD 是“Kubernetes 自定義類型”或資源藍圖,用于描述其規范和狀態。我們可以定義 CRD 的實例,稱為自定義資源(CR,Custom Resource)。
控制器(也稱為控制循環)持續監視集群狀態,并根據事件做出變更,目標是將資源的當前狀態變為用戶在自定義資源規范中定義的期望狀態。一般來說,控制器是特定于某種類型的資源的,但也可以對一組不同的資源執行 CRUD(創建、讀取、更新和刪除)操作。
在 Kubernetes 的文檔中舉了一個控制器的例子:恒溫器。當我們設置溫度時,告訴恒溫器所需的狀態,房間的實際溫度就是當前的實際狀態,恒溫器通過打開或關閉空調,使實際狀態更接近預期狀態。
那管理器(manager)呢?該組件的目標是啟動所有控制器,并使控制循環共存。假設項目中有兩個 CRD,同時有兩個控制器,每個 CRD 對應一個控制器,管理器將啟動這兩個控制器并使它們共存。
現在我們知道了 Operator 是如何工作的,可以開始使用 Kubebuilder 框架創建一個 Operator,我們從創建新的 API(組/版本)和新的 Kind(CRD)開始,當提示創建 CRD 和控制器時,按 yes。
$kubebuildercreateapi--grouptutorial--versionv1--kindFoo CreateResource[y/n]y CreateController[y/n]y Writingkustomizemanifestsforyoutoedit... Writingscaffoldforyoutoedit... api/v1/foo_types.go controllers/foo_controller.go Updatedependencies: $gomodtidy Runningmake: $makegenerate mkdir-p/Users/leovct/Documents/tutorial/bin GOBIN=/Users/leovct/Documents/tutorial/bingoinstallsigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0 /Users/leovct/Documents/tutorial/bin/controller-genobject: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。
packagev1 import( metav1"k8s.io/apimachinery/pkg/apis/meta/v1" ) //FooSpecdefinesthedesiredstateofFoo typeFooSpecstruct{ //NameofthefriendFooislookingfor Namestring`json:"name"` } //FooStatusdefinestheobservedstateofFoo typeFooStatusstruct{ //HappywillbesettotrueifFoofoundafriend Happybool`json:"happy,omitempty"` } //+kubebuilderroot=true //+kubebuilderstatus //FooistheSchemaforthefoosAPI typeFoostruct{ metav1.TypeMeta`json:",inline"` metav1.ObjectMeta`json:"metadata,omitempty"` SpecFooSpec`json:"spec,omitempty"` StatusFooStatus`json:"status,omitempty"` } //+kubebuilderroot=true //FooListcontainsalistofFoo typeFooListstruct{ metav1.TypeMeta`json:",inline"` metav1.ListMeta`json:"metadata,omitempty"` Items[]Foo`json:"items"` } funcinit(){ SchemeBuilder.Register(&Foo{},&FooList{}) }
接下來實現控制器邏輯。沒什么復雜的,通過觸發 reconciliation 請求獲取 Foo 資源,從而得到 Foo 的朋友的名稱。然后,列出所有和 Foo 的朋友同名的 Pod。如果找到一個或多個,將 Foo 的 happy 狀態更新為 true,否則設置為 false。
注意,控制器也會對 Pod 事件做出反應。實際上,如果創建了一個新的 Pod,我們希望 Foo 資源能夠相應更新其狀態。這個方法將在每次發生 Pod 事件時被觸發(創建、更新或刪除)。然后,只有當 Pod 名稱是集群中部署的某個 Foo 自定義資源的“朋友”時,才觸發 Foo 控制器的 reconciliation 循環。
packagecontrollers 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" ) //FooReconcilerreconcilesaFooobject typeFooReconcilerstruct{ client.Client Scheme*runtime.Scheme } //RBACpermissionstomonitorfoocustomresources //+kubebuildergroups=tutorial.my.domain,resources=foos,verbs=get;list;watch;create;update;patch;delete //+kubebuildergroups=tutorial.my.domain,resources=foos/status,verbs=get;update;patch //+kubebuildergroups=tutorial.my.domain,resources=foos/finalizers,verbs=update //RBACpermissionstomonitorpods //+kubebuildergroups="",resources=pods,verbs=get;list;watch //Reconcileispartofthemainkubernetesreconciliationloopwhichaimsto //movethecurrentstateoftheclusterclosertothedesiredstate. func(r*FooReconciler)Reconcile(ctxcontext.Context,reqctrl.Request)(ctrl.Result,error){ log:=log.FromContext(ctx) log.Info("reconcilingfoocustomresource") //GettheFooresourcethattriggeredthereconciliationrequest varfootutorialv1.Foo iferr:=r.Get(ctx,req.NamespacedName,&foo);err!=nil{ log.Error(err,"unabletofetchFoo") returnctrl.Result{},client.IgnoreNotFound(err) } //GetpodswiththesamenameasFoo'sfriend varpodListcorev1.PodList varfriendFoundbool iferr:=r.List(ctx,&podList);err!=nil{ log.Error(err,"unabletolistpods") }else{ for_,item:=rangepodList.Items{ ifitem.GetName()==foo.Spec.Name{ log.Info("podlinkedtoafoocustomresourcefound","name",item.GetName()) friendFound=true } } } //UpdateFoo'happystatus foo.Status.Happy=friendFound iferr:=r.Status().Update(ctx,&foo);err!=nil{ log.Error(err,"unabletoupdatefoo'shappystatus","status",friendFound) returnctrl.Result{},err } log.Info("foo'shappystatusupdated","status",friendFound) log.Info("foocustomresourcereconciled") returnctrl.Result{},nil } //SetupWithManagersetsupthecontrollerwiththeManager. func(r*FooReconciler)SetupWithManager(mgrctrl.Manager)error{ returnctrl.NewControllerManagedBy(mgr). For(&tutorialv1.Foo{}). Watches( &source.Kind{Type:&corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(r.mapPodsReqToFooReq), ). Complete(r) } func(r*FooReconciler)mapPodsReqToFooReq(objclient.Object)[]reconcile.Request{ ctx:=context.Background() log:=log.FromContext(ctx) //ListalltheFoocustomresource req:=[]reconcile.Request{} varlisttutorialv1.FooList iferr:=r.Client.List(context.TODO(),&list);err!=nil{ log.Error(err,"unabletolistfoocustomresources") }else{ //OnlykeepFoocustomresourcesrelatedtothePodthattriggeredthereconciliationrequest for_,item:=rangelist.Items{ ifitem.Spec.Name==obj.GetName(){ req=append(req,reconcile.Request{ NamespacedName:types.NamespacedName{Name:item.Name,Namespace:item.Namespace}, }) log.Info("podlinkedtoafoocustomresourceissuedanevent","name",obj.GetName()) } } } returnreq }
我們已經完成了對 API 定義和控制器的編輯,可以運行以下命令來更新 Operator manifest。
$makemanifests /Users/leovct/Documents/tutorial/bin/controller-genrbac:roleName=manager-rolecrdwebhookpaths="./..."outputartifacts:config=config/crd/bases
「運行 Controller」
我們使用 Kind 設置本地 Kubernetes 集群,它很容易使用。
首先將 CRD 安裝到集群中。
$makeinstall /Users/leovct/Documents/tutorial/bin/controller-genrbac:roleName=manager-rolecrdwebhookpaths="./..."outputartifacts:config=config/crd/bases kubectlapply-kconfig/crd customresourcedefinition.apiextensions.k8s.io/foos.tutorial.my.domaincreated
可以看到 Foo CRD 已經創建好了。
$kubectlgetcrds NAMECREATEDAT foos.tutorial.my.domain2022-06-30T1745Z
然后終端中運行控制器。請記住,也可以將其部署為 Kubernetes 集群中的 deployment。
$makerun /Users/leovct/Documents/tutorial/bin/controller-genrbac:roleName=manager-rolecrdwebhookpaths="./..."outputartifacts:config=config/crd/bases /Users/leovct/Documents/tutorial/bin/controller-genobject:headerFile="hack/boilerplate.go.txt"paths="./..." gofmt./... govet./... gorun./main.go INFOcontroller-runtime.metricsMetricsserverisstartingtolisten{"addr":":8080"} INFOsetupstartingmanager INFOStartingserver{"path":"/metrics","kind":"metrics","addr":"[::]:8080"} INFOStartingserver{"kind":"healthprobe","addr":"[::]:8081"} INFOcontroller.fooStartingEventSource{"reconcilergroup":"tutorial.my.domain","reconcilerkind":"Foo","source":"kindsource:*v1.Foo"} INFOcontroller.fooStartingEventSource{"reconcilergroup":"tutorial.my.domain","reconcilerkind":"Foo","source":"kindsource:*v1.Pod"} INFOcontroller.fooStartingController{"reconcilergroup":"tutorial.my.domain","reconcilerkind":"Foo"} INFOcontroller.fooStartingworkers{"reconcilergroup":"tutorial.my.domain","reconcilerkind":"Foo","workercount":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 $kubectlapply-fconfig/samples foo.tutorial.my.domain/foo-1created foo.tutorial.my.domain/foo-2created
可以看到控制器為每個 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 自定義資源狀態,可以看到狀態為空,這正是所期望的,目前為止一切正常!
$kubectldescribefoos Name:foo-1 Namespace:default APIVersion:tutorial.my.domain/v1 Kind:Foo Metadata:... Spec: Name:jack Status: Name:foo-2 Namespace:default APIVersion: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 #Justsleepforever 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!恭喜!!
如果需要瀏覽完整代碼,請訪問GitHub[1]:
更多工作
我們已經看到如何創建非常基本的 Kubernetes operator,但遠非完美,還有很多地方需要改善,下面是可以探索的主題列表:
優化事件過濾(有時,事件會被提交兩次……)。
完善 RBAC 權限。
改進日志記錄系統。
當 operator 更新資源時,觸發 Kubernetes 事件。
獲取 Foo 自定義資源時添加自定義字段(也許顯示 happy 狀態?)
編寫單元測試和端到端測試。
通過這個列表,可以深入挖掘這一主題。
審核編輯:湯梓紅
-
代碼
+關注
關注
30文章
4827瀏覽量
69053 -
MySQL
+關注
關注
1文章
829瀏覽量
26743 -
開發環境
+關注
關注
1文章
230瀏覽量
16697 -
kubernetes
+關注
關注
0文章
227瀏覽量
8752
原文標題:通過例子介紹如何從零開發 Kubernetes Operator
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論