Skip to main content
版本: 0.10

使用 KCL Schema 编写复杂配置

1. 介绍

KCL 是一种简单易用的配置语言,用户可以简单地编写可重用的配置代码。

在本节教程中,我们将学习如何使用 KCL 编写定制配置,这样我们就可以定义一个架构并以协作方式编写配置。

本节将会学习

  1. 定义一个简单的 schema
  2. 为 schema 字段设置默认的不可变值
  3. 基于简单的 schema 创建配置
  4. 在 schema 中编写复杂的逻辑
  5. 通过 schema 的组合创建新的 schema
  6. 使用 dict/map 创建具有深度嵌套 schema 的配置
  7. 通过 schema 继承创建新的 schema
  8. 通过多个 mixin schema 创建新的 schema
  9. 声明 schema 验证规则
  10. 配置 schema 的输出布局
  11. 共享和重用 schema

2. 编写简单的 Schema

假设我们希望定义一个具有特定属性的工作负载,我们可以通过创建一个 my_config.k 文件来创建一个简单的配置。我们可以按以下方式填写下面的代码,定义一个可重复使用的部署配置的 schema:

schema Deployment:
name: str
cpu: int
memory: int
image: str
service: str
replica: int
command: [str]
labels: {str:str}

在上述代码中,cpumemory 被定义为 int 值;nameimageservice 是字符串;command 是由字符串构成的列表;labels 是字典类型,其键和值的类型均为字符串。

另外,每个属性都必须被赋予非 None 值作为 schema 实例,除非它被标记问号 ? 而作为可选参数。

schema Deployment:
name: str
cpu: int
memory: int
image: str
service: str
replica: int
command: [str]
labels?: {str:str} # labels 是一个可选的参数

当存在继承关系时:

  • 如果在基 schema 中该属性为可选(optional)参数,则在子 schema 中它应该是可选的(optional)或必需的(required)。
  • 如果在基 schema 中该属性为必需(required)属性,则在子 schema 中它需要是必需的(required)。

3. Enhance Schema as Needed

Suppose we need to set default values to service and replica, we can make them as below:

schema Deployment:
name: str
cpu: int
memory: int
image: str
service: str = "my-service" # defaulting
replica: int = 1 # defaulting
command: [str]
labels?: {str:str} # labels is an optional attribute

And then we can set the service type annotation as the string literal type to make it immutable:

schema Deployment:
name: str
cpu: int
memory: int
image: str
service: "my-service" = "my-service"
replica: int = 1
command: [str]
labels?: {str:str}

In the schema, type hint is a must, for example we can define cpu as cpu: int.

Specially, we can define a string-interface dict as {str:}, and in case we want to define an object or interface, just define as {:}.

4. 基于简单 Schema 创建配置

现在我们有了一个简单的 schema 定义,我们可以用它来定义配置:

nginx = Deployment {
name = "my-nginx"
cpu = 256
memory = 512
image = "nginx:1.14.2"
command = ["nginx"]
labels = {
run = "my-nginx"
env = "pre-prod"
}
}

使用以下 KCL 命令运行,我们应该能够看到生成的 yaml 文件作为输出,如下所示:

KCL 命令:

    kcl my_config.k

标准输出:

nginx:
name: my-nginx
cpu: 256
memory: 512
image: nginx:1.14.2
service: my-service
replica: 1
command:
- nginx
labels:
run: my-nginx
env: pre-prod

有关集合数据类型和块的更多详细信息,请查看手册和规范。

此外,配置选择器表达式(config selector expressions)可以用于初始化 schema 实例,我们可以忽略配置表达式中行末的逗号。

nginx = Deployment {
name = "my-nginx"
cpu = 256
memory = 512
image = "nginx:1.14.2"
command = ["nginx"] # 忽略行尾的逗号
labels.run = "my-nginx" # schema 中的字典变量可以使用选择器表达式
labels.env = "pre-prod" # schema 中的字典变量可以使用选择器表达式
}

5. 在 Schema 中编写更为复杂的逻辑

假设我们有一些schema逻辑,我们可以将它包装进 schema 中:

schema Deployment[priority]:
name: str
image: str
service: "my-service" = "my-service"
replica: int = 1
command: [str]
labels?: {str:str}

_cpu = 2048
if priority == 1:
_cpu = 256
elif priority == 2:
_cpu = 512
elif priority == 3:
_cpu = 1024
else:
_cpu = 2048

cpu: int = _cpu
memory: int = _cpu * 2

现在,我们可以通过创建 schema 实例来定义配置,并将优先级作为参数传递给模式:

nginx = Deployment(priority=2) {
name = "my-nginx"
image = "nginx:1.14.2"
command = ["nginx"]
labels.run = "my-nginx"
labels.env = "pre-prod"
}

使用以下 KCL 命令运行,我们应该能够看到生成的 yaml 文件作为输出,如下所示:

KCL 命令:

kcl my_config.k

标准输出:

nginx:
name: my-nginx
cpu: 512
memory: 1024
image: nginx:1.14.2
service: my-service
replica: 1
command:
- nginx
labels:
run: my-nginx
env: pre-prod

6. 通过 Schema 组合创建新 Schema

现在我们想要定义一个详细的 schema,包括服务(service)和卷(volumes),我们可以按以下方式进行操作:

schema Deployment[priority]:
name: str
volumes?: [Volume]
image: str
service?: Service
replica: int = 1
command: [str]
labels?: {str:str}

if priority == 1:
_cpu = 256
elif priority == 2:
_cpu = 512
elif priority == 3:
_cpu = 1024
else:
_cpu = 2048

cpu: int = _cpu
memory: int = _cpu * 2

schema Port:
name: str
protocol: str
port: int
targetPort: int

schema Service:
name: "my-service" = "my-service"
ports: [Port]

schema Volume:
name: str
mountPath: str
hostPath: str

在这种情况下,Deployment 由 Service 和一系列 Volume 组成,而 Service 又由一系列 Port 组成。

7. 使用 dict/map 创建具有深度嵌套 schema 的配置

现在我们有一个新的 Deployment schema,但我们可能会注意到,它包含多层嵌套的结构,在复杂的结构定义中,这是非常常见的,我们通常必须编写命令式组装代码来生成最终结构。

使用 KCL,我们可以使用简单的字典声明创建配置,并具有完整的 schema 初始化和验证功能。例如,我们可以按照以下方式使用新的 Deployment schema简单地配置 nginx:

nginx = Deployment(priority=2) {
name = "my-nginx"
image = "nginx:1.14.2"
volumes = [Volume {
name = "mydir"
mountPath = "/test-pd"
hostPath = "/data"
}]
command = ["nginx"]
labels.run = "my-nginx"
labels.env = "pre-prod"
service.ports = [Port {
name = "http"
protocol = "TCP"
port = 80
targetPort = 9376
}]
}

使用以下 KCL 命令运行,我们应该能够看到生成的 yaml 文件作为输出,如下所示:

KCL 命令:

kcl my_config.k

标准输出:

nginx:
name: my-nginx
cpu: 512
memory: 1024
volumes:
- name: mydir
mountPath: /test-pd
hostPath: /data
image: nginx:1.14.2
service:
name: my-service
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
replica: 1
command:
- nginx
labels:
run: my-nginx
env: pre-prod

请注意,我们用于定义 Deployment 配置的字典必须与 schema 定义对齐,否则我们将会得到一个错误。例如,假设我们将服务端口的类型定义错误如下:

nginx = Deployment(priority=2) {
name = "my-nginx"
image = "nginx:1.14.2"
volumes = [Volume {
name = "mydir"
mountPath = "/test-pd"
hostPath = "/data"
}]
command = ["nginx"]
labels.run = "my-nginx"
labels.env = "pre-prod"
service.ports = [Port {
name = "http"
protocol = "TCP"
port = [80] # 错误的数据类型,试图将 List 分配给 int
targetPort = 9376
}]
}

使用以下 KCL 命令运行,我们应该能够看到生成的 yaml 文件作为输出,如下所示:

KCL 命令:

kcl my_config.k

标准错误输出:

The type got is inconsistent with the type expected: expect int, got [int(80)]

8. 声明 Schema 验证规则

现在我们已经看到了一个复杂的 schema,在其中每个字段都有一个类型提示,以使其更加不容错(error-prone)。

但是这还不够好,我们希望为我们的 schema 支持更多的增强验证,以便尽快发现 schema 和配置中的代码错误。许多验证规则,如 None 类型检查、范围检查、值检查、长度检查、正则表达式匹配、枚举检查已经被添加或陆续添加进来。以下是一段代码示例:

import regex

schema Deployment[priority]:
name: str
volumes?: [Volume]
image: str
service?: Service
replica: int = 1
command: [str]
labels?: {str:str}

if priority == 1:
_cpu = 256
elif priority == 2:
_cpu = 512
elif priority == 3:
_cpu = 1024
else:
_cpu = 2048

cpu: int = _cpu
memory: int = _cpu * 2

check:
multiplyof(cpu, 256), "cpu must be a multiplier of 256"
regex.match(image, "^[a-zA-Z]+:\d+\.\d+\.\d+$"), "image name should be like 'nginx:1.14.2'"
1 <= replica < 100, "replica should be in range (1, 100)"
len(labels) >= 2 if labels, "the length of labels should be large or equal to 2"
"env" in labels, "'env' must be in labels"
len(command) > 0, "the command list should be non-empty"

schema Port:
name: str
protocol: str
port: int
targetPort: int

check:
port in [80, 443], "we can only expose 80 and 443 port"
protocol in ["HTTP", "TCP"], "protocol must be either HTTP or TCP"
1024 < targetPort, "targetPort must be larger than 1024"

schema Service:
name: "my-service" = "my-service"
ports: [Port]

check:
len(ports) > 0, "ports list must be non-empty"

schema Volume:
name: str
mountPath: str
hostPath: str

由于schema定义的属性默认是必需的(required),因此可以省略判断变量不能为 None/Undefined 的验证。

schema Volume:
name: str
mountPath: str
hostPath: str

现在我们可以基于新的 schema 编写配置,并及时暴露配置错误。例如,使用以下无效的配置:

nginx = Deployment(priority=2) {
name = "my-nginx"
image = "nginx:1142" # 镜像值不匹配正则表达式
volumes = [Volume {
name = "mydir"
mountPath = "/test-pd"
hostPath = "/data"
}]
command = ["nginx"]
labels.run = "my-nginx"
labels.env = "pre-prod"
service.ports = [Port {
name = "http"
protocol = "TCP"
port = 80
targetPort = 9376
}]
}

每个字段都是类型有效的,但镜像名无效。

运行 KCL,我们将看到如下错误信息:

KCL 命令:

kcl my_config.k

标准错误输出:

Schema check is failed to check condition: regex.match(image, "^[a-zA-Z]+:\d+\.\d+\.\d+$"), "image name should be like 'nginx:1.14.2'"

KCL 的验证功能涵盖了 Openapi 定义的验证,因此我们可以通过 KCL 编写任何 API 验证。

9. 通过 Schema 继承创建新 Schema

现在,我们拥有了一个稳定的部署 schema 定义,可以用它来声明配置。

通常,部署 schema 将被用于多个场景中。我们可以直接使用 schema 在不同的用例中声明配置(见上文的部分),或者我们可以通过继承生成一个更具体的 schema 定义。

例如,我们可以使用部署 schema 作为基础,来定义 nginx 的基本 schema,并在每个场景中扩展定义。在这种情况下,我们定义了一些常用的属性。请注意,我们使用“final”关键字将名称标记为不可变,以防止被覆盖。

schema Nginx(Deployment):
""" A base nginx schema """
name: "my-nginx" = "my-nginx"
image: str = "nginx:1.14.2"
replica: int = 3
command: [str] = ["nginx"]

schema NginxProd(Nginx):
""" A prod nginx schema with stable configurations """
volumes: [Volume] = [{
name = "mydir"
mountPath = "/test-pd"
hostPath = "/data"
}]
""" A volume mapped to host path """
service: Service = {
ports = [{
name = "http"
protocol = "TCP"
port = 80
targetPort = 9376
}]
}
""" An 80 port to target backend server """

现在我们有了一些 nginx 的静态配置。建议将我们认为是静态的配置声明在那里,并将更多的动态配置放在下面:

nginx = Nginx {
labels.run = "my-nginx"
labels.env = "pre-prod"
}
nginx = NginxProd {
labels.run = "my-nginx"
labels.env = "pre-prod"
}

现在,我们只需要通过运行时标签值 “prod” 来简单定义 不那么静态的 nginx 生产环境配置。

实际上,在某些复杂情况下,我们可以将所有配置分为基本配置、业务配置和环境配置定义,并基于此实现团队成员之间的协作。

使用以下 KCL 命令运行,我们应该能够看到生成的 yaml 文件作为输出,如下所示:

KCL 命令:

kcl prod_config.k

标准输出:

nginx:
name: my-nginx
cpu: 512
memory: 1024
volumes:
- name: mydir
mountPath: /test-pd
hostPath: /data
image: nginx:1.14.2
service:
name: my-service
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
replica: 3
command:
- nginx
labels:
run: my-nginx
env: pre-prod

10. Create New Schema by Multiple Protocol and Mixin Schemas Inheritance

现在,我们可以通过 Deployment schema 完成服务器配置的声明。

然而,通常实际情况更为复杂,部署可能有各种可选变量附件。

例如,我们想要在现有 schema 中支持声明持久卷,作为可重用的 Kubernetes schema。在这种情况下,我们可以通过以下 mixinprotocal 进行包装:

import k8spkg.api.core.v1

protocol PVCProtocol:
pvc?: {str:}

mixin PersistentVolumeClaimMixin for PVCProtocol:
"""
PersistentVolumeClaim (PVC) sample:
Link: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims
"""

# Mix in a new attribute `kubernetesPVC`
kubernetesPVC?: v1.PersistentVolumeClaim

if pvc:
kubernetesPVC = v1.PersistentVolumeClaim {
metadata.name = pvc.name
metadata.labels = pvc.labels
spec = {
accessModes = pvc.accessModes
resources = pvc.resources
storageClassName = pvc.storageClassName
}
}

有了 PersistentVolumeClaimMixin,我们使用清晰的用户界面(user interface)定义了一个 PVC schema,并使用 Kubernetes PVC 作为实现。然后,我们可以使用 Deployment schema 和 PVC mixin schema 定义一个 server schema。

schema Server(Deployment):
mixin [PersistentVolumeClaimMixin]
pvc?: {str:}
""" pvc user interface data defined by PersistentVolumeClaimMixin """

在 Server schema 中,Deployment schema 是基础 schema,而 PersistentVolumeClaimMixin 是一个可选附加项,其用户界面数据为pvc?:{str:}

请注意,mixin 通常用于向宿主 schema 添加新属性,或修改宿主 schema 的现有属性。因此,mixin 可以使用宿主 schema 中的属性。由于其被设计为可重用,因此我们需要一个额外的协议来限制 mixin 中宿主 schema 中属性的名称和类型。

现在,如果我们想要使用 PVC 进行部署,只需声明用户界面:

server = Server {
name = "my-nginx"
image = "nginx:1.14.2"
volumes = [Volume {
name = "mydir"
mountPath = "/test-pd"
hostPath = "/data"
}]
command = ["nginx"]
labels = {
run = "my-nginx"
env = "pre-prod"
}
service.ports = [Port {
name = "http"
protocol = "TCP"
port = 80
targetPort = 9376
}]
pvc = {
name = "my_pvc"
accessModes = ["ReadWriteOnce"]
resources.requests.storage = "8Gi"
storageClassName = "slow"
}
}

使用以下 KCL 命令运行,我们应该能够看到生成的 yaml 文件作为输出,如下所示:

KCL 命令:

kcl server.k

标准输出:

server:
name: my-nginx
cpu: 512
memory: 1024
volumes:
- name: mydir
mountPath: /test-pd
hostPath: /data
image: nginx:1.14.2
service:
name: my-service
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
replica: 1
command:
- nginx
labels:
run: my-nginx
env: pre-prod
pvc:
name: my_pvc
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
storageClassName: slow
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my_pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: slow
resources:
requests:
storage: 8Gi

如果我们不需要持久卷,只需删除 pvc 配置块。

11. 共享和重用 Schema

可以通过导入来共享 Server schema,我们只需要将代码与 KCL 一起打包即可。

import pkg

server = pkg.Server {
name = "my-nginx"
image = "nginx:1.14.2"
volumes = [Volume {
name = "mydir"
mountPath = "/test-pd"
hostPath = "/data"
}]
command = ["nginx"]
labels.run = "my-nginx"
labels.env = "pre-prod"
service.ports = [Port {
name = "http"
protocol = "TCP"
port = 80
targetPort = 9376
}]
}

另一个关于共享代码的技巧是:在同一包下的模块不需要相互导入。

假设我们在 pkg 中有如下 models:

pkg/
- deploy.k
- server.k
- pvc.k

server.k 中,我们可以只使用 deploy.k 中的 Deployment schema 和 pvc.k 中的 pvc schema 而无需导入:

# 无需 import
schema Server(Deployment):
mixin [PersistentVolumeClaimMixin]
pvc?: {str:}
""" pvc user interface data defined by PersistentVolumeClaimMixin """

然后用户必须导入 pkg 才能作为一个整体使用它:

import pkg

server = pkg.Server {
name = "my-nginx"
image = "nginx:1.14.2"
volumes = [pkg.Volume {
name = "mydir"
mountPath = "/test-pd"
hostPath = "/data"
}]
command = ["nginx"]
labels = {
run = "my-nginx"
env = "pre-prod"
}
service.ports = [pkg.Port {
name = "http"
protocol = "TCP"
port = 80
targetPort = 9376
}]
}

运行 KCL 命令:

kcl pkg_server.k

Output:

server:
name: my-nginx
cpu: 512
memory: 1024
volumes:
- name: mydir
mountPath: /test-pd
hostPath: /data
image: nginx:1.14.2
service:
name: my-service
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
replica: 1
command:
- nginx
labels:
run: my-nginx
env: pre-prod

12. 最后

恭喜!

我们已经完成了 KCL 的第二节课。我们使用 KCL 来替换我们的 key-value 文本文件,以便获得更好的可编程性。