👋 再见 Helm,你好 CUE
date
Jun 22, 2021
slug
cue-vs-helm.html
status
Published
tags
tech
k8s
back-pressure
helm
summary
长江后浪推前浪
type
Post
什么是 CUE
CUE 是一种开源数据验证语言和推理引擎,其根源在于逻辑编程
说实话,我一开始也没看懂这句描述。我们先来看个小例子:
Data
Schema
CUE
moscow: {
name: "Moscow"
pop: 11.92M
capital: true
}
municipality: {
name: string
pop: int
capital: bool
}
largeCapital: {
name: string
pop: >5M
capital: true
}
在定义 JSON 数据时,我们通常会将 Data 和 Schema 分开处理。而 CUE 则将二者结合在一起,既可以指定数据字段类型,也可以直接填写具体值,也就是 CUE 并不会特意地区分 “类型” 和 “值”,
string
和 "Moscow"
都会被当作值 ,但是二者之间有包含的先后顺序,在这里 "Moscow"
可以背归纳于 string
的一种,那么 string
在格(lattice)中会优先于 "Moscow"
。使用 CUE 做模版渲染
CUE 有很多很酷的使用场景,而先让我们关注其中的配置文件渲染能力。
我们在这里借用
KubeVela
文档中的例子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
# values.yaml
app_name: "example_code"
other_attr1: "foo"
other_attr2: "bar"
# labels.tpl
{{- 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 }}
# labels.cue
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
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
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
引用问题。我们看下面的例子:
rootDomain: ""
# 我们期待的是通过引用 rootDomain 拼接,例如:"foo.{{ .Values.rootDomain }}"
productDomain: ""
通常的情况下,Chart 的使用者需要针对这两个变量分别填写内容,增加了出错的可能。
虽然我们可以通过定义模版来实现:
# values.yaml
rootDomain: ""
productDomain: "foo.{{ .Values.rootDomain }}"
# helpers.tpl
{{- define "foo.productDomain" -}}
{{ tpl .Values.productDomain $ }}
{{- end }}
# ingress.yaml
...
spec:
rules:
- host: {{ include "foo.productDomain" . | quote }}
...
但在实际使用中,所有引用的地方都需要额外
include
,同时定义的维护也非常耗费心力(要时刻保证空行、缩进不出错)。而在 CUE 中,相互引用显得自然而舒服。
rootDomain: string
productDomain: *"foo.\( rootDomain )" | string
导入 Kubernetes 包
CUE 的另一大杀器,可以针对原生 Kubernetes 源码生成描述 cue 文件,所有 k8s 资源相关的配置文件,都可以天然地拥有 schema 校验。具体教程可以查看 Kubernetes tutorial。
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 文件
好了,先说这么多,我去尝试写一写,下篇文章见 👋 ~