Skip to main content

· 阅读需要 1 分钟

简介

Helm 是一个为 Kubernetes 对象生成可部署清单的工具,它承担了以两种不同形式生成最终清单的任务。Helm 是一个管理 Kubernetes 包(称为 charts)的必备模板工具。图表是 YAML 清单的模板化版本,其中混合了 Go template 的子集,它也是 Kubernetes 的包管理器,可以打包、配置和部署/应用 Helm 图表到 Kubernetes 集群。

在 KCL 中,用户可以使用更多的工具和 IDE 插件支持直接编写配置代码文件,而不是模板文件,这些工具和插件支持需要在相应位置的代码中进行修改,从而消除了读取 YAML 的成本。同时,用户可以通过代码重用配置片段,避免了YAML 配置的大量复制和粘贴。信息密度更高,更不容易出错。

下面以一个经典的 Helm Chart 配置管理的例子详细说明 Kustomize 和 KCL 在 Kubernetes 资源配置管理上的区别。

Helm

Helm 具备 values.yamltemplate 的概念, 通常一个 Helm Chart 由一个包含 Chart.yaml 的路径组成。我们可以执行如下命令获得一个典型的 Helm Chart 工程

  • 创建 workload-helm 目录来保存 chart 工程
# Create a directory to hold the chart project
mkdir workload-helm
# Create a workload-helm/Chart.yaml
cat <<EOF > workload-helm/Chart.yaml
apiVersion: v2
appVersion: 0.3.0
description: A helm chart to provision standard workloads.
name: workload
type: application
version: 0.3.0
EOF
# Create a workload-helm/values.yaml
cat <<EOF > workload-helm/values.yaml
service:
type: ClusterIP
ports:
- name: www
protocol: TCP
port: 80
targetPort: 80

containers:
my-container:
image:
name: busybox:latest
command: ["/bin/echo"]
args:
- "-c"
- "Hello World!"
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
EOF
  • 创建模版文件夹
# Create a directory to hold templates
mkdir workload-helm/templates
# Create a workload-helm/templates/helpers.tpl
cat <<EOF > workload-helm/templates/helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "workload.name" -}}
{{- default .Release.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "workload.fullname" -}}
{{- \$name := default .Chart.Name .Values.nameOverride }}
{{- if contains \$name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name \$name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "workload.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "workload.labels" -}}
helm.sh/chart: {{ include "workload.chart" . }}
{{ include "workload.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "workload.selectorLabels" -}}
app.kubernetes.io/name: {{ include "workload.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
EOF
cat <<EOF > workload-helm/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "workload.name" . }}
labels:
{{- include "workload.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "workload.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "workload.selectorLabels" . | nindent 8 }}
spec:
containers:
{{- range \$name, \$container := .Values.containers }}
- name: {{ \$name }}
image: "{{ $container.image.name }}"
{{- with \$container.command }}
command:
{{- toYaml \$container.command | nindent 12 }}
{{- end }}
{{- with \$container.args }}
args:
{{- toYaml \$container.args | nindent 12 }}
{{- end }}
{{- with \$container.env }}
env:
{{- toYaml \$container.env | nindent 12 }}
{{- end }}
{{- with \$container.volumeMounts }}
volumeMounts:
{{- toYaml \$container.volumeMounts | nindent 12 }}
{{- end }}
{{- with \$container.livenessProbe }}
livenessProbe:
{{- toYaml \$container.livenessProbe | nindent 12 }}
{{- end }}
{{- with \$container.readinessProbe }}
readinessProbe:
{{- toYaml \$container.readinessProbe | nindent 12 }}
{{- end }}
{{- with \$container.resources }}
resources:
{{- toYaml \$container.resources | nindent 12 }}
{{- end }}
{{- end }}
EOF
cat <<EOF > workload-helm/templates/service.yaml
{{ if .Values.service }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "workload.name" . }}
labels:
{{- include "workload.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "workload.selectorLabels" . | nindent 4 }}
{{- with .Values.service.ports }}
ports:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
EOF

可以得到如下的 Helm chart 工程

.
├── Chart.yaml
├── templates
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ └── service.yaml
└── values.yaml

我们可以通过如下的命令渲染真实的部署配置

helm template workload-helm

可以得到如下 YAML 输出

---
# Source: workload-helm/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: release-name
labels:
helm.sh/chart: workload-0.3.0
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "0.3.0"
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
ports:
- name: www
port: 80
protocol: TCP
targetPort: 80
---
# Source: workload-helm/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-name
labels:
helm.sh/chart: workload-0.3.0
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
app.kubernetes.io/version: "0.3.0"
app.kubernetes.io/managed-by: Helm
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
template:
metadata:
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
containers:
- name: my-container
image: "busybox:latest"
command:
- /bin/echo
args:
- -c
- Hello World!
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi

KCL

在 KCL 中,我们提供了与 Helm values.yaml 相似的动态配置参数 kcl.yaml 文件,我们可以执行如下的命令获得一个典型的 KCL 工程。

  • 创建 workload-kcl 目录来保存 KCL 工程
# Create a directory to hold the KCL project
mkdir workload-kcl
# Create a workload-kcl/kcl.yaml
cat <<EOF > workload-kcl/kcl.yaml
kcl_options:
- key: containers
value:
my-container:
image:
name: busybox:latest
command: ["/bin/echo"]
args:
- "-c"
- "Hello World!"
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi

- key: service
value:
type: ClusterIP
ports:
- name: www
protocol: TCP
port: 80
targetPort: 80
EOF
  • 创建如下 KCL 文件来保存 kubernetes 资源
# Create a workload-kcl/deployment.k
cat <<EOF > workload-kcl/deployment.k
apiVersion = "apps/v1"
kind = "Deployment"
metadata = {
name = "release-name"
labels = {
"app.kubernetes.io/name" = "release-name"
"app.kubernetes.io/instance" = "release-name"
}
}
spec = {
selector.matchLabels = metadata.labels
template.metadata.labels = metadata.labels
template.spec.containers = [
{
name = name
image = container.image.name
command = container.command
command = container.args
env = container.env
resources = container.resources
} for name, container in option("containers") or {}
]
}
EOF
cat <<EOF > workload-kcl/service.k
apiVersion = "v1"
kind = "Service"
metadata = {
name = "release-name"
labels = {
"app.kubernetes.io/name" = "release-name"
"app.kubernetes.io/instance" = "release-name"
}
}
spec = {
selector.matchLabels = metadata.labels
type = option("service", default={})?.type
ports = option("service", default={})?.ports
}
EOF

上述 KCL 代码中我们分别声明了一个 Kubernetes DeploymentService 资源的 apiVersionkindmetadataspec 等变量,并分别赋值了相应的内容,特别地,我们将 metadata.labels 字段分别重用在 spec.selector.matchLabelsspec.template.metadata.labels 字段。可以看出,相比于 Helm 模版 或者 YAML,KCL 定义的数据结构更加紧凑,而且可以通过定义局部变量实现配置重用。

在 KCL 中,我们可以通过条件语句和 option 内置函数接收动态参数,并设置不同的配置值以生成资源。

可以通过如下的命令得到 DeploymentService YAML 输出:

  • Deployment
$ kcl workload-kcl/deployment.k -Y workload-kcl/kcl.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: release-name
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
template:
metadata:
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
containers:
- name: my-container
image: busybox:latest
command:
- -c
- Hello World!
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
  • Service
$ kcl workload-kcl/service.k -Y workload-kcl/kcl.yaml
apiVersion: v1
kind: Service
metadata:
name: release-name
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
type: ClusterIP
ports:
- name: www
protocol: TCP
port: 80
targetPort: 80

此外我们可以通过 -D 标志设置额外的参数并覆盖 kcl.yaml 文件的配置值

$ kcl workload-kcl/service.k -Y workload-kcl/kcl.yaml -D service=None
apiVersion: v1
kind: Service
metadata:
name: release-name
labels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
spec:
selector:
matchLabels:
app.kubernetes.io/name: release-name
app.kubernetes.io/instance: release-name
type: null
ports: null

小结

可以看出,与 Helm 相比,KCL 通过在配置重用和覆盖的基础上生成代码,减少了配置文件和代码行的数量。与 Helm 一样,它是一个纯客户端解决方案,可以将配置和策略验证尽可能地左移,而不会对集群造成额外的依赖或负担,或者甚至没有 Kubernetes 集群时也可以通过 KCL Schema 等特性对 YAML 进行充分验证和测试。

Helm 可以在 .tpl 文件中定义可重用模板,并支持其他模板引用它。但是,只有模板定义才能重用。在一个复杂的 Helm 图表项目中,我们需要定义许多附加的基本模板。与 Helm 繁琐的写作方法相比,KCL 中的所有内容都是变量。指定模板不需要其他语法。任何变量都可以相互引用。

此外,Helm 中还有大量与实际逻辑无关的 {{- include }}, nindenttoYaml 标记字符,我们需要计算每个 Helm 引用处的空格和缩进。在 KCL 中,无用代码更少,并且不需要很多的 {{*}} 来标记代码块,信息密度更高。

事实上,KCL 和 Helm Chart 并不对立。我们甚至可以使用 KCL 编写 Helm 模板或者使用 KCL 来生成 values.yaml,或者为现有的 Helm 图表提供可编程扩展功能,比如为 Helm 开发可选的 KCL Schema 插件来验证已有的 Helm 图表或者为 Helm Chart 编写额外的 Transformer 来 Patch 已有的 Helm Chart。

未来计划

我们后续计划 KCL 的模型和约束可以作为一个包来管理(这个包只有 KCL 文件)。例如,Kubernetes 的模型和约束可以开箱即用。用户可以通过已有的模型生成配置或验证现有配置,并且可以通过 KCL 继承手段简单地扩展用户想要的模型和约束。

在此阶段,您可以使用 Git 或 OCI Registry as Storage(ORAS) 等工具来管理 KCL 配置版本。

· 阅读需要 1 分钟

简介

Kustomize 提供了一种无需模板和即可自定义 Kubernetes 资源基础配置和差异化配置的解决方案,通过文件级的 YAML 配置方式完成配置合并或覆盖。在 Kustomize 中用户需要更详细地了解将要发生更改的内容和位置,对于复杂递归过深的基础 YAML 可能不太容易通过选择器来匹配 Kustomize 文件。

而在 KCL 中,用户可以直接把对应代码需要修改的配置书写在对应的地方,免去了阅读基础 YAML 的成本,同时能够通过代码的方式复用配置片段,避免 YAML 配置的大量复制粘贴,信息密度更高,也不容易出错。

下面以一个经典的 Kustomize 多环境配置管理例子详细说明 Kustomize 和 KCL 在 Kubernetes 资源配置管理上的区别。

Kustomize

Kustomize 有 base 和 overlay 的概念,bases 和 overlays 一般是一个包含 kustomization.yaml 文件的目录,一个 base 可以被多个 overlay 使用

我们可以执行如下命令行获得一个典型的 Kustomize 工程

  • 创建 base 目录并新建一个 deployment 资源
# Create a directory to hold the base
mkdir base
# Create a base/deployment.yaml
cat <<EOF > base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ldap
labels:
app: ldap
spec:
replicas: 1
selector:
matchLabels:
app: ldap
template:
metadata:
labels:
app: ldap
spec:
containers:
- name: ldap
image: osixia/openldap:1.1.11
args: ["--copy-service"]
volumeMounts:
- name: ldap-data
mountPath: /var/lib/ldap
ports:
- containerPort: 389
name: openldap
volumes:
- name: ldap-data
emptyDir: {}
EOF
# Create a base/kustomization.yaml
cat <<EOF > base/kustomization.yaml
resources:
- deployment.yaml
EOF
  • 创建一个 prod 目录并放置生产环境的配置
# Create a directory to hold the prod overlay
mkdir prod
# Create a prod/deployment.yaml
cat <<EOF > prod/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ldap
spec:
replicas: 6
template:
spec:
volumes:
- name: ldap-data
emptyDir: null
gcePersistentDisk:
readOnly: true
pdName: ldap-persistent-storage
EOF
cat <<EOF > prod/kustomization.yaml
resources:
- ../base
patchesStrategicMerge:
- deployment.yaml
EOF

此时我们可以得到一个基本的 Kustomzie 目录

.
├── base
│ ├── deployment.yaml
│ └── kustomization.yaml
└── prod
├── deployment.yaml
└── kustomization.yaml

其中,base 目录存放的是基本的 deployment 配置,prod 环境存放的是需要覆盖的 deployment 配置,在其中指定了 metadata.name 等字段用于表示对哪个资源进行覆盖

我们可以通过如下命令行显示 prod 环境的真实 deployment 配置

$ kubectl kustomize ./prod
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ldap
name: ldap
spec:
replicas: 6
selector:
matchLabels:
app: ldap
template:
metadata:
labels:
app: ldap
spec:
containers:
- args:
- --copy-service
image: osixia/openldap:1.1.11
name: ldap
ports:
- containerPort: 389
name: openldap
volumeMounts:
- mountPath: /var/lib/ldap
name: ldap-data
volumes:
- gcePersistentDisk:
pdName: ldap-persistent-storage
readOnly: true
name: ldap-data

也可以通过如下命令行直接将配置下发到集群当中

$ kubectl apply -k ./prod

deployment.apps/ldap created

KCL

我们可以编写如下 KCL 代码并命名为 main.k

apiVersion = "apps/v1"
kind = "Deployment"
metadata = {
name = "ldap"
labels.app = "ldap"
}
spec = {
replicas = 1
# When env is prod, override the `replicas` attribute with `6`
if option("env") == "prod": replicas = 6
# Assign `metadata.labels` to `selector.matchLabels`
selector.matchLabels = metadata.labels
template.metadata.labels = metadata.labels
template.spec.containers = [
{
name = metadata.name
image = "osixia/openldap:1.1.11"
args = ["--copy-service"]
volumeMounts = [{ name = "ldap-data", mountPath = "/var/lib/ldap" }]
ports = [{ containerPort = 80, name = "openldap" }]
}
]
template.spec.volumes = [
{
name = "ldap-data"
emptyDir = {}
# When env is prod
# override the `emptyDir` attribute with `None`
# patch a `gcePersistentDisk` attribute with the value `{readOnly = True, pdName = "ldap-persistent-storage"}`
if option("env") == "prod":
emptyDir = None
gcePersistentDisk = {
readOnly = True
pdName = "ldap-persistent-storage"
}
}
]
}

上述 KCL 代码中我们分别声明了一个 Kubernetes Deployment 资源的 apiVersionkindmetadataspec 等变量,并分别赋值了相应的内容,特别地,我们将 metadata.labels 字段分别重用在 spec.selector.matchLabelsspec.template.metadata.labels 字段。可以看出,相比于 Kustomize 或者 YAML,KCL 定义的数据结构更加紧凑,而且可以通过定义局部变量实现配置重用。

在 KCL 中,我们可以通过条件语句和 option 函数动态地接收外部参数,为不同的环境需要设置不同的配置值生成不同环境的资源。比如对于如上代码,我们编写了一个条件语句并输入一个名为 env 的动态参数,当 envprod 时,我们将 replicas 字段由 1 覆盖为 6,并且对名为 ldap-data 的 volume 配置进行一些调整,如将 emptyDir 字段修改为 None, 增加 gcePersistentDisk 的配置值等。

可以使用如下命令查看不同环境配置的 diff

diff \
<(kcl main.k) \
<(kcl main.k -D env=prod) |\
more

输出如下:

8c8
< replicas: 1
---
> replicas: 6
30c30,33
< emptyDir: {}
---
> emptyDir: null
> gcePersistentDisk:
> readOnly: true
> pdName: ldap-persistent-storage

可以看到生产环境的配置和基本配置的 diff 主要在于 replicasemptyDir 等字段,与预期相符

此外,我们可以使用 KCL 命令行工具的 -o 参数将编译产生的 YAML 输出到文件中,并查看文件之间的 diff

# Generate base deployment
kcl main.k -o deployment.yaml
# Generate prod deployment
kcl main.k -o prod-deployment.yaml -D env=prod
# Diff prod deployment and base deployment
diff prod-deployment.yaml deployment.yaml

当然我们也可以将 KCL 工具与 kubectl 等工具结合使用,将生产环境的配置下发到集群当中

$ kcl main.k -D env=prod | kubectl apply -f -

deployment.apps/ldap created

可以从命令行的结果看出与我们使用直接使用 Kustomize 配置和 kubectl apply 的一个 Deployment 体验完全一致,并且无更多的副作用

最后,通过 kubectl 检查部署状态

$ kubectl get deploy

NAME READY UP-TO-DATE AVAILABLE AGE
ldap 0/6 6 0 15s

小结

本期内容大概简单介绍了用 KCL 编写复杂多环境 Kubernetes 配置的快速入门和使用 Kustomize 工具进行 Kubernetes 多环境配置管理的对比,可以看出相比于 Kustomize, KCL 在实现配置复用和覆盖的基础上,通过代码化的方式减少了配置文件的个数和代码行数,提升了信息密度比,并且同 Kustomize 一样是一个纯客户端方案,并不会对集群有额外依赖或造成负担。