你也許能夠?qū)?yīng)用熟練的部署到 Kubernetes 上,但你知道什么是 Operator 嗎?Operator 是如何工作的?如何構(gòu)建 Operator?這是一個(gè)復(fù)雜的課題,但幸運(yùn)的是,自 2016 年發(fā)明以來,已經(jīng)開發(fā)了許多相關(guān)工具,可以簡化工程師的生活。
這些工具允許我們將自定義邏輯加入 Kubernetes,從而自動(dòng)化大量任務(wù),而這已經(jīng)超出了軟件本身功能的范圍。
閑話少說,讓我們深入了解更多關(guān)于 Operator 的知識(shí)吧!
什么是 Operator?
等一下,你知道 Kubernetes(或 k8s)嗎?簡單介紹一下,這是由谷歌云開發(fā)的“可以在任何地方部署、擴(kuò)展和管理容器應(yīng)用程序的開源系統(tǒng)”。
大多數(shù)人使用 Kubernetes 的方式是使用原生資源(如 Pod、Deployment、Service 等)部署應(yīng)用程序。但是,也可以擴(kuò)展 Kubernetes 的功能,從而添加滿足特定需求的新業(yè)務(wù)邏輯,這就是 Operator 的作用。Operator 的主要目標(biāo)是將工程師的邏輯轉(zhuǎn)換為代碼,以便實(shí)現(xiàn)原生 Kubernetes 無法完成的某些任務(wù)的自動(dòng)化。
負(fù)責(zé)開發(fā)應(yīng)用程序或服務(wù)的工程師對(duì)系統(tǒng)應(yīng)該如何運(yùn)行、如何部署以及如何在出現(xiàn)問題時(shí)做出反應(yīng)有很深的了解。將這些技術(shù)知識(shí)封裝在代碼中并自動(dòng)化操作的能力意味著在可以花費(fèi)更少的時(shí)間處理重復(fù)任務(wù),而在重要問題上可以投入更多時(shí)間。
例如,可以想象 Operator 在 Kubernetes 中部署和維護(hù) MySQL、Elasticsearch 或 Gitlab runner 等工具,Operator 可以配置這些工具,根據(jù)事件調(diào)整系統(tǒng)狀態(tài),并對(duì)故障做出反應(yīng)。
聽起來很有趣不是嗎?讓我們動(dòng)手干吧。
構(gòu)建 Operator
可以使用 Kubernetes 開發(fā)的 controller-runtime 項(xiàng)目從頭構(gòu)建 Operator,也可以使用最流行的框架之一加速開發(fā)周期并降低復(fù)雜性(Kubebuilder 或 OperatorSDK)。因?yàn)?Kubebuilder 框架非常容易使用,文檔也很容易閱讀,而且久經(jīng)考驗(yàn),因此我選擇基于 Kubebuilder 構(gòu)建。
不管怎樣,這兩個(gè)項(xiàng)目目前正在合并為單獨(dú)的項(xiàng)目。
「設(shè)置開發(fā)環(huán)境」
開發(fā) Operator 需要以下必備工具:
Go v1.17.9+
Docker 17.03+
kubectl v1.11.3+
訪問 Kubernetes v1.11.3+集群(強(qiáng)烈建議使用 kind 設(shè)置自己的本地集群,它非常容易使用!)
然后安裝 kubebuilder:
$curl-L-okubebuilderhttps://go.kubebuilder.io/dl/latest/$(goenvGOOS)/$(goenvGOARCH)&&chmod+xkubebuilder&&mvkubebuilder/usr/local/bin/
如果一切正常,應(yīng)該會(huì)看到類似輸出(版本可能會(huì)隨時(shí)間發(fā)生變化):
$kubebuilderversion Version:main.version{KubeBuilderVersion:"3.4.1",KubernetesVendor:"1.23.5",GitCommit:"d59d7882ce95ce5de10238e135ddff31d8ede026",BuildDate:"2022-05-06T1356Z",GoOs:"darwin",GoArch:"amd64"}
太棒了,現(xiàn)在可以開始了!
「構(gòu)建簡單的 Operator」
接下來做個(gè)小練習(xí),構(gòu)建一個(gè)簡單的 foo operator,除了演示 Operator 的功能之外,沒有實(shí)際用處。運(yùn)行以下命令初始化新項(xiàng)目,該命令將下載 controller-runtime 二進(jìn)制文件,并為我們準(zhǔn)備好項(xiàng)目。
$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 ...
下面是項(xiàng)目結(jié)構(gòu)(注意這是一個(gè) Go 項(xiàng)目):
$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
我們來看看這個(gè) Operator 最重要的組成部分:
main.go 是項(xiàng)目入口,負(fù)責(zé)設(shè)置并運(yùn)行管理器。
config/包含在 Kubernetes 中部署 Operator 的 manifest。
Dockerfile 是用于構(gòu)建管理器鏡像的容器文件。
等等,這個(gè)管理器組件是什么玩意兒?
這涉及到部分理論知識(shí),我們稍后再說!
Operator 由兩個(gè)組件組成,自定義資源定義(CRD,Custom Resource Definition)和控制器(Controller)。
CRD 是“Kubernetes 自定義類型”或資源藍(lán)圖,用于描述其規(guī)范和狀態(tài)。我們可以定義 CRD 的實(shí)例,稱為自定義資源(CR,Custom Resource)。
控制器(也稱為控制循環(huán))持續(xù)監(jiān)視集群狀態(tài),并根據(jù)事件做出變更,目標(biāo)是將資源的當(dāng)前狀態(tài)變?yōu)橛脩粼谧远x資源規(guī)范中定義的期望狀態(tài)。一般來說,控制器是特定于某種類型的資源的,但也可以對(duì)一組不同的資源執(zhí)行 CRUD(創(chuàng)建、讀取、更新和刪除)操作。
在 Kubernetes 的文檔中舉了一個(gè)控制器的例子:恒溫器。當(dāng)我們設(shè)置溫度時(shí),告訴恒溫器所需的狀態(tài),房間的實(shí)際溫度就是當(dāng)前的實(shí)際狀態(tài),恒溫器通過打開或關(guān)閉空調(diào),使實(shí)際狀態(tài)更接近預(yù)期狀態(tài)。
那管理器(manager)呢?該組件的目標(biāo)是啟動(dòng)所有控制器,并使控制循環(huán)共存。假設(shè)項(xiàng)目中有兩個(gè) CRD,同時(shí)有兩個(gè)控制器,每個(gè) CRD 對(duì)應(yīng)一個(gè)控制器,管理器將啟動(dòng)這兩個(gè)控制器并使它們共存。
現(xiàn)在我們知道了 Operator 是如何工作的,可以開始使用 Kubebuilder 框架創(chuàng)建一個(gè) Operator,我們從創(chuàng)建新的 API(組/版本)和新的 Kind(CRD)開始,當(dāng)提示創(chuàng)建 CRD 和控制器時(shí),按 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 和控制器來滿足需求,注意看已經(jīng)創(chuàng)建了兩個(gè)新文件夾:
api/v1 包含 Foo CRD
controllers 包含 Foo 控制器
「自定義 CRD 和 Controller」
接下來定制我們可愛的 Foo CRD。正如前面所說,這個(gè) CRD 沒有任何目的,只是簡單展示如何使用 Operator 在 Kubernetes 中執(zhí)行簡單的任務(wù)。
Foo CRD 在其定義中有 name 字段,該字段指的是 Foo 正在尋找的朋友的名稱。如果 Foo 找到了一個(gè)朋友(一個(gè)和朋友同名的 Pod),happy 狀態(tài)將被設(shè)置為 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{})
}
接下來實(shí)現(xiàn)控制器邏輯。沒什么復(fù)雜的,通過觸發(fā) reconciliation 請(qǐng)求獲取 Foo 資源,從而得到 Foo 的朋友的名稱。然后,列出所有和 Foo 的朋友同名的 Pod。如果找到一個(gè)或多個(gè),將 Foo 的 happy 狀態(tài)更新為 true,否則設(shè)置為 false。
注意,控制器也會(huì)對(duì) Pod 事件做出反應(yīng)。實(shí)際上,如果創(chuàng)建了一個(gè)新的 Pod,我們希望 Foo 資源能夠相應(yīng)更新其狀態(tài)。這個(gè)方法將在每次發(fā)生 Pod 事件時(shí)被觸發(fā)(創(chuàng)建、更新或刪除)。然后,只有當(dāng) Pod 名稱是集群中部署的某個(gè) Foo 自定義資源的“朋友”時(shí),才觸發(fā) Foo 控制器的 reconciliation 循環(huán)。
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
}
我們已經(jīng)完成了對(duì) API 定義和控制器的編輯,可以運(yùn)行以下命令來更新 Operator manifest。
$makemanifests /Users/leovct/Documents/tutorial/bin/controller-genrbac:roleName=manager-rolecrdwebhookpaths="./..."outputartifacts:config=config/crd/bases
「運(yùn)行 Controller」
我們使用 Kind 設(shè)置本地 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 已經(jīng)創(chuàng)建好了。
$kubectlgetcrds NAMECREATEDAT foos.tutorial.my.domain2022-06-30T1745Z
然后終端中運(yùn)行控制器。請(qǐng)記住,也可以將其部署為 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}
如你所見,管理器啟動(dòng)了,然后 Foo 控制器也啟動(dòng)了,控制器現(xiàn)在正在運(yùn)行并監(jiān)聽事件!
「測試控制器」
為了測試是否一切工作正常,我們創(chuàng)建兩個(gè) Foo 自定義資源以及一些 pod,觀察控制器的行為。
首先,在 config/samples 中創(chuàng)建 Foo 自定義資源清單,運(yùn)行以下命令在本地 Kubernetes 集群中創(chuàng)建資源。
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
可以看到控制器為每個(gè) Foo 自定義資源創(chuàng)建事件觸發(fā)了 reconciliation 循環(huán)。
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 自定義資源狀態(tài),可以看到狀態(tài)為空,這正是所期望的,目前為止一切正常!
$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:
接下來我們部署一個(gè)叫 jack 的 Pod 來觀察系統(tǒng)的反應(yīng)。
apiVersion:v1 kind:Pod metadata: name:jack spec: containers: -name:ubuntu image:ubuntu:latest #Justsleepforever command:["sleep"] args:["infinity"]
Pod 部署完成后,應(yīng)該可以看到控制器對(duì) Pod 創(chuàng)建事件作出響應(yīng),然后按照預(yù)期更新第一個(gè) Foo 自定義資源狀態(tài),可以通過 describe Foo 自定義資源來驗(yàn)證。
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"}
我們更新第二個(gè) Foo 自定義資源規(guī)范,將其 name 字段的值從 joe 更改為 jack,控制器應(yīng)該捕獲更新事件并觸發(fā) reconciliation 循環(huán)。
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,成功了!我們已經(jīng)做了足夠多的實(shí)驗(yàn),你應(yīng)該明白這是怎么回事了!如果刪除名為 jack 的 pod,自定義資源的 happy 狀態(tài)將被設(shè)置為 false。
我們可以確認(rèn) Operator 是正常工作的!最好再編寫一些單元測試和端到端測試,但本文不會(huì)覆蓋相關(guān)內(nèi)容。
為自己感到驕傲吧,你已經(jīng)設(shè)計(jì)、部署并測試了第一個(gè) Operator!恭喜!!
如果需要瀏覽完整代碼,請(qǐng)?jiān)L問GitHub[1]:
更多工作
我們已經(jīng)看到如何創(chuàng)建非常基本的 Kubernetes operator,但遠(yuǎn)非完美,還有很多地方需要改善,下面是可以探索的主題列表:
優(yōu)化事件過濾(有時(shí),事件會(huì)被提交兩次……)。
完善 RBAC 權(quán)限。
改進(jìn)日志記錄系統(tǒng)。
當(dāng) operator 更新資源時(shí),觸發(fā) Kubernetes 事件。
獲取 Foo 自定義資源時(shí)添加自定義字段(也許顯示 happy 狀態(tài)?)
編寫單元測試和端到端測試。
通過這個(gè)列表,可以深入挖掘這一主題。
審核編輯:湯梓紅
-
代碼
+關(guān)注
關(guān)注
30文章
4967瀏覽量
73954 -
MySQL
+關(guān)注
關(guān)注
1文章
905瀏覽量
29516 -
開發(fā)環(huán)境
+關(guān)注
關(guān)注
1文章
270瀏覽量
17637 -
kubernetes
+關(guān)注
關(guān)注
0文章
263瀏覽量
9492
原文標(biāo)題:通過例子介紹如何從零開發(fā) Kubernetes Operator
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
With mismatch in relational operator
ESP32-DU1906編譯失敗報(bào)Error: missing binary operator before token "("怎么解決?
placement new詳解
"Eclipse + AVR插件編譯類時(shí),報(bào)undefined reference to `operator delete(void*, unsigned int)‘"
什么是Operator?Operator是如何工作的?如何構(gòu)建Operator?
評(píng)論