CUE语言在Kubernetes教程中的【中文翻译】
这篇文章是关于CUE公式的Kubernetes教程的中文翻译。
我对某些部分感到解释有点不足,因此适当补充了一些词语。
另外,我知道这有点自吹自擂,但是如果对于CUE语言还不太了解的人,可以从以下的文章中阅读来方便地了解一下。
我们在这里讨论CUE语言的成就。
Kubernetes教程
在本教程中,将展示如何将Kubernetes配置文件转换为用于微服务集合的配置文件。
设置文件是从实际设置文件中删除不必要的内容,仅更改名称的文件。
这些文件按相关服务组织在子目录中,这在Kubernetes的清单管理中是一个很常见的模式。cue工具就是为了这个用例而设计的。
在这个教程中,我们将讨论以下主题。
-
- 将YAML转换为CUE
-
- 将其应用到父目录
-
- 使用工具修改CUE文件,删除不必要的字段
-
- 对于不同的子目录,从步骤2开始重复
-
- 定义用于操作配置的命令
-
- 从Kubernetes源代码中提取Go结构体并生成CUE模板
- 手动调整设置
关于数据集的内容
该数据集是基于实际案例的,并使用不同的名称用于服务。为了获得关于CUE转换实际执行方式的现实印象,所有真实设置的不匹配之处都被重现在了文件中。
┌─(~/works/cue-lang/cue/doc/tutorial/kubernetes)
└─(15:22:11 on master)──> tree ./original | head ──(木,1209)─┘
./original
└── services
├── frontend
│ ├── bartender
│ │ └── kube.yaml
│ ├── breaddispatcher
│ │ └── kube.yaml
│ ├── host
│ │ └── kube.yaml
│ ├── maitred
...
每个子目录中都包含与之相关的微服务,这些微服务具有相似的特点和结构。这些结构包括服务、部署、配置地图、守护进程集、有状态集、定时任务等多种Kubernetes对象。
最初教程中的结果在“快速和简单”的目录中,意思是快速目录。手动优化的配置在手动目录中。
导入现有设置
首先,创建一个数据目录的副本。
$ cp -a original tmp
$ cd tmp
为了将在子目录中的所有配置文件作为一个包进行处理,我们将初始化模块。
通过给它们相同的包名,我们可以这样处理它们。
同时,我们也初始化了Go模块。这是为了以后引用Kubernetes源代码。
$ cue mod init
$ go mod init example.com
另外,通过创建模块,可以导入外部包。让我们尝试使用cue import命令将给定的YAML文件转换为CUE。
$ cd services
$ cue import ./...
path, list, or files flag needed to handle multiple objects in file ./services/frontend/bartender/kube.yaml
因为有多个kube.yaml文件,所以显示了一个错误消息,指出需要指定它们所属的包。
$ cue import ./... -p kube
path, list, or files flag needed to handle multiple objects in file "./frontend/bartender/kube.yaml"
许多文件包含多个Kubernetes对象。
此外,我们试图创建一个包含所有文件及其所有对象的单个配置。
首先,我们需要整理所有Kubernetes对象,并使它们在一个配置中能够被正确识别。
为此,我们需要为每种对象类型定义不同的结构体,并通过以对象名称为键将其存储起来。
这样一来,就可以像Kubernetes允许的那样,不同类型的对象可以共享相同的名称。
为此,cue指示在配置树中的路径中将”kind”作为第一个元素,”name”作为第二个元素,并将每个对象放置在适当的位置。
$ cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f
由于导入命令的选项越来越多,我们需要整理一下。
首先,我们之前使用的-p标志(–package)用于为非CUE文件指定包名的选项。
现在我们想要做的是将Kubernetes的yaml文件转换为CUE文件。在这个过程中,需要明确设置CUE包的名称。
追加的 -l 标记 (–path) 使用每个对象的值为基础,使用常规 CUE 的字段标签语法来定义每个对象的路径。
在这里,我们使用每个对象的 kind 字段作为驼峰命名的变体,并将 metadata 部分的 name 字段用作每个对象的名称。
你可能会思考为什么需要明确指定路径。
这是因为在导出 cue 的 package 时,需要一个唯一确定的路径。在这种情况下,我们可以使用类似于 cue export exported_path -e (kind).(metaname) –out yaml 的命令来从 CUE 反向导出到 yaml。这里使用了 (kind).(metaname) 来利用单一路径组件。
另外,-f 标记 (–force) 是一个允许覆盖文件的选项。
让我们来看看实际发生了什么。
$ tree . | head
.
└── services
├── frontend
│ ├── bartender
│ │ ├── kube.cue
│ │ └── kube.yaml
│ ├── breaddispatcher
│ │ ├── kube.cue
│ │ └── kube.yaml
...
每个YAML文件都被转换成相应的CUE文件。YAML文件中的注释也被保留下来。
然而,仍然存在一些不完美的部分。
首先,请查看mon/prometheus/configmap.cue文件。
$ cat mon/prometheus/configmap.cue
package kube
apiVersion: "v1"
kind: "ConfigMap"
metadata: name: "prometheus"
data: {
"alert.rules": """
groups:
- name: rules.yaml
...
如果仔细查看此配置文件的”data:”下面的内容,可以看出这不是cue的格式,而是yaml的格式。保留yaml作为字符串的形式是不可取的。
解决这个问题的选项是-R标签。通过使用-R标签,可以递归地检测嵌入在配置文件中的YAML或JSON字符串,并将其转换为CUE。
$ cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f -R
结果如下。
$ cat mon/prometheus/configmap.cue
package kube
import "encoding/yaml"
configMap: prometheus: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: name: "prometheus"
data: {
"alert.rules": yaml.Marshal(_cue_alert_rules)
_cue_alert_rules: {
groups: [{
...
太棒了。
通过调用yaml.Marshal函数,将结构化的CUE源代码转换为与YAML文件等效的字符串。在生成的配置文件中,原始的嵌入字符串将被替换为转换后的字符串。生成与YAML文件等效的字符串,创建一个等效的YAML文件。以下划线(_)开头的字段在输出配置文件时将不会包括在内(如果被双引号引起来,则会包括进来)。
$ cue eval ./mon/prometheus -e configMap.prometheus
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: "prometheus"
}
data: {
"alert.rules": """
groups:
- name: rules.yaml
...
耶!
快速且简易教程
本教程将介绍如何从一系列设置中快速删除模板文本。
介绍删除模板文本的方法。
手动调整通常会得到更好的结果,但需要花费相当的时间和精力。而使用快速且简单的方法,几乎可以完成大部分事情。
这种快速转换的结果将为仔细的手动优化提供良好的基础。
创建顶级模板
因此,您已成功导入YAML文件,可以开始简化过程。
在开始重建之前,您可以保存完整的评估,并确保简化后仍然得到相同的结果。
$ cue eval -c ./... > snapshot
-c选项告诉cue只允许具体的值,即有效的JSON。在这里,我们关注在各种kube.cue文件中定义的对象。大体上看,许多部署和服务都具有共同的结构。
在这里,我们将复制一个包含了创建模板所需的基础文件的文件到目录树的根目录上。
cp frontend/breaddispatcher/kube.cue .
稍微修改这个文件,变成以下这样。
$ cat <<EOF > kube.cue
package kube
service: [ID=_]: {
apiVersion: "v1"
kind: "Service"
metadata: {
name: ID
labels: {
app: ID // by convention
domain: "prod" // always the same in the given files
component: string // varies per directory
}
}
spec: {
// Any port has the following properties.
ports: [...{
port: int
protocol: *"TCP" | "UDP" // from the Kubernetes definition
name: string | *"client"
}]
selector: metadata.labels // we want those to be the same
}
}
deployment: [ID=_]: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: ID
spec: {
// 1 is the default, but we allow any number
replicas: *1 | int
template: {
metadata: labels: {
app: ID
domain: "prod"
component: string
}
// we always have one namesake container
spec: containers: [{ name: ID }]
}
}
}
EOF
通过将服务名称和部署名称替换为[ID=_],将定义更改为与任意字段匹配的模板。CUE会将字段名称绑定到ID上。
在导入时,由于将metadata.name用作对象名称的键,因此可以将该字段设置为ID。
模板将应用于已定义结构体的所有条目,因此需要删除或通用breaddispatcher定义中的特定字段。
在Kubernetes的元数据中,一个标签被定义为始终设置为父目录名。通过定义component: string,我们强制实施这一要求。也就是说,名为component的字段必须设置为某种字符串值,并且可以在稍后进行定义。如果有字段没有被指定,例如在转换为JSON时会导致错误。换句话说,只有在定义了这个标签时,部署和服务才会生效。
让我们将合并了新模板的结果与原始快照进行比较。
$ cue eval ./... -c > snapshot2
--- ./mon/alertmanager
service.alertmanager.metadata.labels.component: incomplete value (string):
./kube.cue:11:24
service.alertmanager.spec.selector.component: incomplete value (string):
./kube.cue:11:24
deployment.alertmanager.spec.template.metadata.labels.component: incomplete value (string):
./kube.cue:36:28
service."node-exporter".metadata.labels.component: incomplete value (string):
./kube.cue:11:24
...
Alertmanager缺少组件标签。
CUE检测到配置不一致。
由于几乎没有未指定组件标签的对象,所以我们需要对配置进行更改,将它们包含在所有地方。
为此,在每个目录中,将新定义的顶级字段设置为目录名称,并修改主模板文件以使用它们。
# コンポーネントラベルを新しいトップレベルフィールドに設定します
$ sed -i.bak 's/component:.*string/component: #Component/' kube.cue && rm kube.cue.bak
# 以前のテンプレート定義に新しいトップレベルフィールドを追加します
$ cat <<EOF >> kube.cue
#Component: string
EOF
# コンポーネントラベルの付いたファイルを各ディレクトリに追加します
$ ls -d */ | sed 's/.$//' | xargs -I DIR sh -c 'cd DIR; echo "package kube
#Component: \"DIR\"
" > kube.cue; cd ..'
# ファイルのフォーマットを整えます
$ cue fmt kube.cue */kube.cue
$ cue eval -c ./... > snapshot2
$ diff snapshot snapshot2
...
除了标签顺序等其他方面,其余都相同。由于结果很好,我们可以将其作为新的基准线使用。
$ cp snapshot2 snapshot
通过这个方法,可以使用cue trim来删除相应的锅炉板。
$ find . | grep kube.cue | xargs wc | tail -1
1792 3616 34815 total
$ cue trim ./...
$ find . | grep kube.cue | xargs wc | tail -1
1223 2374 22903 total
通过模板或包含已生成的设置,cue trim将从文件中删除。
如上所述,已删除500行以上即30%以上的设置。
内容并未改变。
$ cue eval -c ./... > snapshot2
$ diff snapshot snapshot2 | wc
0 0 0
然而,我们可以更加熟练地做到这一点。
首先,请注意DaemonSets和StatefulSets与Deployments具有相同的结构。我们将顶层模板通用化如下。
$ cat <<EOF >> kube.cue
daemonSet: [ID=_]: _spec & {
apiVersion: "apps/v1"
kind: "DaemonSet"
_name: ID
}
statefulSet: [ID=_]: _spec & {
apiVersion: "apps/v1"
kind: "StatefulSet"
_name: ID
}
deployment: [ID=_]: _spec & {
apiVersion: "apps/v1"
kind: "Deployment"
_name: ID
spec: replicas: *1 | int
}
configMap: [ID=_]: {
metadata: name: ID
metadata: labels: component: #Component
}
_spec: {
_name: string
metadata: name: _name
metadata: labels: component: #Component
spec: selector: {}
spec: template: {
metadata: labels: {
app: _name
component: #Component
domain: "prod"
}
spec: containers: [{name: _name}]
}
}
EOF
$ cue fmt
将共同设置聚集到_spec中。
此外,为了指定或引用对象的名称,引入_name,并将configMap作为顶层条目添加。
我们还没有删除旧的部署定义。尽管删除了,意义仍然相同,因为它与新的定义等效。删除将成为读者的任务。
接下来,我们要确保所有的Deployment、StatefulSet和DaemonSet都有共享同一字段的附属服务。我们会添加它们。
$ cat <<EOF >> kube.cue
// Define the _export option and set the default to true
// for all ports defined in all containers.
_spec: spec: template: spec: containers: [...{
ports: [...{
_export: *true | false // include the port in the service
}]
}]
for x in [deployment, daemonSet, statefulSet] for k, v in x {
service: "\(k)": {
spec: selector: v.spec.template.metadata.labels
spec: ports: [
for c in v.spec.template.spec.containers
for p in c.ports
if p._export {
let Port = p.containerPort // Port is an alias
port: *Port | int
targetPort: *Port | int
}
]
}
}
EOF
$ cue fmt
在这个例子中,我们将介绍一些新概念。
开放式列表用…表示。使用它可以与后续元素进行整合,并定义附加的列表元素类型或模板。
Port宣言是一个别名。别名仅在其词法范围内可见,并不是模型的一部分。别名用于在嵌套的作用域中显示被隐藏的字段,或者用于减少定型文而不引入新的字段,这个例子中使用。
最後,我们将介绍列表内涵和字段内涵。列表内涵与其他语言中的列表内涵非常相似。字段内涵可以将字段插入到结构体中。在这个例子中,通过字段内涵,我们可以为任意部署、守护进程集和有状态集添加命名服务。字段内涵还可以用于有条件地添加字段。
targetPort的指定并非必须,但由于它在许多文件中已经定义,如果在此处定义,您可以使用队列修剪对这些定义进行删除。我们添加了一个名为”_export”的选项,用于指定是否将容器中定义的端口包含在服务中,并明确将其设置为false,以针对infra/events、infra/tasks和infra/watcher的每个端口。
为了达到这个教程的目的,我将介绍几种快速修补方法。
$ cat <<EOF >> infra/events/kube.cue
deployment: events: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
EOF
$ cat <<EOF >> infra/tasks/kube.cue
deployment: tasks: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
EOF
$ cat <<EOF >> infra/watcher/kube.cue
deployment: watcher: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
EOF
实际上,在原始端口的声明中添加这个字段将是更合适的格式。
确认所有更改都被接受后,保存另一个快照。
然后,执行剪辑以压缩配置。
$ cue trim ./...
$ find . | grep kube.cue | xargs wc | tail -1
1129 2270 22073 total
尽管添加了模板,但仍然删除了近100行代码。
在大多数文件中,服务定义可能已经丢失了。
剩下的要么是额外的设置,要么是需要清理的不一致之一。
不过,我们还有另一项秘策。
使用-s或–simplify选项,可以指示trim或fmt将拥有单一元素的结构体合并成一个。
可以将结构体折叠成一行。
$ head frontend/breaddispatcher/kube.cue
package kube
deployment: breaddispatcher: {
spec: {
template: {
metadata: {
annotations: {
"prometheus.io.scrape": "true"
"prometheus.io.port": "7080"
}
$ cue trim ./... -s
$ head -7 frontend/breaddispatcher/kube.cue
package kube
deployment: breaddispatcher: spec: template: {
metadata: annotations: {
"prometheus.io.scrape": "true"
"prometheus.io.port": "7080"
}
$ find . | grep kube.cue | xargs wc | tail -1
975 2116 20264 total
通过进一步减少了150行,能够大幅减少标点符号的数量,从而改善了配置的可读性。
对于子目录要进行重复操作
为了了解所有服务和部署都具有的共同特点,我们在目录结构的根目录中定义了服务和部署的模板。此外,还定义了每个目录的标签。在本节中,我们将考虑为每个目录通用化对象。
前端目录:
可以看到,位于前端子目录中的所有部署都包含一个具有一个端口的容器。此外,大多数有两个与prometheus相关的注释,但也有一个只有一个注释的部署。保持端口不匹配的情况下,无条件地添加这两个注释。
$ cat <<EOF >> frontend/kube.cue
deployment: [string]: spec: template: {
metadata: annotations: {
"prometheus.io.scrape": "true"
"prometheus.io.port": "\(spec.containers[0].ports[0].containerPort)"
}
spec: containers: [{
ports: [{containerPort: *7080 | int}] // 7080 is the default
}]
}
EOF
$ cue fmt ./frontend
# check differences
$ cue eval -c ./... > snapshot2
$ diff snapshot snapshot2
368a369
> prometheus.io.port: "7080"
577a579
> prometheus.io.port: "8080"
$ cp snapshot2 snapshot
通过添加了注释的两行,一致性得到了提高。
$ cue trim -s ./frontend/...
$ find . | grep kube.cue | xargs wc | tail -1
931 2052 19624 total
我成功地进一步减少了40行。
剩下的需要删除的内容已经不多了。前端文件中甚至只剩下4行的设置。
厨房目录
查看这个目录,可以看到所有的Deployment都有一个例外,即一个名为Port8080的容器,所有的Deployment都具有相同的活力证明度,即一行Prometheus注释,并且大多数的Deployment都有两个或三个相似模式的磁盘。
在这里,让我们将除了磁盘以外的一切都添加进来吧。
$ cat <<EOF >> kitchen/kube.cue
deployment: [string]: spec: template: {
metadata: annotations: "prometheus.io.scrape": "true"
spec: containers: [{
ports: [{
containerPort: 8080
}]
livenessProbe: {
httpGet: {
path: "/debug/health"
port: 8080
}
initialDelaySeconds: 40
periodSeconds: 3
}
}]
}
EOF
$ cue fmt ./kitchen
查看diff时,发现服务中添加了Prometheus的一个注释。这应该只是一个偶然的变化。
光盘需要在模板规范部分和使用的容器中进行定义。建议将这两个定义放在一起。获取expiditer的卷定义并将其概括化。
$ cat <<EOF >> kitchen/kube.cue
deployment: [ID=_]: spec: template: spec: {
_hasDisks: *true | bool
// field comprehension using just "if"
if _hasDisks {
volumes: [{
name: *"\(ID)-disk" | string
gcePersistentDisk: pdName: *"\(ID)-disk" | string
gcePersistentDisk: fsType: "ext4"
}, {
name: *"secret-\(ID)" | string
secret: secretName: *"\(ID)-secrets" | string
}, ...]
containers: [{
volumeMounts: [{
name: *"\(ID)-disk" | string
mountPath: *"/logs" | string
}, {
mountPath: *"/etc/certs" | string
name: *"secret-\(ID)" | string
readOnly: true
}, ...]
}]
}
}
EOF
$ cat <<EOF >> kitchen/souschef/kube.cue
deployment: souschef: spec: template: spec: {
_hasDisks: false
}
EOF
$ cue fmt ./kitchen/...
这个模板的定义并不理想。定义的顺序不应该对定义产生影响。
如果以不同的顺序定义字段,即使无法再利用,甚至都不会发生冲突。另外,为了解决这个限制,几乎所有的字段值都只是默认值,可以被实例覆盖。
与整理Kubernetes顶级对象的方法类似,定义卷映射并从该映射生成这两个部分是更好的方法。
然而,这需要一些设计,不适合“快速&脏”教程。本文档的后半部分将介绍手动优化的配置。
在这里,可以通过默认情况下添加两个磁盘,并通过定义_hasDisks选项来禁用它。 souschef的配置是仅定义了没有磁盘的唯一配置。
$ cue trim -s ./kitchen/...
# check differences
$ cue eval ./... > snapshot2
$ diff snapshot snapshot2
...
$ cp snapshot2 snapshot
$ find . | grep kube.cue | xargs wc | tail -1
807 1862 17190 total
从diff中可以看出,添加了hadDisks选项,但除此之外没有其他特别的不同。而且,还大幅简化了配置。
不过,仔细查看剩余的文件后发现,由于名称的不一致性,硬盘规格中仍有许多字段留存。如果像这次一样简化配置,将会暴露出不一致性。这种不一致性可以通过删除特定配置的覆盖来解决。如果保持不变,可以明确显示配置存在矛盾。
快速且简单的教程总结
在其他文件夹中,你仍然可以进行一些压缩。
在本教程中,我们成功地减少了1,000行(55%),但剩下的部分将交给读者作为您的任务。
我們以上,介紹了使用CUE可以減少固定格式的文本、確保統一性和檢測不一致性的功能。能夠處理一致性和不一致性是基於約束模型的結果,這在基於繼承的語言中是困難的。
另外,CUE的适用于机器操作也间接地证明了这一点。这是由于其语法和语义的顺序无关性因素引起的。trim命令是利用这种特性实现的许多自动重构工具之一。在基于继承的配置语言中,要实现这样的操作是困难的。
从Kubernetes源代码中的Go结构中提取CUE模板。
要从Go源代码中生成CUE模板,首先需要将源代码准备在本地。
$ go get k8s.io/api/apps/v1
$ cue get go k8s.io/api/apps/v1
这样一来,您就拥有了Kubernetes的定义,然后可以将其导入并使用。
$ cat <<EOF > k8s_defs.cue
package kube
import (
"k8s.io/api/core/v1"
apps_v1 "k8s.io/api/apps/v1"
)
service: [string]: v1.#Service
deployment: [string]: apps_v1.#Deployment
daemonSet: [string]: apps_v1.#DaemonSet
statefulSet: [string]: apps_v1.#StatefulSet
EOF
我们最后再次整理一下格式。
cue fmt