首頁>科技>

上個月,KubeVela 正式釋出了, 作為一款簡單易用且高度可擴充套件的應用管理平臺與核心引擎,可以說是廣大平臺工程師用來構建自己的雲原生 PaaS 的神兵利器。 那麼本文就以一個實際的例子,講解一下如何在 20 分鐘內,為你基於 KubeVela 的 PaaS “上線“一個新能力。

在正式開始本文件的教程之前,請確保你本地已經正確安裝了 KubeVela 及其依賴的 K8s 環境。

KubeVela 擴充套件的基本結構

KubeVela 的基本架構如圖所示:

簡單來說,KubeVela 透過新增 Workload TypeTrait 來為使用者擴充套件能力,平臺的服務提供方透過 Definition 檔案註冊和擴充套件,向上透過 Appfile 透出擴充套件的功能。官方文件中也分別給出了基本的編寫流程,其中 2 個是 Workload 的擴充套件例子,一個是 Trait 的擴充套件例子:

OpenFaaS 為例的 Workload Type 擴充套件雲資源 RDS 為例的 Workload Type 擴充套件KubeWatch 為例的 Trait 擴充套件

我們以一個內建的 WorkloadDefinition 為例來介紹一下 Definition 檔案的基本結構:

apiVersion: core.oam.dev/v1alpha2kind: WorkloadDefinitionmetadata:  name: webservice  annotations:    definition.oam.dev/description: "`Webservice` is a workload type to describe long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers.    If workload type is skipped for any service defined in Appfile, it will be defaulted to `Web Service` type."spec:  definitionRef:    name: deployments.apps  extension:    template: |      output: {          apiVersion: "apps/v1"          kind:       "Deployment"          spec: {              selector: matchLabels: {                  "app.oam.dev/component": context.name              }              template: {                  metadata: labels: {                      "app.oam.dev/component": context.name                  }                  spec: {                      containers: [{                          name:  context.name                          image: parameter.image                          if parameter["cmd"] != _|_ {                              command: parameter.cmd                          }                          if parameter["env"] != _|_ {                              env: parameter.env                          }                          if context["config"] != _|_ {                              env: context.config                          }                          ports: [{                              containerPort: parameter.port                          }]                          if parameter["cpu"] != _|_ {                              resources: {                                  limits:                                      cpu: parameter.cpu                                  requests:                                      cpu: parameter.cpu                              }}                      }]              }}}      }      parameter: {          // +usage=Which image would you like to use for your service          // +short=i          image: string          // +usage=Commands to run in the container          cmd?: [...string]          // +usage=Which port do you want customer traffic sent to          // +short=p          port: *80 | int          // +usage=Define arguments by using environment variables          env?: [...{              // +usage=Environment variable name              name: string              // +usage=The value of the environment variable              value?: string              // +usage=Specifies a source the value of this var should come from              valueFrom?: {                  // +usage=Selects a key of a secret in the pod's namespace                  secretKeyRef: {                      // +usage=The name of the secret in the pod's namespace to select from                      name: string                      // +usage=The key of the secret to select from. Must be a valid secret key                      key: string                  }              }          }]          // +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)          cpu?: string      }

乍一看挺長的,好像很複雜,但是不要著急,其實細看之下它分為兩部分:

不含擴充套件欄位的 Definition 註冊部分供 Appfile 使用的擴充套件模板(CUE Template)部分

我們拆開來慢慢介紹,其實學起來很簡單。

不含擴充套件欄位的 Definition 註冊部分
apiVersion: core.oam.dev/v1alpha2kind: WorkloadDefinitionmetadata:  name: webservice  annotations:    definition.oam.dev/description: "`Webservice` is a workload type to describe long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers.    If workload type is skipped for any service defined in Appfile, it will be defaulted to `Web Service` type."spec:  definitionRef:    name: deployments.apps

這一部分滿打滿算 11 行,其中有 3 行是在介紹 webservice 的功能,5行是固定的格式。只有 2 行是有特定資訊:

  definitionRef:    name: deployments.apps

這兩行的意思代表了這個 Definition 背後用的 CRD 名稱是什麼,其格式是 <resources>.<api-group>。瞭解 K8s 的同學應該知道 K8s 中比較常用的是透過 api-group, version 和 kind 定位資源,而 kind 在 K8s restful API 中對應的是 resources。以大家熟悉 Deployment 和 ingress 為例,它的對應關係如下:

這裡補充一個小知識,為什麼有了 kind 還要加個 resources 的概念呢? 因為一個 CRD 除了 kind 本身還有一些像 status,replica 這樣的欄位希望跟 spec 本身解耦開來在 restful API 中單獨更新, 所以 resources 除了 kind 對應的那一個,還會有一些額外的 resources,如 Deployment 的 status 表示為 deployments/status。

所以相信聰明的你已經明白了不含 extension 的情況下,Definition 應該怎麼寫了,最簡單的就是根據 K8s 的資源組合方式拼接一下,只要填下面三個尖括號的空格就可以了。

apiVersion: core.oam.dev/v1alpha2kind: WorkloadDefinitionmetadata:  name: <這裡寫名稱>spec:  definitionRef:    name: <這裡寫resources>.<這裡寫api-group>

針對運維特徵註冊(TraitDefinition)也是這樣。

apiVersion: core.oam.dev/v1alpha2kind: TraitDefinitionmetadata:  name: <這裡寫名稱>spec:  definitionRef:    name: <這裡寫resources>.<這裡寫api-group>

所以把 Ingress 作為 KubeVela 的擴充套件寫進去就是:

apiVersion: core.oam.dev/v1alpha2kind: TraitDefinitionmetadata:  name:  ingressspec:  definitionRef:    name: ingresses.networking.k8s.io

除此之外,TraitDefinition 中還增加了一些其他功能模型層功能,如:

appliesToWorkloads: 表示這個 trait 可以作用於哪些 Workload 型別。conflictWith: 表示這個 trait 和哪些其他型別的 trait 有衝突。workloadRefPath: 表示這個 trait 包含的 workload 欄位是哪個,KubeVela 在生成 trait 物件時會自動填充。 ...

這些功能都是可選的,本文中不涉及使用,在後續的其他文章中我們再給大家詳細介紹。

所以到這裡,相信你已經掌握了一個不含 extensions 的基本擴充套件模式,而剩下部分就是圍繞 CUE 的抽象模板。

供 Appfile 使用的擴充套件模板(CUE Template)部分

對 CUE 本身有興趣的同學可以參考這篇 CUE 基礎入門 多做一些瞭解,限於篇幅本文對 CUE 本身不詳細展開。

大家知道 KubeVela 的 Appfile 寫起來很簡潔,但是 K8s 的物件是一個相對比較複雜的 YAML,而為了保持簡潔的同時又不失可擴充套件性,KubeVela 提供了一個從複雜到簡潔的橋樑。 這就是 Definition 中 CUE Template 的作用。

CUE 格式模板

讓我們先來看一個 Deployment 的 YAML 檔案,如下所示,其中很多內容都是固定的框架(模板部分),真正需要使用者填的內容其實就少量的幾個欄位(引數部分)。

apiVersion: apps/v1kind: Deploymentmeadata:  name: mytestspec:  template:    spec:      containers:      - name: mytest        env:        - name: a          value: b        image: nginx:v1    metadata:      labels:        app.oam.dev/component: mytest  selector:    matchLabels:      app.oam.dev/component: mytest

在 KubeVela 中,Definition 檔案的固定格式就是分為 output 和 parameter 兩部分。其中output中的內容就是“模板部分”,而 parameter 就是引數部分。

那我們來把上面的 Deployment YAML 改寫成 Definition 中模板的格式。

output: {    apiVersion: "apps/v1"    kind:       "Deployment"    metadata: name: "mytest"    spec: {        selector: matchLabels: {            "app.oam.dev/component": "mytest"        }        template: {            metadata: labels: {                "app.oam.dev/component": "mytest"            }            spec: {                containers: [{                    name:  "mytest"                    image: "nginx:v1"                    env: [{name:"a",value:"b"}]                }]            }}}}

這個格式跟 json 很像,事實上這個是 CUE 的格式,而 CUE 本身就是一個 json 的超集。也就是說,CUE的格式在滿足 JSON 規則的基礎上,增加了一些簡便規則, 使其更易讀易用:

C 語言的註釋風格。表示欄位名稱的雙引號在沒有特殊符號的情況下可以預設。欄位值結尾的逗號可以預設,在欄位最後的逗號寫了也不會出錯。最外層的大括號可以省略。CUE 格式的模板引數--變數引用

編寫好了模板部分,讓我們來構建引數部分,而這個引數其實就是變數的引用。

parameter: {    name: string    image: string}output: {    apiVersion: "apps/v1"    kind:       "Deployment"    spec: {        selector: matchLabels: {            "app.oam.dev/component": parameter.name        }        template: {            metadata: labels: {                "app.oam.dev/component": parameter.name            }            spec: {                containers: [{                    name:  parameter.name                    image: parameter.image                }]            }}}}

如上面的這個例子所示,KubeVela 中的模板引數就是透過 parameter 這個部分來完成的,而 parameter 本質上就是作為引用,替換掉了 output 中的某些欄位。

完整的 Definition 以及在 Appfile 使用

事實上,經過上面兩部分的組合,我們已經可以寫出一個完整的 Definition 檔案:

apiVersion: core.oam.dev/v1alpha2kind: WorkloadDefinitionmetadata:  name: mydeployspec:  definitionRef:    name: deployments.apps  extension:    template: |        parameter: {            name: string            image: string        }        output: {            apiVersion: "apps/v1"            kind:       "Deployment"            spec: {                selector: matchLabels: {                    "app.oam.dev/component": parameter.name                }                template: {                    metadata: labels: {                        "app.oam.dev/component": parameter.name                    }                    spec: {                        containers: [{                            name:  parameter.name                            image: parameter.image                        }]                    }}}        }

為了方便除錯,一般情況下可以預先分為兩個檔案,一部分放前面的 yaml 部分,假設命名為 def.yaml 如:

apiVersion: core.oam.dev/v1alpha2kind: WorkloadDefinitionmetadata:  name: mydeployspec:  definitionRef:    name: deployments.apps  extension:    template: |

另一個則放 cue 檔案,假設命名為 def.cue :

parameter: {    name: string    image: string}output: {    apiVersion: "apps/v1"    kind:       "Deployment"    spec: {        selector: matchLabels: {            "app.oam.dev/component": parameter.name        }        template: {            metadata: labels: {                "app.oam.dev/component": parameter.name            }            spec: {                containers: [{                    name:  parameter.name                    image: parameter.image                }]            }}}}

先對 def.cue 做一個格式化,格式化的同時 cue 工具本身會做一些校驗,也可以更深入的透過 cue 命令做除錯:

cue fmt def.cue

除錯完成後,可以透過指令碼把這個 yaml 組裝:

./hack/vela-templates/mergedef.sh def.yaml def.cue > mydeploy.yaml

再把這個 yaml 檔案 apply 到 K8s 叢集中。

$ kubectl apply -f mydeploy.yamlworkloaddefinition.core.oam.dev/mydeploy created

一旦新能力 kubectl apply 到了 Kubernetes 中,不用重啟,也不用更新,KubeVela 的使用者可以立刻看到一個新的能力出現並且可以使用了:

$ vela worklaodsAutomatically discover capabilities successfully ✅ Add(1) Update(0) Delete(0)TYPE           CATEGORY    DESCRIPTION+mydeploy      workload    description not definedNAME        DESCRIPTIONmydeploy    description not defined

在 Appfile 中使用方式如下:

name: my-extend-appservices:  mysvc:    type: mydeploy    image: crccheck/hello-world    name: mysvc

執行 vela up 就能把這個執行起來了:

$ vela up -f docs/examples/blog-extension/my-extend-app.yamlParsing vela appfile ...Loading templates ...Rendering configs for service (mysvc)...Writing deploy config to (.vela/deploy.yaml)Applying deploy configs ...Checking if app has been deployed...App has not been deployed, creating a new deployment...✅ App has been deployed     Port forward: vela port-forward my-extend-app             SSH: vela exec my-extend-app         Logging: vela logs my-extend-app      App status: vela status my-extend-app  Service status: vela status my-extend-app --svc mysvc

我們來檢視一下應用的狀態,已經正常執行起來了(HEALTHY Ready: 1/1):

$ vela status my-extend-appAbout:  Name:          my-extend-app  Namespace:     env-application  Created at:    2020-12-15 16:32:25.08233 +0800 CST  Updated at:    2020-12-15 16:32:25.08233 +0800 CSTServices:  - Name: mysvc    Type: mydeploy    HEALTHY Ready: 1/1
Definition 模板中的高階用法

上面我們已經透過模板替換這個最基本的功能體驗了擴充套件 KubeVela 的全過程,除此之外,可能你還有一些比較複雜的需求,如條件判斷,迴圈,複雜型別等,需要一些高階的用法。

結構體引數

如果模板中有一些引數型別比較複雜,包含結構體和巢狀的多個結構體,就可以使用結構體定義。

定義一個結構體型別,包含 1 個字串成員、1 個整型和 1 個結構體成員。
#Config: { name:  string value: int other: {   key: string   value: string }}
在變數中使用這個結構體型別,並作為陣列使用。
parameter: { name: string image: string config: [...#Config]}
同樣的目標中也是以變數引用的方式使用。
output: {   ...         spec: {             containers: [{                 name:  parameter.name                 image: parameter.image                 env: parameter.config             }]         }    ...}
Appfile 中的寫法就是按照 parameter 定義的結構編寫。
name: my-extend-appservices:mysvc: type: mydeploy image: crccheck/hello-world name: mysvc config: - name: a   value: 1   other:     key: mykey     value: myvalue
條件判斷

有時候某些引數加還是不加取決於某個條件:

parameter: {    name:   string    image:  string    useENV: bool}output: {    ...    spec: {        containers: [{            name:  parameter.name            image: parameter.image            if parameter.useENV == true {                env: [{name: "my-env", value: "my-value"}]            }        }]    }    ...}

在 Appfile 就是寫值。

name: my-extend-appservices:  mysvc:    type: mydeploy    image: crccheck/hello-world    name: mysvc    useENV: true
可預設引數

有些情況下引數可能存在也可能不存在,即非必填,這個時候一般要配合條件判斷使用,對於某個欄位不存在的情況,判斷條件是是 _variable != _|_。

parameter: {    name: string    image: string    config?: [...#Config]}output: {    ...    spec: {        containers: [{            name:  parameter.name            image: parameter.image            if parameter.config != _|_ {                config: parameter.config            }        }]    }    ...}

這種情況下 Appfile 的 config 就非必填了,填了就渲染,沒填就不渲染。

預設值

對於某些引數如果希望設定一個預設值,可以採用這個寫法。

parameter: {    name: string    image: *"nginx:v1" | string}output: {    ...    spec: {        containers: [{            name:  parameter.name            image: parameter.image        }]    }    ...}

這個時候 Appfile 就可以不寫 image 這個引數,預設使用 "nginx:v1":

name: my-extend-appservices:  mysvc:    type: mydeploy    name: mysvc
迴圈Map 型別的迴圈
parameter: {    name:  string    image: string    env: [string]: string}output: {    spec: {        containers: [{            name:  parameter.name            image: parameter.image            env: [                for k, v in parameter.env {                    name:  k                    value: v                },            ]        }]    }}

Appfile 中的寫法:

name: my-extend-appservices:  mysvc:    type: mydeploy    name:  "mysvc"    image: "nginx"    env:      env1: value1      env2: value2
陣列型別的迴圈
parameter: {    name:  string    image: string    env: [...{name:string,value:string}]}output: {  ...     spec: {        containers: [{            name:  parameter.name            image: parameter.image            env: [                for _, v in parameter.env {                    name:  v.name                    value: v.value                },            ]        }]    }}

Appfile 中的寫法:

name: my-extend-appservices:  mysvc:    type: mydeploy    name:  "mysvc"    image: "nginx"    env:    - name: env1      value: value1    - name: env2      value: value2
KubeVela 內建的 context 變數

大家可能也注意到了,我們在 parameter 中定義的 name 每次在 Appfile中 實際上寫了兩次,一次是在 services 下面(每個service都以名稱區分), 另一次則是在具體的name引數裡面。事實上這裡重複的不應該由使用者再寫一遍,所以 KubeVela 中還定義了一個內建的 context,裡面存放了一些通用的環境上下文資訊,如應用名稱、秘鑰等。 直接在模板中使用 context 就不需要額外增加一個 name 引數了, KubeVela 在執行渲染模板的過程中會自動傳入。

parameter: {    image: string}output: {  ...    spec: {        containers: [{            name:  context.name            image: parameter.image        }]    }  ...}
KubeVela 中的註釋增強

KubeVela 還對 cuelang 的註釋做了一些擴充套件,方便自動生成文件以及被 CLI 使用。

 parameter: {          // +usage=Which image would you like to use for your service          // +short=i          image: string          // +usage=Commands to run in the container          cmd?: [...string]       ...      }

其中,+usgae 開頭的註釋會變成引數的說明,+short 開頭的註釋後面則是在 CLI 中使用的縮寫。

總結

本文透過實際的案例和詳細的講述,為你介紹了在 KubeVela 中新增一個能力的詳細過程與原理,以及能力模板的編寫方法。

這裡你可能還有個疑問,平臺管理員這樣添加了一個新能力後,平臺的使用者又該怎麼能知道這個能力怎麼使用呢?其實,在 KubeVela 中,它不僅能方便的新增新能力,它還能自動為“能力”生成 Markdown 格式的使用文件! 不信,你可以看下 KubeVela 本身的官方網站,所有在 References/Capabilities 目錄下能力使用說明文件(比如這個),全都是根據每個能力的模板自動生成的哦。 最後,歡迎大家寫一些有趣的擴充套件功能,提交到 KubeVela 的社群倉庫中來。

12
最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • 瓜瓜龍英語僅售1.9元?廣告做得好,效果呢?家長:買了個寂寞