介紹
kubernetes operator是通過(guò)連接主API并watch時(shí)間的一組進(jìn)程,一般會(huì)watch有限的資源類型。
當(dāng)相關(guān)(所watch的)event觸發(fā)的時(shí)候,operator做出響應(yīng)并執(zhí)行具體的動(dòng)作。這可能僅限于與主 API 交互,但通常會(huì)涉及在其他一些系統(tǒng)上執(zhí)行某些操作(可以是集群中或集群外資源)。
Operators是控制器的集合,并且每個(gè)控制器watch了指定的資源類型。當(dāng)被watched的資源時(shí)間觸發(fā)的時(shí)候,reconcile cycle(直譯:調(diào)諧循環(huán),下文均使用reconcile cycle)也將隨之啟動(dòng)。
在執(zhí)行reconcile cycle期間,控制器有責(zé)任檢查當(dāng)前狀態(tài)是否與被watched資源描述的期望狀態(tài)相匹配。有趣的是,根據(jù)設(shè)計(jì),時(shí)間并不會(huì)傳遞到reconcile cycle中,這將會(huì)強(qiáng)制地讓你去考慮實(shí)例的整個(gè)狀態(tài)。這種方法被稱為基于水平觸發(fā)而不是基于邊緣觸發(fā)(level-based, as opposed to edge-based)。這源自于電子電路的設(shè)計(jì),水平觸發(fā)是接收event(例如中斷)并對(duì)狀態(tài)做出反應(yīng)的理念,而基于邊緣的觸發(fā)是接收event并對(duì)狀態(tài)變化做出反應(yīng)的理念。
水平觸發(fā)雖說(shuō)效率較低,因?yàn)樗鼜?qiáng)制重新評(píng)估完整的狀態(tài),而不是僅僅關(guān)注改變了什么,但在信號(hào)可能丟失或多次重復(fù)傳輸?shù)膹?fù)雜不可靠環(huán)境中,這種方式被認(rèn)為是更適用的。
這種設(shè)計(jì)的選擇會(huì)影響我們編寫控制器代碼的方式。
與此討論相關(guān)的還有對(duì) API 請(qǐng)求生命周期的理解。下圖提供了一個(gè)高層次的總結(jié):
當(dāng)向API服務(wù)器發(fā)送請(qǐng)求時(shí),特別是對(duì)于創(chuàng)建和刪除請(qǐng)求,它們會(huì)經(jīng)歷上圖所示的階段。需要注意的是,也可以指定webhook來(lái)執(zhí)行請(qǐng)求的更改和驗(yàn)證。如果operator引入了CRD(custom resource definition),我們可能還必須定義這些webhook。一般來(lái)說(shuō),operator進(jìn)程會(huì)開放一個(gè)端口來(lái)來(lái)實(shí)現(xiàn)webhook endpoint。
本文介紹了一系列在使用Operator SDK來(lái)設(shè)計(jì)和開發(fā)Operator時(shí)需要牢記的最佳實(shí)踐。
如果你的operator引入了一個(gè)新的CRD,Operator SDK將會(huì)協(xié)助你來(lái)搭建。為確保您的 CRD 符合 Kubernetes 擴(kuò)展 API 的最佳實(shí)踐,請(qǐng)遵循這些約定。
文中所提到的所有的最佳實(shí)踐都在operator-utils代碼庫(kù)中,并以可運(yùn)行的例子體現(xiàn)。在你的operator項(xiàng)目中,也可以將operator-utils以library的方式導(dǎo)入,以此提供給你一些有用的工具。
最后,這組編寫operator的最佳實(shí)踐僅代表我的個(gè)人觀點(diǎn),不應(yīng)被視為Red Hat的官方最佳實(shí)踐。
創(chuàng)建 watches
正如我們所說(shuō),控制器watch著資源events。
watch是一種接收某種類型(核心類型或CRD)的機(jī)制。一般通過(guò)指定以下內(nèi)容來(lái)創(chuàng)建watch機(jī)制:
想要watch的資源類型
handler。handler將被監(jiān)視類型上的events映射到一個(gè)或多個(gè)調(diào)用協(xié)調(diào)周期的實(shí)例。監(jiān)視類型和實(shí)例類型不必相同。
predicate。predicate是一組能夠過(guò)濾我們感興趣的events且可自定義的函數(shù)。
下圖記錄了以上提及的內(nèi)容:
通常來(lái)說(shuō),同一類型(kind)開啟多個(gè)watch是可行的,因?yàn)閣atch是多路復(fù)用的。
你也應(yīng)該盡可能多地嘗試過(guò)濾event。這邊有個(gè)predicate例子,用來(lái)過(guò)濾secret資源上的event。這里你只對(duì)類型為TLS的secret資源event感興趣:
isAnnotatedSecret:=predicate.Funcs{ UpdateFunc:func(eevent.UpdateEvent)bool{ oldSecret,ok:=e.ObjectOld.(*corev1.Secret) if!ok{ returnfalse } newSecret,ok:=e.ObjectNew.(*corev1.Secret) if!ok{ returnfalse } ifnewSecret.Type!=util.TLSSecret{ returnfalse } oldValue,_:=e.MetaOld.GetAnnotations()[certInfoAnnotation] newValue,_:=e.MetaNew.GetAnnotations()[certInfoAnnotation] old:=oldValue=="true" new:=newValue=="true" //ifthecontenthaschangedwetriggeriftheannotationisthere if!reflect.DeepEqual(newSecret.Data[util.Cert],oldSecret.Data[util.Cert])|| !reflect.DeepEqual(newSecret.Data[util.CA],oldSecret.Data[util.CA]){ returnnew } //otherwisewetriggeriftheannotationhaschanged returnold!=new }, CreateFunc:func(eevent.CreateEvent)bool{ secret,ok:=e.Object.(*corev1.Secret) if!ok{ returnfalse } ifsecret.Type!=util.TLSSecret{ returnfalse } value,_:=e.Meta.GetAnnotations()[certInfoAnnotation] returnvalue=="true" }, }
一個(gè)非常常見的模式是觀察我們創(chuàng)建(和我們擁有)資源上的events,并且定期在擁有這些資源的CR上執(zhí)行reconcile cycle。為此,你可以使用EnqueueRequestForOwner handler,按照如下方式完成:
err=c.Watch(&source.Kind{Type:&examplev1alpha1.MyControlledType{}},&handler.EnqueueRequestForOwner{})
另一種不太常用的情況是將一個(gè)events傳播到多個(gè)資源上。考慮一種情況,一個(gè)控制器注入了TLS secret的路由。同一個(gè)命名空間中的多個(gè)路由可以指向同一個(gè)secret。如果secret發(fā)生了改變,我們需要更新所有路由。因此,我們需要在secret類型上創(chuàng)建一種watch機(jī)制,處理程序如下所示:
typeenqueueRequestForReferecingRoutesstruct{ client.Client } //triggerarouterreconcileeventforthoseroutesthatreferencethissecret func(e*enqueueRequestForReferecingRoutes)Create(evtevent.CreateEvent,qworkqueue.RateLimitingInterface){ routes,_:=matchSecret(e.Client,types.NamespacedName{ Name:evt.Meta.GetName(), Namespace:evt.Meta.GetNamespace(), }) for_,route:=rangeroutes{ q.Add(reconcile.Request{NamespacedName:types.NamespacedName{ Namespace:route.GetNamespace(), Name:route.GetName(), }}) } } //UpdateimplementsEventHandler //triggerarouterreconcileeventforthoseroutesthatreferencethissecret func(e*enqueueRequestForReferecingRoutes)Update(evtevent.UpdateEvent,qworkqueue.RateLimitingInterface){ routes,_:=matchSecret(e.Client,types.NamespacedName{ Name:evt.MetaNew.GetName(), Namespace:evt.MetaNew.GetNamespace(), }) for_,route:=rangeroutes{ q.Add(reconcile.Request{NamespacedName:types.NamespacedName{ Namespace:route.GetNamespace(), Name:route.GetName(), }}) } }
資源 Reconciliation Cycle
reconcile cycle是在被watch的event傳遞后框架將控制權(quán)轉(zhuǎn)交給我們地方。正如之前所解釋的,在該reconcile cycle中我們沒(méi)有獲得相關(guān)時(shí)間類型的信息,是因?yàn)槲覀兪腔谒接|發(fā)的方式來(lái)工作。
下面是一個(gè)管理CRD控制器的常見reconcile cycle的模型。和其他任何一個(gè)模型一樣,它不會(huì)反映任何特定用例,但我希望它將有助于解決你在編寫operator時(shí)遇到的問(wèn)題。
從圖中我們可以看到,主要步驟是:
檢索你感興趣的CR實(shí)例
確認(rèn)實(shí)例的有效性。我們不會(huì)在不合法實(shí)例上做任何事情。
初始化實(shí)例。如果實(shí)例的某些值沒(méi)有被初始化,我們會(huì)在這一步進(jìn)行處理。
判斷實(shí)例的deletion狀態(tài)。如果實(shí)例正在被刪除,我們也需要做一些特殊的清理。
管理控制器的業(yè)務(wù)邏輯。如果以上步驟均通過(guò),我們最終可以管理和執(zhí)行該實(shí)例的reconcile邏輯。這個(gè)邏輯每個(gè)控制器都不盡相同。
在本節(jié)的剩余部分,你可以找到有關(guān)每個(gè)步驟的更深入的注意事項(xiàng)。
資源驗(yàn)證
這里存在兩種類型的校驗(yàn):語(yǔ)法校驗(yàn)和語(yǔ)義校驗(yàn)。
語(yǔ)法校驗(yàn):通過(guò)定義OpenAPI規(guī)則來(lái)驗(yàn)證。
語(yǔ)義校驗(yàn):可以通過(guò)創(chuàng)建 ValidatingAdmissionConfiguration 來(lái)完成。
注意:在控制器中不能校驗(yàn)CR合法性。一旦CR被APIServer接受了,它就會(huì)存在Etcd中。CR存在Etcd之后,管理該CR資源的控制器就無(wú)法拒絕它,如果這個(gè)CR是不合法的,控制器在嘗試使用或處理它的時(shí)候?qū)?huì)發(fā)生錯(cuò)誤。
推薦:但是由于我們不能保證ValidatingAdmissionConfiguration被創(chuàng)建或正常工作,我們還是應(yīng)該在控制器內(nèi)部去驗(yàn)證CR,如果CR不合法,應(yīng)該避免創(chuàng)建無(wú)限錯(cuò)誤循環(huán)。
語(yǔ)法校驗(yàn)
可以按照這個(gè)鏈接的描述添加OpenAPI驗(yàn)證規(guī)則
推薦:盡可能多地為你的自定義資源模型進(jìn)行語(yǔ)法校驗(yàn)。你應(yīng)該盡量使用語(yǔ)法校驗(yàn),因?yàn)樗鄬?duì)簡(jiǎn)單,并且可以防止格式錯(cuò)誤的 CR 存儲(chǔ)在 etcd 中。
語(yǔ)義校驗(yàn)
語(yǔ)義校驗(yàn)是為了確保字段具有合理的值,從而使整個(gè)資源記錄是有意義的。語(yǔ)義驗(yàn)證業(yè)務(wù)邏輯取決于 CR 所代表的概念,并且必須由operator的開發(fā)人員進(jìn)行編碼實(shí)現(xiàn)。
如果給定的CR需要語(yǔ)義校驗(yàn),那么operator需要暴露一個(gè)webhook,作為operator deploymen的一部分,ValidatingAdmissionConfiguration也應(yīng)該被創(chuàng)建。
以下是目前存在的局限性:
在OpenShift 3.11中,ValidatingAdmissionConfigurations還處于技術(shù)預(yù)覽階段(將從4.1開始支持)
Operator SDK不支持腳手架形式的webhook。可以使用kubebuilder來(lái)進(jìn)行實(shí)現(xiàn):
kubebuilderwebhook--groupcrew--versionv1--kindFirstMate--type=mutating--operations=create,update
驗(yàn)證控制器中的資源
最好的方式是直接拒絕一個(gè)無(wú)效的CR,而不是接受并保存在Etcd中,然后對(duì)它進(jìn)行錯(cuò)誤條件處理。當(dāng)然也有可能的情況是,ValidatingAdmissionConfiguration并沒(méi)有被部署或者根本不可用。所以我認(rèn)為在控制器代碼中進(jìn)行語(yǔ)義校驗(yàn)仍然是一個(gè)很好的做法。你應(yīng)該做到的是,可以在ValidatingAdmissionConfiguration和控制器之間共享這部分結(jié)構(gòu)化的代碼。
控制器中調(diào)用驗(yàn)證方法的代碼如下所示:
ifok,err:=r.IsValid(instance);!ok{ returnr.ManageError(instance,err) }
請(qǐng)注意,如果驗(yàn)證失敗,我們按照錯(cuò)誤管理部分中的描述來(lái)管理這個(gè)錯(cuò)誤。
IsValid函數(shù)如下:
func(r*ReconcileMyCRD)IsValid(objmetav1.Object)(bool,error){ mycrd,ok:=obj.(*examplev1alpha1.MyCRD) //validationlogic }
資源初始化
Kubernetes的一個(gè)很好的慣例是用戶只初始化他所需要的資源字段,其他的可以省略。以上是用戶的視點(diǎn),但從編碼人員和調(diào)試者的角度來(lái)說(shuō),實(shí)際上最好將所有的字段都初始化。這允許在編碼的時(shí)候不必總是去校驗(yàn)字段是否被定義了,并且可以輕松地排除錯(cuò)誤情況。為了初始化資源,這里有兩個(gè)選項(xiàng):
在控制器中定義初始化方法
定義一個(gè) MutatingAdmissionConfiguration(類似于ValidatingAdmissionConfiguration的程序)
建議:在控制器中定義一個(gè)初始化方法。代碼應(yīng)類似于此示例:
ifok:=r.IsInitialized(instance);!ok{ err:=r.GetClient().Update(context.TODO(),instance) iferr!=nil{ log.Error(err,"unabletoupdateinstance","instance",instance) returnr.ManageError(instance,err) } returnreconcile.Result{},nil }
注意,如果IsInitialized方法的結(jié)果返回true,我們更新instance并return。這將會(huì)立即出發(fā)另一個(gè)reconcile cycle。第二次調(diào)用IsInitialized方法將會(huì)返回false,代碼邏輯將會(huì)執(zhí)行到下一部分。
資源 Finalization
如果資源不屬于您的操作員控制的 CR,但在刪除該 CR 時(shí)需要采取措施,您必須使用finalizer。
終結(jié)器提供了一種機(jī)制來(lái)通知 Kubernetes 控制平面,在執(zhí)行標(biāo)準(zhǔn) Kubernetes 垃圾收集邏輯之前需要執(zhí)行一個(gè)操作。
資源可以有一個(gè)或多個(gè)finalizers。每一個(gè)控制器應(yīng)該管理自己的finalizer并且忽略其他的。
這是管理finalizers的偽代碼算法:
如果需要,在初始化方法中添加finalizer。
當(dāng)資源被刪除,檢查此控制器擁有的finalizer是否存在。
清理成功,移除finalizer并更新CR
如果失敗決定是重試還是放棄并可能留下垃圾(在某些情況下這是可以接受的)
如果不存在,直接return
如果存在,執(zhí)行如下清理邏輯:
如果你的清理邏輯需要添加額外的資源,需要記住的是,無(wú)法在正在刪除的命名空間中創(chuàng)建其他資源。刪除命名空間將會(huì)觸發(fā)finalizer并刪除其下所有資源。
看如下的代碼例子:
ifutil.IsBeingDeleted(instance){ if!util.HasFinalizer(instance,controllerName){ returnreconcile.Result{},nil } err:=r.manageCleanUpLogic(instance) iferr!=nil{ log.Error(err,"unabletodeleteinstance","instance",instance) returnr.ManageError(instance,err) } util.RemoveFinalizer(instance,controllerName) err=r.GetClient().Update(context.TODO(),instance) iferr!=nil{ log.Error(err,"unabletoupdateinstance","instance",instance) returnr.ManageError(instance,err) } returnreconcile.Result{},nil }
資源所有權(quán)
資源所有權(quán)是Kubernetes中的原生概念,它決定了資源如何被刪除。默認(rèn)情況下,當(dāng)一個(gè)資源被刪除的時(shí)候,它的子資源也也會(huì)被刪除(你可以設(shè)置cascade=false來(lái)關(guān)閉這種行為)
這種行為有助于確保資源的正確垃圾收集,尤其是當(dāng)資源控制多級(jí)層次結(jié)構(gòu)中的其他資源時(shí)(deployment-> repilcaset->pod)
建議:如果你的控制器創(chuàng)建資源并且它的生命周期與其他資源(kubernetes核心資源或其他CR)有關(guān)聯(lián),那么您應(yīng)該將此資源設(shè)置為其他資源的所有者,如下所示:
controllerutil.SetControllerReference(owner,obj,r.GetScheme())
有關(guān)所有權(quán)的其他規(guī)則如下:
父子資源必須位于同一命名空間中
命名空間資源可以擁有集群資源。但我們必須小心處理。一個(gè)對(duì)象可以有一個(gè)所有者列表。如果多個(gè)命名空間對(duì)象擁有相同的集群資源,則每個(gè)對(duì)象都應(yīng)聲明所有權(quán),而不會(huì)覆蓋其他對(duì)象的所有權(quán)
集群資源不能擁有命名空間資源
集群資源可以擁有另外一個(gè)集群資源
狀態(tài)管理
Status是資源的一個(gè)標(biāo)準(zhǔn)部分。Status被用于報(bào)告資源的狀態(tài)。在本文檔中,我們將使用 status 報(bào)告最后一次執(zhí)行協(xié)調(diào)循環(huán)的結(jié)果。你也可以在Status中添加更多的信息。
在正常情況下,如果我們每次執(zhí)行reconcile cycle的時(shí)候都要更新資源,這將觸發(fā)更新時(shí)間,進(jìn)而導(dǎo)致無(wú)限觸發(fā)reconcile cycle。
因此,正如上面描述的那樣,我們應(yīng)該把Status作為子資源。
使用這種方法,我們能夠不增加ResourceGeneration元數(shù)據(jù)域的情況下更新資源的狀態(tài)。使用如下命令更新狀態(tài):
err=r.Status().Update(context.Background(),instance)
現(xiàn)在我們需要為我們的watch機(jī)制寫一個(gè)predicate(有關(guān)這些概念的更多詳細(xì)信息,請(qǐng)參閱有關(guān)watches的部分)用來(lái)丟棄不增加ResourceGeneration的更新事件。可以使用GenerationChangePredicate來(lái)完成此功能。
如果你還記得的話,上文提到過(guò),在使用finalizer的時(shí)候,應(yīng)該在初始化的時(shí)候設(shè)置。如果finalizer是初始化的唯一項(xiàng),由于它是元數(shù)據(jù)項(xiàng)的一部分,所以ResourceGeneration不會(huì)遞增。為了說(shuō)明該用例,以下是predicate的修改版本:
typeresourceGenerationOrFinalizerChangedPredicatestruct{ predicate.Funcs } //UpdateimplementsdefaultUpdateEventfilterforvalidatingresourceversionchange func(resourceGenerationOrFinalizerChangedPredicate)Update(eevent.UpdateEvent)bool{ ife.MetaNew.GetGeneration()==e.MetaOld.GetGeneration()&&reflect.DeepEqual(e.MetaNew.GetFinalizers(),e.MetaOld.GetFinalizers()){ returnfalse } returntrue }
現(xiàn)在假設(shè)你的status如下所示:
typeMyCRStatusstruct{ //+kubebuilderEnum=Success,Failure Statusstring`json:"status,omitempty"` LastUpdatemetav1.Time`json:"lastUpdate,omitempty"` Reasonstring`json:"reason,omitempty"` }
你可以寫一個(gè)函數(shù)來(lái)管理并保證reconcile cycle成功執(zhí)行:
func(r*ReconcilerBase)ManageSuccess(objmetav1.Object)(reconcile.Result,error){ runtimeObj,ok:=(obj).(runtime.Object) if!ok{ log.Error(errors.New("notaruntime.Object"),"passedobjectwasnotaruntime.Object","object",obj) returnreconcile.Result{},nil } ifreconcileStatusAware,updateStatus:=(obj).(apis.ReconcileStatusAware);updateStatus{ status:=apis.ReconcileStatus{ LastUpdate:metav1.Now(), Reason:"", Status:"Success", } reconcileStatusAware.SetReconcileStatus(status) err:=r.GetClient().Status().Update(context.Background(),runtimeObj) iferr!=nil{ log.Error(err,"unabletoupdatestatus") returnreconcile.Result{ RequeueAfter:time.Second, Requeue:true, },nil } }else{ log.Info("objectisnotRecocileStatusAware,notsettingstatus") } returnreconcile.Result{},nil }
錯(cuò)誤管理
如果控制器進(jìn)入了一個(gè)錯(cuò)誤條件,并且在reconcile方法中返回了一個(gè)錯(cuò)誤。operator將會(huì)打印錯(cuò)誤日志到標(biāo)準(zhǔn)輸出,reconlie event將會(huì)立即再次調(diào)度(默認(rèn)的調(diào)度器實(shí)際上應(yīng)該檢測(cè)是否一遍又一遍地出現(xiàn)相同的錯(cuò)誤,并增加相應(yīng)的調(diào)度時(shí)間,但在我的經(jīng)驗(yàn)看來(lái),這并沒(méi)有發(fā)生)。如果錯(cuò)誤一直存在,那么也將永遠(yuǎn)存在錯(cuò)誤循環(huán)。而且,這個(gè)錯(cuò)誤條件對(duì)用戶來(lái)說(shuō)是不可見的。
有兩種方法可以通知用戶發(fā)生了錯(cuò)誤,它們可以同時(shí)使用:
在對(duì)象的status字段中返回錯(cuò)誤
生成一個(gè)event描述錯(cuò)誤
此外,如果你認(rèn)為錯(cuò)誤能夠自解決,你應(yīng)該在一段周期時(shí)間后重新調(diào)度reconcile cycle。通常來(lái)說(shuō),周期時(shí)間是呈指數(shù)增長(zhǎng)的,因此在每次迭代中,reconcile event周期會(huì)越來(lái)越長(zhǎng)(例如每次增長(zhǎng)時(shí)間量的兩倍)。
我們現(xiàn)在構(gòu)建狀態(tài)管理來(lái)處理錯(cuò)誤條件:
func(r*ReconcilerBase)ManageError(objmetav1.Object,issueerror)(reconcile.Result,error){ runtimeObj,ok:=(obj).(runtime.Object) if!ok{ log.Error(errors.New("notaruntime.Object"),"passedobjectwasnotaruntime.Object","object",obj) returnreconcile.Result{},nil } varretryIntervaltime.Duration r.GetRecorder().Event(runtimeObj,"Warning","ProcessingError",issue.Error()) ifreconcileStatusAware,updateStatus:=(obj).(apis.ReconcileStatusAware);updateStatus{ lastUpdate:=reconcileStatusAware.GetReconcileStatus().LastUpdate.Time lastStatus:=reconcileStatusAware.GetReconcileStatus().Status status:=apis.ReconcileStatus{ LastUpdate:metav1.Now(), Reason:issue.Error(), Status:"Failure", } reconcileStatusAware.SetReconcileStatus(status) err:=r.GetClient().Status().Update(context.Background(),runtimeObj) iferr!=nil{ log.Error(err,"unabletoupdatestatus") returnreconcile.Result{ RequeueAfter:time.Second, Requeue:true, },nil } iflastUpdate.IsZero()||lastStatus=="Success"{ retryInterval=time.Second }else{ retryInterval=status.LastUpdate.Sub(lastUpdate).Round(time.Second) } }else{ log.Info("objectisnotRecocileStatusAware,notsettingstatus") retryInterval=time.Second } returnreconcile.Result{ RequeueAfter:time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2),float64(time.Hour.Nanoseconds()*6))), Requeue:true, },nil }
注意,此函數(shù)會(huì)立即發(fā)送一個(gè)event,然后使用錯(cuò)誤條件更新狀態(tài)。最后,計(jì)算何時(shí)重新安排下一次reconcile。該算法嘗試將每個(gè)循環(huán)的時(shí)間加倍,最多到六個(gè)小時(shí)為止。
六個(gè)小時(shí)是一個(gè)很好的上限時(shí)間,因?yàn)閑vent大約持續(xù)6個(gè)小時(shí),所以這應(yīng)該確保始終有一個(gè)活動(dòng)event描述當(dāng)前的錯(cuò)誤情況。
總結(jié)
本博客中介紹的實(shí)踐涉及Kubernetes Operator時(shí)最常見的問(wèn)題,且讓你可以編寫一個(gè)有信心投入生產(chǎn)環(huán)境的operator。當(dāng)然很有可能,這僅僅是一個(gè)開始,我們很容易預(yù)見將會(huì)有更多的框架和工具的出現(xiàn)來(lái)幫助你編寫operator。
審核編輯:劉清
-
控制器
+關(guān)注
關(guān)注
112文章
16444瀏覽量
179068 -
API接口
+關(guān)注
關(guān)注
1文章
84瀏覽量
10479 -
CRD
+關(guān)注
關(guān)注
0文章
14瀏覽量
4024 -
TLS
+關(guān)注
關(guān)注
0文章
44瀏覽量
4267
原文標(biāo)題:Kubernetes Operator 最佳實(shí)踐
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論