自定义组件

自定义组件 - 图1提示

在阅读本部分之前,请确保你已经了解 KubeVela 中 组件定义(ComponentDefinition) 的概念且学习掌握了 CUE 的基本知识

本节将以组件定义的例子展开说明,介绍如何使用 CUE 通过组件定义 ComponentDefinition 来自定义应用部署计划的组件。

我们可以通过 vela def init 来根据已有的 YAML 文件来生成一个 ComponentDefinition 模板。

YAML 文件:

  1. apiVersion: "apps/v1"
  2. kind: "Deployment"
  3. spec:
  4. selector:
  5. matchLabels:
  6. "app.oam.dev/component": "name"
  7. template:
  8. metadata:
  9. labels:
  10. "app.oam.dev/component": "name"
  11. spec:
  12. containers:
  13. - name: "name"
  14. image: "image"

根据以上的 YAML 来生成 ComponentDefinition

  1. vela def init stateless -t component --template-yaml ./stateless.yaml -o stateless.cue

得到如下结果:

stateless.cue

  1. stateless: {
  2. annotations: {}
  3. attributes: workload: definition: {
  4. apiVersion: "<change me> apps/v1"
  5. kind: "<change me> Deployment"
  6. }
  7. description: ""
  8. labels: {}
  9. type: "component"
  10. }
  11. template: {
  12. output: {
  13. spec: {
  14. selector: matchLabels: "app.oam.dev/component": "name"
  15. template: {
  16. metadata: labels: "app.oam.dev/component": "name"
  17. spec: containers: [{
  18. name: "name"
  19. image: "image"
  20. }]
  21. }
  22. }
  23. apiVersion: "apps/v1"
  24. kind: "Deployment"
  25. }
  26. outputs: {}
  27. parameter: {}
  28. }

在这个自动生成的模板中:

  • The stateless is the name of component definition, it can be defined by yourself when initialize the component.
  • stateless.attributes.workload indicates the workload type of this component, it can help integrate with traits that apply to this kind of workload.
  • template is a CUE template, specifically:
    • The output and outputs fields define the resources that the component will be composed.
    • The parameter field defines the parameters of the component, i.e. the configurable properties exposed in the Application (and schema will be automatically generated based on them for end users to learn this component).

下面我们来给这个自动生成的自定义组件添加参数并进行赋值:

  1. stateless: {
  2. annotations: {}
  3. attributes: workload: definition: {
  4. apiVersion: "apps/v1"
  5. kind: "Deployment"
  6. }
  7. description: ""
  8. labels: {}
  9. type: "component"
  10. }
  11. template: {
  12. output: {
  13. spec: {
  14. selector: matchLabels: "app.oam.dev/component": parameter.name
  15. template: {
  16. metadata: labels: "app.oam.dev/component": parameter.name
  17. spec: containers: [{
  18. name: parameter.name
  19. image: parameter.image
  20. }]
  21. }
  22. }
  23. apiVersion: "apps/v1"
  24. kind: "Deployment"
  25. }
  26. outputs: {}
  27. parameter: {
  28. name: string
  29. image: string
  30. }
  31. }

修改后可以用 vela def vet 做一下格式检查和校验。

  1. vela def vet stateless.cue

期望输出

  1. Validation succeed.

Apply above ComponentDefinition to your Kubernetes cluster to make it work:

  1. vela def apply stateless.cue

expected output

  1. ComponentDefinition stateless created in namespace vela-system.

Then the end user can check the schema and use it in an application now:

  1. vela show stateless

expected output

  1. # Specification
  2. +-------+-------------+--------+----------+---------+
  3. | NAME | DESCRIPTION | TYPE | REQUIRED | DEFAULT |
  4. +-------+-------------+--------+----------+---------+
  5. | name | | string | true | |
  6. | image | | string | true | |
  7. +-------+-------------+--------+----------+---------+

接着,让我们声明另一个名为 task 的组件,其原理类似。

点击查看声明 task 组件的创建过程。

  1. vela def init task -t component -o task.cue

得到如下结果:

  1. $ cat task.cue
  2. task: {
  3. annotations: {}
  4. attributes: workload: definition: {
  5. apiVersion: "<change me> apps/v1"
  6. kind: "<change me> Deployment"
  7. }
  8. description: ""
  9. labels: {}
  10. type: "component"
  11. }
  12. template: {
  13. output: {}
  14. parameter: {}
  15. }

修改该组件定义:

  1. task: {
  2. annotations: {}
  3. attributes: workload: definition: {
  4. apiVersion: "batch/v1"
  5. kind: "Job"
  6. }
  7. description: ""
  8. labels: {}
  9. type: "component"
  10. }
  11. template: {
  12. output: {
  13. apiVersion: "batch/v1"
  14. kind: "Job"
  15. spec: {
  16. parallelism: parameter.count
  17. completions: parameter.count
  18. template: spec: {
  19. restartPolicy: parameter.restart
  20. containers: [{
  21. image: parameter.image
  22. if parameter["cmd"] != _|_ {
  23. command: parameter.cmd
  24. }
  25. }]
  26. }
  27. }
  28. }
  29. parameter: {
  30. count: *1 | int
  31. image: string
  32. restart: *"Never" | string
  33. cmd?: [...string]
  34. }
  35. }

将以上两个组件定义部署到集群中:

  1. $ vela def apply task.cue
  2. ComponentDefinition task created in namespace vela-system.

这两个已经定义好的组件,最终会在应用部署计划中实例化,我们引用自定义的组件类型 stateless,命名为 hello。同样,我们也引用了自定义的第二个组件类型 task,并命令为 countdown

然后把它们编写到应用部署计划中,如下所示:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: Application
  3. metadata:
  4. name: website
  5. spec:
  6. components:
  7. - name: hello
  8. type: stateless
  9. properties:
  10. image: oamdev/hello-world
  11. name: mysvc
  12. - name: countdown
  13. type: task
  14. properties:
  15. image: centos:7
  16. cmd:
  17. - "bin/bash"
  18. - "-c"
  19. - "for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done"

以上,我们就完成了一个自定义应用组件的应用交付全过程。值得注意的是,作为管理员的我们,可以通过 CUE 提供用户所需要的任何自定义组件类型,同时也为用户提供了模板参数 parameter 来灵活地指定对 Kubernetes 相关资源的要求。

了解背后的 Kubernetes 最终资源信息

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: backend
  5. ... # 隐藏一些与本小节讲解无关的信息
  6. spec:
  7. template:
  8. spec:
  9. containers:
  10. - name: mysvc
  11. image: oamdev/hello-world
  12. metadata:
  13. labels:
  14. app.oam.dev/component: mysvc
  15. selector:
  16. matchLabels:
  17. app.oam.dev/component: mysvc
  18. ---
  19. apiVersion: batch/v1
  20. kind: Job
  21. metadata:
  22. name: countdown
  23. ... # 隐藏一些与本小节讲解无关的信息
  24. spec:
  25. parallelism: 1
  26. completions: 1
  27. template:
  28. metadata:
  29. name: countdown
  30. spec:
  31. containers:
  32. - name: countdown
  33. image: 'centos:7'
  34. command:
  35. - bin/bash
  36. - '-c'
  37. - for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done
  38. restartPolicy: Never

You can also use dry run to show what the yaml results will be rendered for debugging.

KubeVela 让你可以在运行时,通过 context 关键字来引用一些信息。

最常用的就是应用部署计划的名称 context.appName 和组件的名称 context.name

  1. context: {
  2. appName: string
  3. name: string
  4. }

举例来说,假设你在实现一个组件定义,希望将容器的名称填充为组件的名称。那么这样做:

  1. parameter: {
  2. image: string
  3. }
  4. output: {
  5. ...
  6. spec: {
  7. containers: [{
  8. name: context.name
  9. image: parameter.image
  10. }]
  11. }
  12. ...
  13. }

自定义组件 - 图2提示

注意 context 的信息会在资源部署到目标集群之前就自动注入了。

在本文的最后列出了完整的 context 变量列表。

除了上面这个例子外,一个组件的定义通常也会由多个 Kubernetes API 资源组成。例如,一个由 DeploymentService 组成的 webserver 组件。CUE 同样能很好的满足这种自定义复合组件的需求。

自定义组件 - 图3提示

Compare to using Helm, this approach gives your more flexibility as you can control the abstraction any time and integrate with traits, workflows in KubeVela better.

我们会使用 output 这个字段来定义工作负载类型的模板,而其他剩下的资源模板,都在 outputs 这个字段里进行声明,格式如下:

  1. output: {
  2. <template of main workload structural data>
  3. }
  4. outputs: {
  5. <unique-name>: {
  6. <template of auxiliary resource structural data>
  7. }
  8. }

自定义组件 - 图4备注

The reason for this requirement is KubeVela needs to know it is currently rendering a workload so it could do some “magic” by traits such like patching annotations/labels or other data during it.

回到 webserver 这个复合自定义组件上,它的 CUE 文件编写如下:

  1. webserver: {
  2. annotations: {}
  3. attributes: workload: definition: {
  4. apiVersion: "apps/v1"
  5. kind: "Deployment"
  6. }
  7. description: ""
  8. labels: {}
  9. type: "component"
  10. }
  11. template: {
  12. output: {
  13. apiVersion: "apps/v1"
  14. kind: "Deployment"
  15. spec: {
  16. selector: matchLabels: {
  17. "app.oam.dev/component": context.name
  18. }
  19. template: {
  20. metadata: labels: {
  21. "app.oam.dev/component": context.name
  22. }
  23. spec: {
  24. containers: [{
  25. name: context.name
  26. image: parameter.image
  27. if parameter["cmd"] != _|_ {
  28. command: parameter.cmd
  29. }
  30. if parameter["env"] != _|_ {
  31. env: parameter.env
  32. }
  33. if context["config"] != _|_ {
  34. env: context.config
  35. }
  36. ports: [{
  37. containerPort: parameter.port
  38. }]
  39. if parameter["cpu"] != _|_ {
  40. resources: {
  41. limits:
  42. cpu: parameter.cpu
  43. requests:
  44. cpu: parameter.cpu
  45. }
  46. }
  47. }]
  48. }
  49. }
  50. }
  51. }
  52. // an extra template
  53. outputs: service: {
  54. apiVersion: "v1"
  55. kind: "Service"
  56. spec: {
  57. selector: {
  58. "app.oam.dev/component": context.name
  59. }
  60. ports: [
  61. {
  62. port: parameter.port
  63. targetPort: parameter.port
  64. },
  65. ]
  66. }
  67. }
  68. parameter: {
  69. image: string
  70. cmd?: [...string]
  71. port: *80 | int
  72. env?: [...{
  73. name: string
  74. value?: string
  75. valueFrom?: {
  76. secretKeyRef: {
  77. name: string
  78. key: string
  79. }
  80. }
  81. }]
  82. cpu?: string
  83. }
  84. }

可以看到:

  1. 最核心的工作负载,我们按需要在 output 字段里,定义了一个要交付的 Deployment 类型的 Kubernetes 资源。
  2. Service 类型的资源,则放到 outputs 里定义。以此类推,如果你要复合第三个资源,只需要继续在后面以键值对的方式添加:
  1. outputs: service: {
  2. apiVersion: "v1"
  3. kind: "Service"
  4. spec: {
  5. ...
  6. outputs: third-resource: {
  7. apiVersion: "v1"
  8. kind: "Service"
  9. spec: {
  10. ...

在理解这些之后,将上面的组件定义对象保存到 CUE 文件中,并部署到你的 Kubernetes 集群。

  1. vela def apply webserver.cue

期望输出

  1. ComponentDefinition webserver created in namespace vela-system.

然后,我们使用它们,来编写一个应用部署计划:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: Application
  3. metadata:
  4. name: webserver-demo
  5. namespace: default
  6. spec:
  7. components:
  8. - name: hello-world
  9. type: webserver
  10. properties:
  11. image: oamdev/hello-world
  12. port: 8000
  13. env:
  14. - name: "foo"
  15. value: "bar"
  16. cpu: "100m"

进行部署:

  1. $ vela up -f webserver.yaml

最后,它将在运行时集群生成相关 Kubernetes 资源如下:

  1. vela status webserver-demo --tree --detail

期望输出

  1. CLUSTER NAMESPACE RESOURCE STATUS APPLY_TIME DETAIL
  2. local ─── default ─┬─ Service/hello-webserver-auxiliaryworkload-685d98b6d9 updated 2022-10-15 21:58:35 Type: ClusterIP
  3. Cluster-IP: 10.43.255.55
  4. External-IP: <none>
  5. Port(s): 8000/TCP
  6. Age: 66s
  7. └─ Deployment/hello-webserver updated 2022-10-15 21:58:35 Ready: 1/1 Up-to-date: 1
  8. Available: 1 Age: 66s

你可以通过自定义健康检查和状态信息,将自定义组件的真实状态反馈给最终用户。

定义健康检查的字段为 <component-type-name>.attributes.status.healthPolicy.

如果没有定义,它的值默认是 true,意味着在部署完对象后就将对象的状态设置为健康。为了让组件的状态及时、准确,通常你需要为组件定义监控状态,这个过程可以通过一个 CUE 表达式完成。

在 CUE 里的关键词是 isHealth,CUE 表达式结果必须是 bool 类型。 KubeVela 运行时会一直检查 CUE 表达式,直至其状态显示为健康。每次控制器都会获取所有的 Kubernetes 资源,并将他们填充到 context 字段中。

所以 context 字段会包含如下信息:

  1. context:{
  2. name: <component name>
  3. appName: <app name>
  4. output: <Kubernetes workload resource>
  5. outputs: {
  6. <resource1>: <Kubernetes trait resource1>
  7. <resource2>: <Kubernetes trait resource2>
  8. }
  9. }

我们看看健康检查的例子:

  1. webserver: {
  2. type: "component"
  3. ...
  4. attributes: {
  5. status: {
  6. healthPolicy: #"""
  7. isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == context.output.status.replicas)
  8. """#
  9. }
  10. }
  11. }

你也可以在健康检查中使用 parameter 中定义的参数,类似如下:

  1. webserver: {
  2. type: "component"
  3. ...
  4. attributes: {
  5. status: {
  6. healthPolicy: #"""
  7. isHealth: (context.output.status.readyReplicas > 0) && (context.output.status.readyReplicas == parameter.replicas)
  8. """#
  9. }
  10. }
  11. template: {
  12. parameter: {
  13. replicas: int
  14. }
  15. ...
  16. }

健康检查的结果会输出到 Application 对象的 .status.services 字段中。

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: Application
  3. status:
  4. ...
  5. services:
  6. - healthy: true
  7. name: myweb
  8. ...
  9. status: running

请参考文档 查阅更多示例。

自定义状态的字段未 <component-type-name>.attributes.status.customStatus, 自定义状态和健康检查的原理一致。

在 CUE 中的关键词是 message。同时,CUE 表达式的结果必须是 string 类型。

Application 对象的 CRD 控制器都会检查 CUE 表达式,直至显示健康通过。

The example of custom status likes below:

  1. webserver: {
  2. type: "component"
  3. ...
  4. attributes: {
  5. status: {
  6. customStatus: #"""
  7. ready: {
  8. readyReplicas: *0 | int
  9. } & {
  10. if context.output.status.readyReplicas != _|_ {
  11. readyReplicas: context.output.status.readyReplicas
  12. }
  13. }
  14. message: "Ready:\(ready.readyReplicas)/\(context.output.spec.replicas)"
  15. """#
  16. }
  17. }
  18. }

The message will be recorded into the corresponding component in .status.services of Application resource like below.

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: Application
  3. status:
  4. ...
  5. services:
  6. - healthy: false
  7. message: Ready:1/1
  8. name: express-server

请参考文档 查阅更多示例。

Context VariableDescriptionType
context.appNameThe app name corresponding to the current instance of the application.string
context.namespaceThe target namespace of the current resource is going to be deployed, it can be different with the namespace of application if overridden by some policies.string
context.clusterThe target cluster of the current resource is going to be deployed, it can be different with the namespace of application if overridden by some policies.string
context.appRevisionThe app version name corresponding to the current instance of the application.string
context.appRevisionNumThe app version number corresponding to the current instance of the application.int
context.nameThe component name corresponding to the current instance of the application.string
context.revisionThe version name of the current component instance.string
context.outputThe object structure after instantiation of current component.Object Map
context.outputs.<resourceName>Structure after instantiation of current component auxiliary resources.Object Map
context.workflowNameThe workflow name specified in annotation.string
context.publishVersionThe version of application instance specified in annotation.string
context.appLabelsThe labels of the current application instance.Object Map
context.appAnnotationsThe annotations of the current application instance.Object Map
context.replicaKeyThe key of replication in context. Replication is an internal policy, it will replicate resources with different keys specified. (This feature will be introduced in v1.6+.)string
Context VariableDescriptionType
context.clusterVersion.majorThe major version of the runtime Kubernetes cluster.string
context.clusterVersion.gitVersionThe gitVersion of the runtime Kubernetes cluster.string
context.clusterVersion.platformThe platform information of the runtime Kubernetes cluster.string
context.clusterVersion.minorThe minor version of the runtime Kubernetes cluster.int

The cluster version context info can be used for graceful upgrade of definition. For example, you can define different API according to the cluster version.

  1. outputs: ingress: {
  2. if context.clusterVersion.minor < 19 {
  3. apiVersion: "networking.k8s.io/v1beta1"
  4. }
  5. if context.clusterVersion.minor >= 19 {
  6. apiVersion: "networking.k8s.io/v1"
  7. }
  8. kind: "Ingress"
  9. }

Or use string contain pattern for this usage:

  1. import "strings"
  2. if strings.Contains(context.clusterVersion.gitVersion, "k3s") {
  3. provider: "k3s"
  4. }
  5. if strings.Contains(context.clusterVersion.gitVersion, "aliyun") {
  6. provider: "aliyun"
  7. }

KubeVela is fully programmable via CUE, while it leverage Kubernetes as control plane and align with the API in yaml. As a result, the CUE definition will be converted as Kubernetes API when applied into cluster.

The component definition will be in the following API format:

  1. apiVersion: core.oam.dev/v1beta1
  2. kind: ComponentDefinition
  3. metadata:
  4. name: <ComponentDefinition name>
  5. annotations:
  6. definition.oam.dev/description: <Function description>
  7. spec:
  8. workload: # Workload Capability Indicator
  9. definition:
  10. apiVersion: <Kubernetes Workload resource group>
  11. kind: <Kubernetes Workload types>
  12. schematic: # Component description
  13. cue: # Details of components defined by CUE language
  14. template: <CUE format template>

You can check the detail of this format here.

You can check the following resources for more examples:

Last updated on 2023年8月4日 by Daniel Higuero