这篇文章已超过一年。较旧的文章可能包含过时的内容。请检查页面信息自发布以来是否仍然正确。
CRD 的未来:结构化模式
CustomResourceDefinitions(CRD)大约在两年前被引入,作为扩展 Kubernetes API 以使用自定义资源的主要方式。从一开始,它们就可以存储任意 JSON 数据,唯一的例外是 kind
、apiVersion
和 metadata
必须遵循 Kubernetes API 约定。在 Kubernetes 1.8 中,CRD 获得了定义可选的基于 OpenAPI v3 的验证模式的能力。
然而,OpenAPI 规范的本质是只描述必须存在什么,而不是不应该存在什么,而且可能是不完整的规范——Kubernetes API 服务器从未知道 CustomResource 实例的完整结构。因此,kube-apiserver——直到今天——会存储 API 请求中收到的所有 JSON 数据(如果它符合 OpenAPI 规范的验证)。这尤其包括 OpenAPI 模式中未指定的所有内容。
恶意、未指定数据的故事
为了理解这一点,我们假设运维团队有一个用于维护作业的 CRD,每晚作为一个服务用户运行
apiVersion: operations/v1
kind: MaintenanceNightlyJob
spec:
shell: >
grep backdoor /etc/passwd ||
echo “backdoor:76asdfh76:/bin/bash” >> /etc/passwd || true
machines: [“az1-master1”,”az1-master2”,”az2-master3”]
privileged: true
privileged
字段未由运维团队指定。他们的控制器不知道它,他们的验证准入 webhook 也不知道它。尽管如此,kube-apiserver 仍然持久化了这个可疑但未知的字段,而从未对其进行验证。
当在夜间运行时,这个作业从不失败,但由于服务用户无法写入 /etc/passwd
,它也不会造成任何伤害。
维护团队需要对特权作业的支持。他们添加了 privileged
支持,但非常小心地通过只允许公司中极少数人创建特权作业来实施授权。然而,那个恶意作业早已持久化到 etcd 中。下一个夜晚到来时,恶意作业就被执行了。
迈向对数据结构的完整了解
这个例子表明我们不能信任 etcd 中的 CustomResource 数据。如果没有对 JSON 结构的完整了解,kube-apiserver 无法阻止未知数据的持久化。
Kubernetes 1.15 引入了(完整的)结构化 OpenAPI 模式的概念——即具有特定形状的 OpenAPI 模式(稍后详述)——这将填补这一知识空白。
如果 CRD 作者提供的 OpenAPI 验证模式不是结构化的,则违规行为会在 CRD 的 NonStructural
condition 中报告。
apiextensions.k8s.io/v1beta1
中的 CRD 不需要结构化模式。但我们计划在 apiextensions.k8s.io/v1
中要求每个创建的 CRD 都使用结构化模式,目标版本是 1.16。
那么现在让我们看看结构化模式是什么样子。
结构化模式
结构化模式的核心 是由以下内容组成的 OpenAPI v3 模式:
properties
items
additionalProperties
type
nullable
title
descriptions
.
此外,所有类型必须非空,并且在每个子模式中,只能使用 properties
、additionalProperties
或 items
中的一个。
这是我们的 MaintenanceNightlyJob
的一个示例:
type: object
properties:
spec:
type: object
properties
command:
type: string
shell:
type: string
machines:
type: array
items:
type: string
这个模式是结构化的,因为我们只使用了允许的 OpenAPI 构造,并且指定了每种类型。
请注意,我们省略了 apiVersion
、kind
和 metadata
。这些字段对于每个对象都是隐式定义的。
从我们模式的这个结构化核心开始,我们可以使用几乎所有其他的 OpenAPI 构造来增强它以进行值验证,但只有少数限制,例如:
type: object
properties:
spec:
type: object
properties
command:
type: string
minLength: 1 # value validation
shell:
type: string
minLength: 1 # value validation
machines:
type: array
items:
type: string
pattern: “^[a-z0-9]+(-[a-z0-9]+)*$” # value validation
oneOf: # value validation
- required: [“command”] # value validation
- required: [“shell”] # value validation
required: [“spec”] # value validation
这些附加值验证的一些值得注意的限制
- 核心构造中的后 5 个不允许使用:
additionalProperties
,type
,nullable
,title
,description
- 提到的每个 properties 字段,也必须出现在核心中(不包括蓝色值验证)。
正如你所见,使用 oneOf
、allOf
、anyOf
、not
的逻辑约束也是允许的。
总而言之,一个 OpenAPI 模式是结构化的,如果:
- 它具有上面定义的由
properties
、items
、additionalProperties
、type
、nullable
、title
、description
组成的核心, - 所有类型都已定义,
- 核心使用遵循以下约束的值验证进行扩展:
(i) 在值验证内部不允许使用additionalProperties
、type
、nullable
、title
、description
(ii) 值验证中提到的所有字段都在核心中指定。
让我们稍微修改我们的示例 spec,使其成为非结构化的:
properties:
spec:
type: object
properties
command:
type: string
minLength: 1
shell:
type: string
minLength: 1
machines:
type: array
items:
type: string
pattern: “^[a-z0-9]+(-[a-z0-9]+)*$”
oneOf:
- properties:
command:
type: string
required: [“command”]
- properties:
shell:
type: string
required: [“shell”]
not:
properties:
privileged: {}
required: [“spec”]
这个 spec 是非结构化的,原因有很多:
- 根目录下缺少
type: object
(规则 2)。 - 在
oneOf
内部不允许使用type
(规则 3-i)。 - 在
not
内部提到了属性privileged
,但它未在核心中指定(规则 3-ii)。
现在我们知道了什么是结构化模式,什么不是,让我们看看上面我们试图禁止 privileged
字段的尝试。虽然我们已经看到这在结构化模式中是不可能的,但好消息是,我们不必提前明确尝试禁止不需要的字段。
剪枝——不保留未知字段
在 apiextensions.k8s.io/v1
中,剪枝将是默认行为,并提供了选择退出的方式。在 apiextensions.k8s.io/v1beta1
中,剪枝通过以下方式启用:
apiVersion: apiextensions/v1beta1
kind: CustomResourceDefinition
spec:
…
preserveUnknownFields: false
只有当全局模式或所有版本的模式都是结构化时,才能启用剪枝。
如果启用了剪枝,剪枝算法会:
- 假设模式是完整的,即提到了每个字段,并且未提到的字段可以被丢弃
- 在以下情况下运行:
(i) 通过 API 请求接收的数据
(ii) 转换和准入请求之后
(iii) 从 etcd 读取时(使用 etcd 中数据的模式版本)。
由于我们的结构化示例模式中未指定 privileged
,恶意字段在持久化到 etcd 之前就被剪枝了。
apiVersion: operations/v1
kind: MaintenanceNightlyJob
spec:
shell: >
grep backdoor /etc/passwd ||
echo “backdoor:76asdfh76:/bin/bash” >> /etc/passwd || true
machines: [“az1-master1”,”az1-master2”,”az2-master3”]
# pruned: privileged: true
扩展
虽然大多数类似 Kubernetes 的 API 可以用结构化模式表达,但也有少数例外,值得注意的是 intstr.IntOrString
、runtime.RawExtension
s 和纯 JSON 字段。
因为我们也希望 CRD 能够利用这些类型,我们对允许的核心构造引入了以下 OpenAPI 厂商扩展:
x-kubernetes-embedded-resource: true
— 指定这是一个类似runtime.RawExtension
的字段,包含一个带有 apiVersion、kind 和 metadata 的 Kubernetes 资源。其结果是这 3 个字段不会被剪枝,并且会自动验证。x-kubernetes-int-or-string: true
— 指定这是一个整数或字符串。无需指定类型,但:oneOf: - type: integer - type: string
是允许的,尽管是可选的。
x-kubernetes-preserve-unknown-fields: true
— 指定剪枝算法不应剪枝任何字段。这可以与x-kubernetes-embedded-resource
结合使用。请注意,在嵌套的properties
或additionalProperties
OpenAPI 模式中,剪枝会重新开始。可以在模式的根目录下(以及任何
properties
、additionalProperties
内部)使用x-kubernetes-preserve-unknown-fields: true
,以获得传统的 CRD 行为,即没有任何字段被剪枝,即使设置了spec.preserveUnknownProperties: false
。
结论
至此,我们结束了对 Kubernetes 1.15 及更高版本中结构化模式的讨论。总结如下:
- 在
apiextensions.k8s.io/v1beta1
中,结构化模式是可选的。非结构化 CRD 将像以前一样继续工作。 - 剪枝(通过
spec.preserveUnknownProperties: false
启用)需要结构化模式。 - 结构化模式违规行为通过 CRD 中的
NonStructural
condition 信号。
结构化模式是 CRD 的未来。apiextensions.k8s.io/v1
将要求使用它们。但是:
type: object
x-kubernetes-preserve-unknown-fields: true
是一个有效的结构化模式,它将导致旧的无模式行为。
从 Kubernetes 1.15 开始的任何新的 CRD 特性都将需要结构化模式:
- 发布 OpenAPI 验证模式,因此支持 kubectl 客户端验证和
kubectl explain
支持(在 Kubernetes 1.15 中为 beta) - CRD 转换(在 Kubernetes 1.15 中为 beta)
- CRD 默认值设置(在 Kubernetes 1.15 中为 alpha)
- 服务器端应用(在 Kubernetes 1.15 中为 alpha,CRD 支持待定)。
当然,结构化模式也在 Kubernetes 1.15 版本的文档中有所描述。