日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Kubernetes operator 模式开发实践

發(fā)布時(shí)間:2024/8/23 编程问答 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Kubernetes operator 模式开发实践 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

0. 前言

近日我們在開發(fā)符合我們業(yè)務(wù)自身需求的微服務(wù)平臺(tái)時(shí),使用了 Kubernetes 的 Operator Pattern 來實(shí)現(xiàn)其中的運(yùn)維系統(tǒng),在本文,我們將實(shí)現(xiàn)過程中積累的主要知識(shí)點(diǎn)和技術(shù)細(xì)節(jié)做了一個(gè)整理。

讀者在閱讀完本文之后,會(huì)對(duì) Operator Pattern 有一個(gè)基本的了解,并能將該模式應(yīng)用到自己的業(yè)務(wù)中去。除此之外,我們也會(huì)分享要實(shí)現(xiàn)這一運(yùn)維系統(tǒng)需要具備的一些相關(guān)知識(shí)。
注:閱讀本文內(nèi)容需要對(duì) Kubernetes 和 Go 語言有基本了解。

1. 什么是 Operator Pattern

在解釋什么是 Operator Pattern 之前,我們得先了解在我們使用一個(gè) Kubernetes 客戶端——這里以 kubectl 舉例——向 Kubernetes 集群發(fā)出指令,直到這項(xiàng)指令被 Kubernetes 集群執(zhí)行結(jié)束,這段時(shí)間之內(nèi)到底都發(fā)生了什么。
這里以我們輸入_ kubectl create -f ns-my-workspace.yaml_ 這條命令舉例,這條命令的整條執(zhí)行鏈路大致如下圖所示:

### ns-my-workspace.yaml apiVersion: v1 kind: Namespace metadata:name: my-workspace

如上圖所示,所有在 Kubernetes 集群中的組件交互都是通過 RESTful API 的形式完成的,包括第一步的控制器監(jiān)聽操作,以及第二步中 kubectl 發(fā)送的指令。雖說我們執(zhí)行的是 _kubectl create -f ns-my-workspace.yaml _指令,但其實(shí) kubectl 會(huì)向「API服務(wù)器」發(fā)送一個(gè) POST 請(qǐng)求:

curl --request POST \--url http://${k8s.host}:${k8s.port}/api/v1/namespaces \--header 'content-type: application/json' \--data '{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"my-workspace"} }'

如上面的 cURL 指令,Kubernetes API 服務(wù)器接受的其實(shí)是 JSON 數(shù)據(jù)類型,而并非是 YAML。
然后所有這些創(chuàng)建的 resources 都會(huì)持久化到 etcd 組件中,「API服務(wù)器」也是 Kubernetes 集群中與「etcd」交互的唯一一個(gè)組件。
之后被創(chuàng)建的 my-workspace resource 就會(huì)被發(fā)送給監(jiān)聽了 namespaces resource 變更的 「Namespace控制器」中,最后就由「Namespace控制器」執(zhí)行創(chuàng)建 my-workspace 命名空間的具體操作。那么同理,當(dāng)創(chuàng)建 ReplicaSet resource 時(shí)就會(huì)由「ReplicaSet控制器」做具體執(zhí)行,當(dāng)創(chuàng)建 Pod 時(shí),則會(huì)由「Pod控制器」具體執(zhí)行,其他類型的 resource 與之類似,這些控制器共同組成了上圖中的「Kubernetes API 控制器集合」。

說到這里,我們不難發(fā)現(xiàn),實(shí)現(xiàn) Kubernetes 中某一種領(lǐng)域類型——如上面提到的 Namespace、ReplicaSet、Pod,也即 Kubernetes 中的 Kind——的操作邏輯,需要具備兩個(gè)因素:

  • 對(duì)該領(lǐng)域類型的模型抽象,如上面的 ns-my-workspace.yaml 文件描述的 YAML 數(shù)據(jù)結(jié)構(gòu),這個(gè)抽象決定了 Kubernetes client 發(fā)送到 Kubernetes API server 的 RESTful API 請(qǐng)求,也描述了這個(gè)領(lǐng)域類型本身。
  • 實(shí)際去處理這個(gè)領(lǐng)域類型抽象的控制器,如上面的「Namespace控制器」、「ReplicaSet控制器」、「Pod控制器」,這些控制器實(shí)現(xiàn)了這個(gè)抽象描述的具體業(yè)務(wù)邏輯,并通過 RESTful API 提供這些服務(wù)。
  • 而當(dāng) Kubernetes 開發(fā)者需要擴(kuò)展 Kubernetes 能力時(shí),也可以遵循這種模式,即提供一份對(duì)想要擴(kuò)展的能力的抽象,和實(shí)現(xiàn)了這個(gè)抽象具體邏輯的控制器。前者稱作 CRD(Custom Resource Definition),后者稱作 Controller。
    Operator pattern 就是通過這種方式實(shí)現(xiàn) Kubernetes 擴(kuò)展性的一種模式,Operator 模式認(rèn)為可以將一個(gè)領(lǐng)域問題的解決方案想像成是一個(gè)「操作者」,這個(gè)操作者在用戶和集群之間,通過一份份「訂單」,去操作集群的API,來達(dá)到完成這個(gè)領(lǐng)域各種需求的目的。這里的訂單就是 CR(Custom Resource,即 CRD 的一個(gè)實(shí)例),而操作者就是控制器,是具體邏輯的實(shí)現(xiàn)者。之所以強(qiáng)調(diào)是 operator,而不是計(jì)算機(jī)領(lǐng)域里傳統(tǒng)的 server 角色,則是因?yàn)?operator 本質(zhì)上不創(chuàng)造和提供新的服務(wù),他只是已有 Kubernetes API service 的組合
    而本文實(shí)踐的「運(yùn)維系統(tǒng)」,就是一個(gè)為了解決運(yùn)維領(lǐng)域問題,而實(shí)現(xiàn)出來的 operator。

    2. Operator Pattern 實(shí)戰(zhàn)

    在本節(jié)我們會(huì)通過使用 kubebuilder 工具,構(gòu)建一個(gè) Kubernetes Operator,在本節(jié)之后,我們會(huì)在自己的 Kubernetes 集群中獲得一個(gè) CRD 和其對(duì)應(yīng)的 Kubernetes API 控制器,用于簡單的部署一個(gè)微服務(wù)。即當(dāng)我們 create 如下 YAML 時(shí):

    apiVersion: devops.my.domain/v1 kind: DemoMicroService metadata:name: demomicroservice-sample spec:image: stefanprodan/podinfo:0.0.1

    可得到一個(gè) Kubernetes 部署實(shí)例:

    本節(jié)所有示例代碼均提供在:https://github.com/l4wei/kubebuilder-example

    2.1 Kubebuilder 實(shí)現(xiàn)

    Kubebuilder(https://github.com/kubernetes-sigs/kubebuilder)是一個(gè)用 Go 語言構(gòu)建 Kubernetes APIs 控制器和 CRD 的腳手架工具,通過使用 kubebuilder,用戶可以遵循一套簡單的編程框架,使用 Go 語言方便的實(shí)現(xiàn)一個(gè) operator。

    2.1.1 安裝

    在安裝 kubebuilder 之前,需要先安裝 Go 語言和 kustomize,并確保可以正常使用。
    kustomize 是一個(gè)可定制化生成 Kubernetes YAML Configuration 文件的工具,你可以通過遵循一套 kustomize 的配置,批量的生成你需要的?Kubernetes YAML 配置。kubebuilder 使用了 kustomize 去生成控制器所需的一些 YAML 配置。mac 用戶可使用 brew 方便地安裝 kustomize。
    然后使用使用下面的腳本安裝 kubebuilder:

    os=$(go env GOOS) arch=$(go env GOARCH)# download kubebuilder and extract it to tmp curl -L https://go.kubebuilder.io/dl/2.2.0/${os}/${arch} | tar -xz -C /tmp/# move to a long-term location and put it on your path # (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else) sudo mv /tmp/kubebuilder_2.2.0_${os}_${arch} /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin

    使用 kubebuilder -h 若能看到幫助文檔,則表示 kubebuilder 安裝成功。

    2.1.2 創(chuàng)建工程

    使用下面的腳本創(chuàng)建一個(gè) kubebuilder 工程:

    mkdir example cd example go mod init my.domain/example kubebuilder init --domain my.domain

    上述命令的?my.domain?一般是你所在機(jī)構(gòu)的域名,_example_ 一般是你這個(gè) Go 語言項(xiàng)目的項(xiàng)目名。根據(jù)這樣的設(shè)定,如果這個(gè) Go 項(xiàng)目作為一個(gè)模塊要被其他 Go 項(xiàng)目依賴,那么一般命名為?my.domain/example_。
    如果你的 example 目錄建立在 ${GOPATH} 目錄之下,那么就不需要 _go mod init my.domain/example?這條命令,Go 語言也能找到該 example 目錄下的 go pkg。
    然后確保以下兩條命令在你的開發(fā)機(jī)器上被執(zhí)行過:

    export GO111MODULE=on sudo chmod -R 777 ${GOPATH}/go/pkg

    以上兩條命令的執(zhí)行可以解決在開發(fā)時(shí)可能出現(xiàn)的cannot find package ... (from $GOROOT)這種問題。
    在創(chuàng)建完工程之后,你的 example 目錄結(jié)構(gòu)會(huì)大致如下:

    . ├── Dockerfile ├── Makefile ├── PROJECT ├── bin │?? └── manager ├── config │?? ├── certmanager │?? │?? ├── certificate.yaml │?? │?? ├── kustomization.yaml │?? │?? └── kustomizeconfig.yaml │?? ├── default │?? │?? ├── kustomization.yaml │?? │?? ├── manager_auth_proxy_patch.yaml │?? │?? ├── manager_webhook_patch.yaml │?? │?? └── webhookcainjection_patch.yaml │?? ├── manager │?? │?? ├── kustomization.yaml │?? │?? └── manager.yaml │?? ├── prometheus │?? │?? ├── kustomization.yaml │?? │?? └── monitor.yaml │?? ├── rbac │?? │?? ├── auth_proxy_role.yaml │?? │?? ├── auth_proxy_role_binding.yaml │?? │?? ├── auth_proxy_service.yaml │?? │?? ├── kustomization.yaml │?? │?? ├── leader_election_role.yaml │?? │?? ├── leader_election_role_binding.yaml │?? │?? └── role_binding.yaml │?? └── webhook │?? ├── kustomization.yaml │?? ├── kustomizeconfig.yaml │?? └── service.yaml ├── go.mod ├── go.sum ├── hack │?? └── boilerplate.go.txt └── main.go

    上面目錄中的 bin 目錄下的 manager 即工程編譯出的二進(jìn)制可執(zhí)行文件,也就是這個(gè)控制器的可執(zhí)行文件。
    config 目錄下都是 kustomize 的配置,例如 config/manager 目錄下面的文件即生成控制器部署 YAML 配置文件的 kustomize 配置,如果你執(zhí)行下面的指令:

    kustomize build config/manager

    就能看到 kustomize 生成的 YAML 配置:

    apiVersion: v1 kind: Namespace metadata:labels:control-plane: controller-managername: system --- apiVersion: apps/v1 kind: Deployment metadata:labels:control-plane: controller-managername: controller-managernamespace: system spec:replicas: 1selector:matchLabels:control-plane: controller-managertemplate:metadata:labels:control-plane: controller-managerspec:containers:- args:- --enable-leader-electioncommand:- /managerimage: controller:latestname: managerresources:limits:cpu: 100mmemory: 30Mirequests:cpu: 100mmemory: 20MiterminationGracePeriodSeconds: 10

    上面就是將 bin/manager 部署到 Kubernetes 集群的 YAML configurations。

    2.1.3 創(chuàng)建API

    上面創(chuàng)建的工程僅僅只是一個(gè)空殼,還沒有提供任何的 Kubernetes API,也不能處理任何的 CR。使用下面的腳本創(chuàng)建一個(gè) Kubernetes API:

    kubebuilder create api --group devops --version v1 --kind DemoMicroService

    上述命令的 group 將與之前創(chuàng)建工程時(shí)輸入的 domain 共同組成你創(chuàng)建的 Kubernetes API YAML resource 里 apiVersion 字段的前半部分,上面的 version 即后半部分,所以你自定義的 resource YAML 里的 apiVersion 就應(yīng)該寫作:devops.my.domain/v1。上面的 kind 就是你自定義 resource 里的 kind 字段。通過該條指令創(chuàng)建的 resource 看起來正如 kubebuilder 創(chuàng)建的 config/samples/devops_v1_demomicroservice.yaml 文件一樣:

    apiVersion: devops.my.domain/v1 kind: DemoMicroService metadata:name: demomicroservice-sample spec:# Add fields here foo: bar

    輸入該命令會(huì)提示你是否創(chuàng)建 Resource(即 CRD),是否創(chuàng)建 Controller(即控制器),全部輸入「y」同意即可。
    在執(zhí)行完該命令之后,你的工程結(jié)構(gòu)將變成這樣:

    . ├── Dockerfile ├── Makefile ├── PROJECT ├── api │?? └── v1 │?? ├── demomicroservice_types.go │?? ├── groupversion_info.go │?? └── zz_generated.deepcopy.go ├── bin │?? └── manager ├── config │?? ├── certmanager │?? │?? ├── certificate.yaml │?? │?? ├── kustomization.yaml │?? │?? └── kustomizeconfig.yaml │?? ├── crd │?? │?? ├── kustomization.yaml │?? │?? ├── kustomizeconfig.yaml │?? │?? └── patches │?? │?? ├── cainjection_in_demomicroservices.yaml │?? │?? └── webhook_in_demomicroservices.yaml │?? ├── default │?? │?? ├── kustomization.yaml │?? │?? ├── manager_auth_proxy_patch.yaml │?? │?? ├── manager_webhook_patch.yaml │?? │?? └── webhookcainjection_patch.yaml │?? ├── manager │?? │?? ├── kustomization.yaml │?? │?? └── manager.yaml │?? ├── prometheus │?? │?? ├── kustomization.yaml │?? │?? └── monitor.yaml │?? ├── rbac │?? │?? ├── auth_proxy_role.yaml │?? │?? ├── auth_proxy_role_binding.yaml │?? │?? ├── auth_proxy_service.yaml │?? │?? ├── demomicroservice_editor_role.yaml │?? │?? ├── demomicroservice_viewer_role.yaml │?? │?? ├── kustomization.yaml │?? │?? ├── leader_election_role.yaml │?? │?? ├── leader_election_role_binding.yaml │?? │?? └── role_binding.yaml │?? ├── samples │?? │?? └── devops_v1_demomicroservice.yaml │?? └── webhook │?? ├── kustomization.yaml │?? ├── kustomizeconfig.yaml │?? └── service.yaml ├── controllers │?? ├── demomicroservice_controller.go │?? └── suite_test.go ├── go.mod ├── go.sum ├── hack │?? └── boilerplate.go.txt └── main.go

    比起創(chuàng)建 API 之前,增加了:

    • api 目錄——即定義了你創(chuàng)建的 Kubernetes API 的數(shù)據(jù)結(jié)構(gòu)代碼。
    • controllers 目錄——即控制器的實(shí)現(xiàn)代碼。
    • config/crd 目錄——該目錄里的 kustomize 配置可生成你要定義的 CRD 的 YAML 配置。

    輸入以下命令:

    make manifests

    即可在?config/crd/bases/devops.my.domain_demomicroservices.yaml?文件里看到你創(chuàng)建該 Kubernetes API 時(shí)創(chuàng)建的 CRD:

    --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata:annotations:controller-gen.kubebuilder.io/version: v0.2.4creationTimestamp: nullname: demomicroservices.devops.my.domain spec:group: devops.my.domainnames:kind: DemoMicroServicelistKind: DemoMicroServiceListplural: demomicroservicessingular: demomicroservicescope: Namespacedvalidation:openAPIV3Schema:description: DemoMicroService is the Schema for the demomicroservices APIproperties:apiVersion:description: 'APIVersion defines the versioned schema of this representationof an object. Servers should convert recognized schemas to the latestinternal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'type: stringkind:description: 'Kind is a string value representing the REST resource thisobject represents. Servers may infer this from the endpoint the clientsubmits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'type: stringmetadata:type: objectspec:description: DemoMicroServiceSpec defines the desired state of DemoMicroServiceproperties:foo:description: Foo is an example field of DemoMicroService. Edit DemoMicroService_types.goto remove/updatetype: stringtype: objectstatus:description: DemoMicroServiceStatus defines the observed state of DemoMicroServicetype: objecttype: objectversion: v1versions:- name: v1served: truestorage: true status:acceptedNames:kind: ""plural: ""conditions: []storedVersions: []

    2.1.4 API屬性定義

    Kubernetes API 創(chuàng)建好了,現(xiàn)在我們需要定義該 API 的屬性,這些屬性才真正描述了創(chuàng)建出的 CRD 的抽象特征。
    在我們這個(gè) DemoMicroService Kind 例子中,我們只簡單的抽象出一個(gè)微服務(wù)的部署 CRD,所以我們這個(gè) CRD 只有一個(gè)屬性,即該服務(wù)的容器鏡像地址。
    為此,我們只需要修改 api/v1/demomicroservice_types.go 文件:

    上面的 git diff 顯示我們將原來示例的 Foo 屬性改成了我們需要的 Image 屬性。對(duì) API 屬性的定義和對(duì) CRD 的定義基本上只需要修改該文件即可。
    再次執(zhí)行:

    make manifests

    即可看到生成的 CRD resource 發(fā)生了變更,這里不再贅述。
    現(xiàn)在我們也修改一下 config/samples/devops_v1_demomicroservice.yaml 文件,后面需要使用該文件測試我們實(shí)現(xiàn)的控制器:

    apiVersion: devops.my.domain/v1 kind: DemoMicroService metadata:name: demomicroservice-sample spec:image: stefanprodan/podinfo:0.0.1

    2.1.5 控制器邏輯實(shí)現(xiàn)

    CRD 定義好了,現(xiàn)在開始實(shí)現(xiàn)控制器。
    我們在這次示例中要實(shí)現(xiàn)的控制器邏輯非常簡單,基本可以描述成:

  • 當(dāng)我們執(zhí)行 kubectl create -f config/samples/devops_v1_demomicroservice.yaml 時(shí),控制器會(huì)在集群中創(chuàng)建一個(gè) Kubernetes Deployment resource,用于實(shí)現(xiàn)該 DemoMicroService 的部署。
  • 當(dāng)我們執(zhí)行?kubectl delete -f config/samples/devops_v1_demomicroservice.yaml 時(shí),控制器會(huì)將在集群中創(chuàng)建的 Deployment resource 刪掉,表示該 DemoMicroService 的下線。
  • 2.1.5.1 部署的實(shí)現(xiàn)

    寫代碼之前,我們需要先了解 kubebuilder 程序的開發(fā)方式。
    因?yàn)槲覀円獙?shí)現(xiàn)的是 DemoMicroService 的控制器,所以我們需要先將注意力集中在?_controllers/demomicroservice_controller.go_ 文件,如果不是復(fù)雜的功能,通常我們只需改該文件即可。而在文件中,我們最需要關(guān)心的就是 Reconcile 方法:

    func (r *DemoMicroServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {_ = context.Background()_ = r.Log.WithValues("demomicroservice", req.NamespacedName)// your logic herereturn ctrl.Result{}, nil }

    簡單來說,每當(dāng) kubernetes 集群監(jiān)控到 DemoMicroService CR 的變化時(shí),都會(huì)調(diào)用到這個(gè) Reconcile 方法,并將變更的 DemoMicroService resource name 及其所在的 namespace 作為 Reconcile 方法的參數(shù),用于定位到變更的 resource。即上面的 req 參數(shù),該參數(shù)的結(jié)構(gòu)為:

    type Request struct {// NamespacedName is the name and namespace of the object to reconcile.types.NamespacedName } type NamespacedName struct {Namespace stringName string }

    熟悉前端開發(fā)的朋友可能會(huì)聯(lián)想到 React 的開發(fā)方式,兩者確實(shí)很像,都是監(jiān)聽對(duì)象的變化,再根據(jù)監(jiān)聽對(duì)象的變化來執(zhí)行一些邏輯。不過 kubebuilder 做的更加極端,他沒有抽象出生命周期的概念,只提供一個(gè) Reconcile 方法,開發(fā)者需要自己在這個(gè)方法中判斷出 CRD 的生命周期,并在不同的生命周期中執(zhí)行不同的邏輯。
    以下的代碼實(shí)現(xiàn)了部署的功能:

    func (r *DemoMicroServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {ctx := context.Background()log := r.Log.WithValues("demomicroservice", req.NamespacedName)dms := &devopsv1.DemoMicroService{}if err := r.Get(ctx, req.NamespacedName, dms); err != nil {if err := client.IgnoreNotFound(err); err == nil {log.Info("此時(shí)沒有找到對(duì)應(yīng)的 DemoMicroService resource, 即此處進(jìn)入了 resource 被刪除成功后的生命周期")return ctrl.Result{}, nil} else {log.Error(err, "不是未找到的錯(cuò)誤,那么就是意料之外的錯(cuò)誤,所以這里直接返回錯(cuò)誤")return ctrl.Result{}, err}}log.Info("走到這里意味著 DemoMicroService resource 被找到,即該 resource 被成功創(chuàng)建,進(jìn)入到了可根據(jù)該 resource 來執(zhí)行邏輯的主流程")podLabels := map[string]string{"app": req.Name,}deployment := appv1.Deployment{TypeMeta: metav1.TypeMeta{Kind: "Deployment",APIVersion: "apps/v1",},ObjectMeta: metav1.ObjectMeta{Name: req.Name,Namespace: req.Namespace,},Spec: appv1.DeploymentSpec{Selector: &metav1.LabelSelector{MatchLabels: podLabels,},Template: corev1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: podLabels,},Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: req.Name,Image: dms.Spec.Image,ImagePullPolicy: "Always",Ports: []corev1.ContainerPort{{ContainerPort: 9898,},},},},},},},}if err := r.Create(ctx, &deployment); err != nil {log.Error(err, "創(chuàng)建 Deployment resource 出錯(cuò)")return ctrl.Result{}, err}return ctrl.Result{}, nil }

    上述代碼展現(xiàn)了生命周期中的兩個(gè)階段:即第8行中代表 DemoMicroService resource 被成功刪除之后的階段,此時(shí)我們什么都沒有做。以及在第16行,進(jìn)入到創(chuàng)建/更新完 DemoMicroService resource 之后的階段,此時(shí)我們構(gòu)建了一個(gè) Deployment resource,并將其創(chuàng)建到了 Kubernetes 集群中。
    這段代碼有個(gè)問題,即無法實(shí)現(xiàn) DemoMicroService resource 的更新,如果同一個(gè)?DemoMicroService resource 的 spec.image 被改變了,那么在上述代碼中會(huì)再次 create 相同的 Deployment resource,這會(huì)導(dǎo)致一個(gè) "already exists" 的報(bào)錯(cuò)。這里為了方便說明開發(fā)邏輯,沒有處理這個(gè)問題,請(qǐng)讀者注意。

    2.1.5.1 下線的實(shí)現(xiàn)

    其實(shí)在上一節(jié)我們說明部署邏輯的時(shí)候,就能實(shí)現(xiàn)下線的邏輯:我們只需在「刪除成功后」的生命周期階段將創(chuàng)建的 Deployment 刪掉即可。但是這樣做有一個(gè)問題,我們是在?DemoMicroService resource 刪除成功之后再刪的 Deployment,如果刪除 Deployment 的邏輯出錯(cuò)了,沒有將 Deployment 刪除成功,那么就會(huì)出現(xiàn) Deployment 還在,DemoMicroService 卻不再的情況,如果我們需要用?DemoMicroService 管理 Deployment,那么這就不是我們想要的結(jié)果。
    所以我們最好在?DemoMicroService 真正消失之前(即「刪除 DemoMicroService」到「DemoMicroService 完全消失」這段時(shí)間)去刪除 Deployment,那么要怎么做呢?請(qǐng)看下面的代碼示例:

    const (demoMicroServiceFinalizer string = "demomicroservice.finalizers.devops.my.domain" )func (r *DemoMicroServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {ctx := context.Background()log := r.Log.WithValues("demomicroservice", req.NamespacedName)dms := &devopsv1.DemoMicroService{}if err := r.Get(ctx, req.NamespacedName, dms); err != nil {if err := client.IgnoreNotFound(err); err == nil {log.Info("此時(shí)沒有找到對(duì)應(yīng)的 DemoMicroService resource, 即此處進(jìn)入了 resource 被刪除成功后的生命周期")return ctrl.Result{}, nil} else {log.Error(err, "不是未找到的錯(cuò)誤,那么就是意料之外的錯(cuò)誤,所以這里直接返回錯(cuò)誤")return ctrl.Result{}, err}}if dms.ObjectMeta.DeletionTimestamp.IsZero() {log.Info("進(jìn)入到 apply 這個(gè) DemoMicroService CR 的邏輯")log.Info("此時(shí)必須確保 resource 的 finalizers 里有控制器指定的 finalizer")if !util.ContainsString(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer) {dms.ObjectMeta.Finalizers = append(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer)if err := r.Update(ctx, dms); err != nil {return ctrl.Result{}, err}}if _, err := r.applyDeployment(ctx, req, dms); err != nil {return ctrl.Result{}, nil}} else {log.Info("進(jìn)入到刪除這個(gè) DemoMicroService CR 的邏輯")if util.ContainsString(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer) {log.Info("如果 finalizers 被清空,則該 DemoMicroService CR 就已經(jīng)不存在了,所以必須在次之前刪除 Deployment")if err := r.cleanDeployment(ctx, req); err != nil {return ctrl.Result{}, nil}}log.Info("清空 finalizers,在此之后該 DemoMicroService CR 才會(huì)真正消失")dms.ObjectMeta.Finalizers = util.RemoveString(dms.ObjectMeta.Finalizers, demoMicroServiceFinalizer)if err := r.Update(ctx, dms); err != nil {return ctrl.Result{}, err}}return ctrl.Result{}, nil }

    為了方便展示主邏輯,我將創(chuàng)建 Deployment 和刪除 Deployment 的代碼封裝到 applyDeployment 和 cleanDeployment 兩個(gè)方法中了。
    如上面代碼所示,想要判斷「刪除 DemoMicroService」到「DemoMicroService 完全消失」這段生命周期的階段,關(guān)鍵點(diǎn)在于判斷?DemoMicroService.ObjectMeta.DeletionTimestam 和 DemoMicroService.ObjectMeta.Finalizers 這兩個(gè)元信息。前者表示「刪除 DemoMicroService」這一行為的具體發(fā)生時(shí)間,如果不為0則表示「刪除 DemoMicroService」指令已經(jīng)下達(dá);而后者表示在真正刪除 DemoMicroService 之前,即「DemoMicroService 完全消失」之前,還有哪些邏輯沒有被執(zhí)行,如果 Finalizers 不為空,那么該?DemoMicroService 則不會(huì)真正消失。
    任何 resource 的 ObjectMeta.Finalizers 都是一個(gè)字符串的列表,每一個(gè)字符串都表示一段 pre-delete 邏輯尚未被執(zhí)行。如上面的「demomicroservice.finalizers.devops.my.domain」所示,表示著?DemoMicroService 控制器對(duì)?DemoMicroService resource 的 pre-delete 邏輯,該?Finalizer 會(huì)在?DemoMicroService 被創(chuàng)建之后,迅速被?DemoMicroService 控制器給種上,并在下達(dá)「刪除 DemoMicroService」指令后,且 pre-delete 邏輯(在這里即刪除 Deployment)被正確執(zhí)行完后,再由?DemoMicroService 控制器將該?Finalizer 抹除。至此?DemoMicroService 上的 Finalizers 為空,此時(shí) Kubernetes 才會(huì)讓該?DemoMicroService 完全消失。Kubernetes 正是通過這種機(jī)制創(chuàng)造出了 resource 的「對(duì)該 resource 下達(dá)刪除指令」到「該 resource 完全消失」這段生命周期的階段。
    如果因?yàn)楦鞣N原因?qū)е?Finalizers 不可能為空,那么會(huì)發(fā)生什么?答案是會(huì)導(dǎo)致這個(gè) resource 永遠(yuǎn)無法被刪掉,如果你使用 kubectl delete 去刪,那么這個(gè)指令將永遠(yuǎn)不會(huì)返回。這也是使用?Finalizers 機(jī)制時(shí)會(huì)經(jīng)常碰到的問題,如果你發(fā)現(xiàn)有一個(gè) resource 始終無法被刪掉,那么請(qǐng)檢查一下你是否種上了某個(gè)不會(huì)被刪掉的?Finalizer。

    2.1.6 調(diào)試與發(fā)布

    2.1.5.1 調(diào)試

    在我們開始運(yùn)行&調(diào)試我們編寫的控制器之前,我們最好為我們的 DemoMicroService CRD 設(shè)置一個(gè)縮寫名稱,這是為了我們后面的操作不用輸入"demomicroservice"這么長的名稱。

    如上圖在 _api/v1/demomicroservice_types.go_?文件里加上一行注釋,我們將"demomicroservice"的縮寫設(shè)置成"dms"。
    順帶一提,kubebuilder 中,所有帶"+kubebuilder"的注釋都是有用的,不要輕易刪掉,他們是對(duì)該 kubebuilder 工程的一些配置。
    改完之后再執(zhí)行"make manifests"指令,會(huì)發(fā)現(xiàn) kubebuiler 生成的 CRD 文件被修改,添加了縮寫的配置:

    再設(shè)置縮寫之后,我們執(zhí)行以下命令,將 CRD 安裝到你開發(fā)機(jī)器當(dāng)前連接的 Kubernetes 集群:

    make install

    之后就能看到你的集群里被安裝了自己定義的 CRD:

    現(xiàn)在我們可以啟動(dòng)我們的控制器程序了,由于筆者使用的是 GoLand IDE,所以我直接點(diǎn)擊啟動(dòng)了 main.go 里的 main 函數(shù):

    讀者可根據(jù)自己的開發(fā)工具,選擇使用自己的啟動(dòng)方式,總之就是運(yùn)行該 Go 程序即可。也可以直接使用下面的命令啟動(dòng):

    make run

    在本地啟動(dòng)控制器之后,你的開發(fā)機(jī)器就是該 DemoMicroService 的控制器了,你連接的 Kubernetes 集群會(huì)將有關(guān) DemoMicroService resource 的變更發(fā)送到你的開發(fā)機(jī)器上執(zhí)行,所以打斷點(diǎn)也會(huì)斷住。
    接下來我們驗(yàn)收一下我們之前寫的代碼是否正常工作,執(zhí)行命令:

    kubectl apply -f ./config/samples/devops_v1_demomicroservice.yaml

    我們會(huì)看到 Kubernetes 集群中出現(xiàn)了該 resource:

    以上的"dms"即我們剛才設(shè)置的"demomicroservice"的縮寫。
    以及伴隨著 DemoMicroService 創(chuàng)建的 Deployment 及其創(chuàng)建的 Pod:

    當(dāng)我們執(zhí)行刪除 dms 的命令時(shí),這些 deployment 和 pod 也會(huì)被刪掉。

    2.1.5.1 發(fā)布

    使用如下命令將控制器發(fā)布到你開發(fā)機(jī)器當(dāng)前連接的 Kubernetes 集群:

    make deploy

    之后 kubebuilder 會(huì)在集群中創(chuàng)建一個(gè)專門用于放該控制器的 namespace,在我們這個(gè)例子里,該 namespace 為 example-system,之后可以通過如下命令看到自己的控制器已經(jīng)被發(fā)布到你當(dāng)前連接的集群:

    kubectl get po -n example-system

    如果該 pod 發(fā)布失敗,那么多半是國內(nèi)連接不上 gcr.io 的鏡像倉庫導(dǎo)致的,在工程內(nèi)搜索"gcr.io"的鏡像倉庫,將其替換成你方便訪問的鏡像倉庫即可。

    2.2 總結(jié)

    我們在本節(jié)大致了解了 kubebuilder 腳手架的使用方法,和 kubebuilder 程序的開發(fā)方法。并實(shí)踐了一個(gè)實(shí)現(xiàn)了微服務(wù)的部署和下線的控制器。
    或許讀者會(huì)問,為什么不直接創(chuàng)建一個(gè) Deployment,而要用這么麻煩的方式來實(shí)現(xiàn)。那是因?yàn)樽远x的 CRD 能更好的抽象開發(fā)者的業(yè)務(wù)場景,比如在我們的這個(gè)例子中,我們的微服務(wù)只關(guān)心鏡像地址,其他的 Deployment 屬性全部可以默認(rèn),那么我們的 DemoMicroService 看上去就比 Deployment 清爽很多。
    除此之外,這樣做還有如下優(yōu)點(diǎn):

  • Kubernetes 的 resource 保證了執(zhí)行結(jié)果的一致性:Kubernetes 對(duì)于執(zhí)行 resource 天然的符合冪等性,并且其內(nèi)提供的 resourceVersion 機(jī)制也解決了并發(fā)執(zhí)行時(shí)帶來的結(jié)果不一致的問題,這些問題如果開發(fā)者自己去解,往往會(huì)費(fèi)時(shí)費(fèi)力,而且吃力不討好。
  • kubebuilder 的開發(fā)模型幫助開發(fā)者節(jié)省了大量工作量,這些工作量包括:監(jiān)聽 resource 變化,出錯(cuò) resource 的重試,以及必要的 YAML configurations 生成。

  • 這里值得一提的是 kubebuilder 的重試機(jī)制,如果你自定的 resource 執(zhí)行失敗,那么 kubebuilder 會(huì)幫助你重試直到該 resource 被成功執(zhí)行,這省去了你自己實(shí)現(xiàn)重試邏輯的工作量。
  • 調(diào)用鏈路安全可控,通過將你的業(yè)務(wù)邏輯沉淀成 CRD 和控制器,可以完全享受 Kubernetes 的 rbac 權(quán)限管控系統(tǒng),能更安全,方便和精細(xì)的管控你開發(fā)的控制器接口。
  • 由于我們的示例過于簡單,所以這些優(yōu)勢聽起來可能比較蒼白,在下一節(jié)我們到更復(fù)雜的運(yùn)維場景里之后,我們能對(duì)以上描述的這些優(yōu)勢有更深的體會(huì)。

    3. 運(yùn)維系統(tǒng)實(shí)現(xiàn)

    如果你只對(duì) operator pattern 及其實(shí)踐感興趣,并不關(guān)心運(yùn)維系統(tǒng)如何實(shí)現(xiàn),那么可以不讀本節(jié)。
    下圖展示了在我們開發(fā)的微服務(wù)平臺(tái)中,微服務(wù)運(yùn)維控制器所做的事情,讀者可以看到實(shí)現(xiàn)的這樣一個(gè) operator 在整個(gè)微服務(wù)平臺(tái)中的位置:

    上圖中的 dmsp 表示我們的微服務(wù)平臺(tái),而 dmsp-ops-operator 即該運(yùn)維系統(tǒng)的控制器??梢钥吹揭?yàn)?dmsp-ops-operator 的存在,用戶操作管控臺(tái)要下達(dá)的指令就很簡單,實(shí)際的運(yùn)維操作都由 dmsp-ops-operator 執(zhí)行即可。并且?dmsp-ops-operator 也作為 Kubernetes 集群里的能力沉淀到了技術(shù)棧的最下層,與上層的業(yè)務(wù)邏輯完全清晰的分離了開來。

    3.1 微服務(wù)的完整抽象

    在第2節(jié),我們實(shí)現(xiàn)了一個(gè) demo 微服務(wù),事實(shí)上那個(gè) demo 微服務(wù)只關(guān)心鏡像地址,這明顯是不夠的,所以我們實(shí)現(xiàn)了 MicroServiceDeploy CRD 及其控制器,能抽象和實(shí)現(xiàn)更多的運(yùn)維功能,一個(gè) MicroServiceDeploy CR 看起來如下所示:

    apiVersion: custom.ops/v1 kind: MicroServiceDeploy metadata:name: ms-sample-v1s0 spec:msName: "ms-sample" # 微服務(wù)名稱fullName: "ms-sample-v1s0" # 微服務(wù)實(shí)例名稱version: "1.0" # 微服務(wù)實(shí)例版本path: "v1" # 微服務(wù)實(shí)例的大版本,該字符串將出現(xiàn)在微服務(wù)實(shí)例的域名中image: "just a image url" # 微服務(wù)實(shí)例的鏡像地址replicas: 3 # 微服務(wù)實(shí)例的 replica 數(shù)量autoscaling: true # 該微服務(wù)是否開啟自動(dòng)擴(kuò)縮容功能needAuth: true # 訪問該微服務(wù)實(shí)例時(shí),是否需要租戶 base 認(rèn)證config: "password=88888888" # 該微服務(wù)實(shí)例的運(yùn)行時(shí)配置項(xiàng)creationTimestamp: "1535546718115" # 該微服務(wù)實(shí)例的創(chuàng)建時(shí)間戳resourceRequirements: # 該微服務(wù)實(shí)例要求的機(jī)器資源limits: # 該微服務(wù)實(shí)例會(huì)使用到的最大資源配置cpu: "2"memory: 4Girequests: # 該微服務(wù)實(shí)例至少要用到的資源配置cpu: "2"memory: 4Giidle: false # 是否進(jìn)入空載狀態(tài)

    而以上一個(gè) resource 實(shí)際上創(chuàng)建了很多其他的 Kubernetes resource,這些 Kubernetes resource 才真正構(gòu)成了該微服務(wù)實(shí)際的能力。創(chuàng)建這些 Kubernetes resource 的方式基本上就是第2節(jié)講解的方式。
    下面我將分開介紹這些 Kubernetes resource,并分別說明這些 Kubernetes resource 的意義和作用。

    3.2 Service&ServiceAccount&Deployment

    首先是對(duì)于一個(gè)微服務(wù)而言必備的 Service, ServiceAccount 和 Deployment。這三種 resource 大家應(yīng)該已經(jīng)很熟悉了,這里就不過多說明,直接貼出由 MicroServiceDeploy 控制器創(chuàng)建出的 YAML 配置。

    3.2.1 Service&ServiceAccount

    apiVersion: v1 kind: Service metadata:labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample spec:ports:- name: httpport: 9898protocol: TCPtargetPort: 9898selector:app: ms-sample status:loadBalancer: {} --- apiVersion: v1 kind: ServiceAccount metadata:labels:my-domain-ops-controller-make: "true"name: ms-sample

    上面的?my-domain-ops-controller-make 是自定義控制器自己打上的 label,用于區(qū)分該 resource 是我們的自定義控制器創(chuàng)建的。

    3.2.2 Deployment

    apiVersion: apps/v1 kind: Deployment metadata:annotations:app.ops.my.domain/last-rollout-at: "1234"labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample-v1s0 spec:replicas: 1selector:matchLabels:app: ms-sampletype: RollingUpdatetemplate:metadata:annotations:app.ops.my.domain/create-at: "1234"prometheus.io/scrape: "true"labels:app: ms-samplespec:containers:- env:- name: POD_NAMEvalueFrom:fieldRef:apiVersion: v1fieldPath: metadata.name- name: SERVICE_NAMEvalueFrom:fieldRef:apiVersion: v1fieldPath: metadata.labels['app']image: "just a image url"imagePullPolicy: Alwaysname: ms-sampleports:- containerPort: 9898protocol: TCPresources:limits:cpu: 100mmemory: 400Mirequests:cpu: 100mmemory: 400MivolumeMounts:- mountPath: /home/admin/logsname: log- mountPath: /home/admin/confname: configinitContainers:- command:- sh- -c- chmod -R 777 /home/admin/logs || exit 0image: busyboximagePullPolicy: Alwaysname: log-volume-mount-hackvolumeMounts:- mountPath: /home/admin/logsname: logvolumes:- hostPath:path: /data0/logs/ms-sample/1.0type: DirectoryOrCreatename: log- configMap:defaultMode: 420name: ms-sample-v1s0name: config

    上面的 Deployment 有三點(diǎn)需要說明:

  • Pod 里 app.ops.my.domain/create-at 的?annotation 是控制器給 Pod 打上的注釋,用于強(qiáng)制讓該 Deployment 下的 Pods重啟,這樣即使 Deployment apply 時(shí)沒有其他變化,這些 Pods 也會(huì)被重啟,這在需要 Pods 被強(qiáng)制重啟時(shí)很有用。
  • 上面 name 為 log 的 volume 表示日志的 volume 掛載,代表容器內(nèi)的 /home/admin/logs 目錄里收集的日志會(huì)同步到宿主機(jī)的?/data0/logs/ms-sample/1.0 目錄下。要讓這個(gè)機(jī)制成立,需要你容器里的服務(wù)確保將日志打到 /home/admin/logs 目錄下。
  • 上面 name 為 config 的 volume 掛載,表示將 name 為 ms-sample-v1s0 的 ConfigMap 配置掛載到容器里的 /home/admin/conf 目錄下,這樣你在你的容器里通過讀取 /home/admin/conf 目錄下的配置文件,就能在容器中讀取到運(yùn)行時(shí)配置。詳情將在 3.3 節(jié)里說明。
  • 3.3 代表運(yùn)行時(shí)配置項(xiàng)的ConfigMap

    在 3.2.2 節(jié)提到的 ms-sample-v1s0 ConfigMap 如下所示:

    apiVersion: v1 data:ms.properties: name=foo kind: ConfigMap metadata:labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample-v1s0

    上述 ConfigMap 當(dāng) mount 到容器內(nèi)的 /home/admin/conf 下時(shí),就會(huì)在 /home/admin/conf 下創(chuàng)建一個(gè) ms.properties 文件,該文件的內(nèi)容就是"name=foo"。此時(shí)容器內(nèi)部便可以通過讀取該文件來獲取運(yùn)行時(shí)配置。而且該配置是動(dòng)態(tài)實(shí)時(shí)更新的,即 ConfigMap 變化了,容器里的文件內(nèi)容也會(huì)變化,這樣就可以做到即使容器不重啟,最新的配置也會(huì)生效。

    3.3 資源管理

    在 Kubernetes 中,資源這一名詞一般指代系統(tǒng)默認(rèn)的機(jī)器資源,即:cpu 與 memory。
    這里的資源管理是指對(duì)微服務(wù)部署的 namespace 進(jìn)行資源總量的管控,以及對(duì)每個(gè)微服務(wù)部署的容器做資源限制。用于實(shí)現(xiàn)這一目的的 YAML 為:

    apiVersion: v1 kind: ResourceQuota metadata:labels:my-domain-ops-controller-make: "true"name: default-resource-quotanamespace: default spec:hard:requests.cpu: "88"limits.cpu: "352"requests.memory: 112Gilimits.memory: 448Gi --- apiVersion: v1 kind: LimitRange metadata:labels:my-domain-ops-controller-make: "true"name: default-limit-rangenamespace: default spec:limits:- default:cpu: 400mmemory: 2GidefaultRequest:cpu: 100mmemory: 500Mimax:cpu: "2"memory: 8Gitype: Container

    與其他的 Kubernetes resource 不同的是,上面兩個(gè) resource 并不是在部署?MicroServiceDeploy CR 時(shí)創(chuàng)建的,而是在控制器部署時(shí)創(chuàng)建的,作為針對(duì)集群內(nèi)某一 namespace 的配置。所以你需要在控制器的 init 方法中去 create 上面兩個(gè) resource。
    上面的?ResourceQuota 限制了 default namespace 所能占用的資源額度總量。
    而?LimitRange 限制了 default namespace 中所有沒有限制資源量的容器所能占用的資源額度。之所以要為每個(gè)讓容器有缺省的資源額度,原因在于 Kubernetes 會(huì)對(duì)根據(jù)資源配置的情況對(duì) Pod 做分級(jí):如果一個(gè) Pod 沒有被配置資源量,則該 Pod 重要性最低;其次是分配了資源配置,但是 limits != requests 的 Pod;最后是分配了資源配置,而且 limits == requests 的 Pod,其重要性最高。Kubernetes 會(huì)在資源總量不足時(shí),將重要性更低的 Pod 釋放掉,用于調(diào)度更重要的 Pod。
    以上描述的三個(gè)等級(jí)分別稱作:BestEffort(優(yōu)先級(jí)最低),Burstable,Guaranteed(優(yōu)先級(jí)最高)。該等級(jí)稱作 QoS(Quality of Service) 等級(jí)。你可以在 Kubernetes Pod resource 的 status.qosClass 字段里查看該 Pod 的 QoS 等級(jí)。

    3.4 HPA自動(dòng)擴(kuò)縮容

    用于實(shí)現(xiàn)自動(dòng)擴(kuò)縮容的 HPA(HorizontalPodAutoscaler) resource 也是在部署?MicroServiceDeploy CR 時(shí)創(chuàng)建的,他針對(duì)的是這個(gè) MicroServiceDeploy CR 代表的微服務(wù):

    apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata:labels:app: ms-samplemy-domain-ops-controller-make: "true"name: ms-sample-v1s0 spec:maxReplicas: 10minReplicas: 1scaleTargetRef:apiVersion: apps/v1kind: Deploymentname: ms-sample-v1s0targetCPUUtilizationPercentage: 81

    上面的 HPA resource 表示將根據(jù) cpu 使用率來對(duì) ms-sample-v1s0 Deployment 下的 Pod 進(jìn)行擴(kuò)縮容,并且 Pod 數(shù)的區(qū)間在:[1, 10]。
    關(guān)于自動(dòng)擴(kuò)縮容可以講的比較多,我會(huì)單寫一篇文章詳細(xì)的來說明這一塊內(nèi)容。

    4. 參考資料

    《Kubernetes in Action》——電子工業(yè)出版社
    Kubernetes 對(duì) Operator 的官方解釋:https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
    kubebuilder 使用手冊:https://book.kubebuilder.io/quick-start.html
    本文所有示例代碼開源于:https://github.com/l4wei/kubebuilder-example

    數(shù)加平臺(tái)&DataWorks團(tuán)隊(duì)2020屆校招實(shí)習(xí)生報(bào)名已經(jīng)開始!
    加入我們將有機(jī)會(huì)參與大數(shù)據(jù),人工智能,算法,云計(jì)算,深度學(xué)習(xí),機(jī)器學(xué)習(xí),WebIDE 的產(chǎn)品化以相關(guān)的國內(nèi)領(lǐng)先的創(chuàng)新工程, 將在專業(yè)師兄指導(dǎo)下快速成長。
    簡歷精準(zhǔn)投遞,快速進(jìn)入面試通道;
    請(qǐng)將簡歷發(fā)送至:?dataplus-develop-recruit@list.alibaba-inc.com
    郵件標(biāo)題:“【實(shí)習(xí)報(bào)名】姓名-學(xué)校-專業(yè)-職位-期望base地(北京或杭州)”
    有其他疑問歡迎郵件咨詢。

    作者信息:
    吳謀,花名四唯,阿里云智能-計(jì)算平臺(tái)事業(yè)部技術(shù)專家,負(fù)責(zé)數(shù)加平臺(tái) &DataWorks 的微服務(wù)生態(tài)建設(shè),目前主要關(guān)注 Kubernetes、微服務(wù)等相關(guān)技術(shù)方向。

    原文鏈接
    本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。

    總結(jié)

    以上是生活随笔為你收集整理的Kubernetes operator 模式开发实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。