什么是 CUE
CUE 是一种开源数据验证语言和推理引擎,其根源在于逻辑编程
说实话,我一开始也没看懂这句描述。我们先来看个小例子:
Data
1 2 3 4 5
| moscow: { name: "Moscow" pop: 11.92M capital: true }
|
Schema
1 2 3 4 5
| municipality: { name: string pop: int capital: bool }
|
CUE
1 2 3 4 5
| largeCapital: { name: string pop: >5M capital: true }
|
在定义 JSON 数据时,我们通常会将 Data 和 Schema 分开处理。而 CUE 则将二者结合在一起,既可以指定数据字段类型,也可以直接填写具体值,也就是 CUE 并不会特意地区分 “类型” 和 “值”, string
和 "Moscow"
都会被当作值 ,但是二者之间有包含的先后顺序,在这里 "Moscow"
可以背归纳于 string
的一种,那么 string
在格(lattice)中会优先于 "Moscow"
。
更多关于 CUE 的介绍,可以通过 官方文档 和 定义 了解过多,这里就不展开了。
使用 CUE 做模版渲染
CUE 有很多很酷的使用场景,而先让我们关注其中的配置文件渲染能力。
我们在这里借用 KubeVela
文档中的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| template: | parameter: { domain: string http: [string]: int }
// trait template can have multiple outputs in one trait outputs: service: { apiVersion: "v1" kind: "Service" spec: { selector: app: context.name ports: [ for k, v in parameter.http { port: v targetPort: v }, ] } }
outputs: ingress: { apiVersion: "networking.k8s.io/v1beta1" kind: "Ingress" metadata: name: context.name spec: { rules: [{ host: parameter.domain http: { paths: [ for k, v in parameter.http { path: k backend: { serviceName: context.name servicePort: v } }, ] } }] } }
|
可以看到,只需要传入 parameter
,就可以得到包含 Service
和 Ingress
的 output
。
这样的写法立刻让我们想到了一个类似的工具—— Helm,作为较早的 CNCF 毕业项目,Helm 已经慢慢演进成在 k8s 配置定义领域的事实意义上的工业标准。那么相较于 Helm,用 CUE 来写配置文件渲染,又有什么异同呢?
CUE vs Helm
最直观的感受就是,在模版编写上 CUE 比 Helm 流畅太多了。主要的原因二者在最初的设计思路差异,Helm 借助的是 Go 和 Spring 的纯文本渲染能力,而对于 CUE,JSON 才是一等公民。所以在 K8S 这类 yaml(JSON 超集) 文件渲染上,CUE(也是 JSON 超集) 拥有天然的优势。
Talk is cheap, show me the code.
我们一起来看几个常用的场景。
Named Templates
Helm
CUE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| app_name: "example_code" other_attr1: "foo" other_attr2: "bar"
{{- define "foo.labels" -}} label1: {{ .Values.app_name | required "app_name is required" }} label2: {{ .Values.other_attr2 }} {{ include "foo.selectorLabels" . }} {{- end }}
{{- define "foo.selectorLabels" -}} label3: {{ printf "%s-%s" .Values.app_name .Values.other_attr1 }} {{- end }}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| app_name: *"example_code" | string other_attr1: "foo" other_attr2: "bar"
selector_labels: { label3: "\( app_name )-\( other_attr2 )" }
labels: { label1: app_name label2: other_attr2 selector_labels }
|
- Helm 可以在
.tpl
文件中定义可复用的模版,并支持其他模版引用它,同时,也只有定义了的模版才能被复用,在复杂的 Chart 项目里你需要额外定义非常多的基础模版。
- 相对于 Helm 繁琐的写法, 在 CUE 中所有内容均为 JSON 对象,不需要额外的语法去指定模版,任意 JSON 对象都可以互相引用。
空行和缩进
Helm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "foo.deploymentName" . }} labels: {{- include "foo.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "foo.selectorLabels" . | nindent 6 }} strategy: {{- include "foo.updateStrategy" . | nindent 4 }} revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "foo.labels" . | nindent 8 }}
|
CUE
1 2 3 4 5 6 7 8 9 10 11 12
| deployment: { apiVersion: "apps/v1" kind: "Deployment" metadata: { name: deployment_name labels: labels } spec: { replicas: runtime.replicas selector: matchLabels: selector_labels strategy: update_strategy revisionHistoryLimit: revision_history_limit
|
- Helm 中有大量的
{{- include }}
和 nindent
等和实际逻辑无关的标记字符,需要在每一次引用的地方计算空格和缩进。
- 而在 CUE 中无用编码更少,不需要过多的
{{ * }}
来标记代码块,信息密度更高,而且在缩进和空格方面得到了完全的解放。
values.yaml 自引用
在 Helm 中,一个长久以来的头疼问题就是,无法优雅地实现 values.yaml
引用问题。
我们看下面的例子:
1 2 3 4
| rootDomain: ""
productDomain: ""
|
通常的情况下,Chart 的使用者需要针对这两个变量分别填写内容,增加了出错的可能。
虽然我们可以通过定义模版来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| rootDomain: "" productDomain: "foo.{{ .Values.rootDomain }}"
{{- define "foo.productDomain" -}} {{ tpl .Values.productDomain $ }} {{- end }}
... spec: rules: - host: {{ include "foo.productDomain" . | quote }} ...
|
但在实际使用中,所有引用的地方都需要额外 include
,同时定义的维护也非常耗费心力(要时刻保证空行、缩进不出错)。
而在 CUE 中,相互引用显得自然而舒服。
1 2
| rootDomain: string productDomain: *"foo.\( rootDomain )" | string
|
导入 Kubernetes 包
CUE 的另一大杀器,可以针对原生 Kubernetes 源码生成描述 cue 文件,所有 k8s 资源相关的配置文件,都可以天然地拥有 schema 校验。具体教程可以查看 Kubernetes tutorial。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package templates
import ( apps "k8s.io/api/apps/v1" )
deployment: apps.#Deployment
deployment: { apiVersion: "apps/v1" kind: "Deployment" metadata: { name: deployment_name labels: _labels } ...
|
类似这个例子中,我们定义的 deployment
将会通过 apps.#Deployment
校验,轻松检测出不合法的字段。理论上,你可以针对多个不同版本的 k8s 资源都生成 cue 文件,并且都作为 deployment
的模型定义,这样可以实现资源定义的多版本兼容能力。
说了这么多好处,现在就把所有 Helm Chart 都替换成 CUE?
且慢,还没到时候。
因为 Helm 作为 Package Manager
,除了 Chart 渲染,本身还具备一定的应用管理功能,例如 Helm install
、 Helm rollback
等,而 CUE 仅仅是模版。所以在某些我们只使用了 Helm 的模版功能的情况下,可以考虑迁移到 CUE,其他情况,还是用 Helm 吧。
脑洞
做一个开源的 CLI,支持将多个文件聚合为一个应用统一管理:
- 类似 Helm 的能力,支持安装、更新、回滚
- 针对配置,支持裸配置和 CUE 文件
好了,先说这么多,我去尝试写一写,下篇文章见 👋 ~
Author:
Blues Yu
Permalink:
https://emergencyexit.xyz/cue-vs-helm.html
License:
Copyright (c) 2025 CC-BY-NC-4.0 LICENSE