服务端应用
Kubernetes v1.22 [stable]
(默认启用:true)Kubernetes 支持多个应用者协同管理单个对象的字段。
Server-Side Apply 提供了一种可选机制,用于集群的控制平面跟踪对象字段的变更。在特定资源的层面,Server-Side Apply 记录并跟踪对该对象字段的控制信息。
Server-Side Apply 帮助用户和控制器通过声明性配置管理其资源。客户端可以通过提交其 完全指定意图 来声明性地创建和修改对象。
完全指定意图是一个部分对象,仅包含用户有主见的字段和值。该意图会创建一个新对象(对未指定字段使用默认值),或者由 API 服务器与现有对象进行组合。
与 Client-Side Apply 的比较 解释了 Server-Side Apply 与原始的客户端 kubectl apply
实现有何不同。
字段管理
Kubernetes API 服务器跟踪所有新创建对象的 受管字段。
尝试应用对象时,值不同且由另一个管理器拥有的字段将导致冲突。这样做是为了提示该操作可能会撤销其他协作者的变更。对带有受管字段的对象进行写入可以被强制执行,在这种情况下,任何冲突字段的值将被覆盖,所有权也将被转移。
无论何时字段的值发生变化,所有权都会从当前管理器转移到进行更改的管理器。
Apply 会检查是否有其他字段管理器也拥有该字段。如果该字段未被任何其他字段管理器拥有,则该字段将被设置为其默认值(如果存在),否则将从对象中删除。同样的规则适用于列表、关联列表或映射中的字段。
从 Server-Side Apply 的意义上来说,用户管理一个字段意味着用户依赖并期望该字段的值不会改变。最后断言了某个字段值的用户将被记录为当前的字段管理器。可以通过使用 HTTP POST
(创建)、PUT
(更新) 或非 Apply PATCH
(补丁) 显式更改字段管理器详细信息来实现此操作。你也可以通过在 Server-Side Apply 操作中包含该字段的值来声明和记录字段管理器。
Server-Side Apply 补丁请求要求客户端将其身份作为字段管理器提供。使用 Server-Side Apply 时,尝试更改由不同管理器控制的字段会导致请求被拒绝,除非客户端强制覆盖。有关覆盖的详细信息,请参阅冲突。
当两个或多个应用者将一个字段设置为相同的值时,他们共享该字段的所有权。任何后续尝试更改共享字段值的操作,无论由哪个应用者执行,都会导致冲突。共享字段所有者可以通过发出不包含该字段的 Server-Side Apply 补丁请求来放弃对该字段的所有权。
字段管理详细信息存储在对象metadata
的 managedFields
字段中。
如果你从清单中移除一个字段并应用该清单,Server-Side Apply 会检查是否有其他字段管理器也拥有该字段。如果该字段未被任何其他字段管理器拥有,则会从活动对象中删除该字段,或者如果该字段有默认值,则将其重置为默认值。同样的规则适用于关联列表或映射中的条目。
与由 kubectl
管理的(旧版)kubectl.kubernetes.io/last-applied-configuration
注解相比,Server-Side Apply 使用一种更具声明性的方法,它跟踪用户(或客户端)的字段管理,而不是用户最后应用的状态。使用 Server-Side Apply 的一个副作用是,关于对象中每个字段由哪个字段管理器管理的信息也变得可用。
示例
使用 Server-Side Apply 创建的简单对象示例如下所示
注意
kubectl get
默认省略受管字段。当输出格式为 json
或 yaml
时,添加 --show-managed-fields
以显示 managedFields
。---
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
namespace: default
labels:
test-label: test
managedFields:
- manager: kubectl
operation: Apply # note capitalization: "Apply" (or "Update")
apiVersion: v1
time: "2010-10-10T0:00:00Z"
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:labels:
f:test-label: {}
f:data:
f:key: {}
data:
key: some value
该示例 ConfigMap 对象在 .metadata.managedFields
中包含一条字段管理记录。字段管理记录由管理实体本身的基本信息以及有关受管字段和相关操作(Apply
或 Update
)的详细信息组成。如果最后更改该字段的请求是 Server-Side Apply 补丁操作,则 operation
的值为 Apply
;否则为 Update
。
还有另一种可能的结果。客户端可能提交无效的请求体。如果完全指定的意图未能生成有效的对象,则请求会失败。
然而,通过更新或不使用 Server-Side Apply 的补丁操作更改 .metadata.managedFields
是可能的。强烈不建议这样做,但如果 .metadata.managedFields
进入不一致状态(正常操作下不应发生这种情况),尝试这样做可能是一个合理的选择。
managedFields
的格式在 Kubernetes API 参考中有所描述。
注意
.metadata.managedFields
字段由 API 服务器管理。你应该避免手动更新它。冲突
冲突 是一种特殊的状态错误,当 Apply
操作尝试更改一个其他管理器也声称管理的字段时发生。这可以防止应用者无意中覆盖其他用户设置的值。发生冲突时,应用者有 3 种方法来解决冲突:
覆盖值,成为唯一管理器: 如果覆盖值是故意的(或者如果应用者是控制器之类的自动化进程),应用者应该将
force
查询参数设置为 true(对于kubectl apply
,你可以使用--force-conflicts
命令行参数),然后再次发出请求。这将强制操作成功,更改字段的值,并将该字段从managedFields
中所有其他管理器的条目中移除。不覆盖值,放弃管理声明: 如果应用者不再关心该字段的值,可以将该字段从其本地资源模型中移除,然后提交一个省略该特定字段的新请求。这将保留值不变,并导致该字段从应用者在
managedFields
中的条目中移除。不覆盖值,成为共享管理器: 如果应用者仍然关心某个字段的值,但不想覆盖它,他们可以在其本地资源模型中更改该字段的值,使其与服务器上对象的值相匹配,然后发出一个考虑了该本地更新的新请求。这样做将保留值不变,并导致该字段的管理由应用者与所有其他已经声称管理该字段的字段管理器共享。
字段管理器
管理器标识修改对象的不同工作流(在冲突时特别有用!),可以通过 fieldManager
查询参数作为修改请求的一部分来指定。当你对资源执行 Apply 操作时,fieldManager
参数是必需的。对于其他更新,API 服务器会从 "User-Agent:" HTTP 头(如果存在)推断出字段管理器的身份。
当你使用 kubectl
工具执行 Server-Side Apply 操作时,kubectl
默认将管理器身份设置为 "kubectl"
。
序列化
在协议层面,Kubernetes 将 Server-Side Apply 消息体表示为 YAML,媒体类型为 application/apply-patch+yaml
。
注意
无论是提交 JSON 数据还是 YAML 数据,都使用 application/apply-patch+yaml
作为 Content-Type
头的值。
所有 JSON 文档都是有效的 YAML。然而,Kubernetes 存在一个缺陷,它使用的 YAML 解析器没有完全实现 YAML 规范。某些 JSON 转义可能无法识别。
序列化方式与 Kubernetes 对象相同,但客户端无需发送完整的对象。
以下是 Server-Side Apply 消息体(完全指定意图)的示例
{
"apiVersion": "v1",
"kind": "ConfigMap"
}
(这会进行无更改的更新,前提是它作为补丁请求的主体发送到有效的 v1/configmaps
资源,并且具有适当的请求 Content-Type
)。
字段管理范围内的操作
Kubernetes API 中考虑字段管理的操作包括
- Server-Side Apply (HTTP
PATCH
,内容类型application/apply-patch+yaml
) - 替换现有对象(Kubernetes 中的更新;HTTP 层面上的
PUT
)
这两种操作都会更新 .metadata.managedFields
,但行为略有不同。
除非你指定强制覆盖,否则遇到字段级别冲突的 Apply 操作总是会失败;相反,如果你使用更新对受管字段进行更改,冲突永远不会导致操作失败。
所有 Server-Side Apply 补丁请求都必须通过提供 fieldManager
查询参数来标识自己,而对于更新操作,该查询参数是可选的。最后,在使用 Apply
操作时,不能在提交的请求体中定义 managedFields
。
具有多个管理器的示例对象可能如下所示
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
namespace: default
labels:
test-label: test
managedFields:
- manager: kubectl
operation: Apply
time: '2019-03-30T15:00:00.000Z'
apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:labels:
f:test-label: {}
- manager: kube-controller-manager
operation: Update
apiVersion: v1
time: '2019-03-30T16:00:00.000Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
f:key: {}
data:
key: new value
在此示例中,第二个操作作为更新运行,由名为 kube-controller-manager
的管理器执行。更新请求成功并更改了 data 字段中的一个值,这导致该字段的管理权更改为 kube-controller-manager
。
如果此更新改为使用 Server-Side Apply 进行尝试,则会因所有权冲突而失败。
合并策略
通过 Server-Side Apply 实现的合并策略通常提供更稳定的对象生命周期。Server-Side Apply 尝试根据管理字段的执行者来合并字段,而不是根据值来覆盖。这样,多个执行者可以更新同一个对象而不会造成意外干扰。
当用户将 完全指定意图 对象发送到 Server-Side Apply 端点时,服务器将其与活动对象合并,如果在两处都指定了值,则优先使用请求体中的值。如果应用配置中存在的条目集合不是同一用户上次应用条目的超集,则未被任何其他应用者管理的每个缺失条目都会被删除。有关在合并时如何使用对象模式进行决策的更多信息,请参阅 sigs.k8s.io/structured-merge-diff。
Kubernetes API(以及为 Kubernetes 实现该 API 的 Go 代码)允许定义 合并策略标记。这些标记描述了 Kubernetes 对象内字段支持的合并策略。对于 CustomResourceDefinition,可以在定义自定义资源时设置这些标记。
Golang 标记 | OpenAPI 扩展 | 可能的值 | 描述 |
---|---|---|---|
//+listType | x-kubernetes-list-type | atomic /set /map | 适用于列表。set 适用于仅包含标量元素的列表。这些元素必须是唯一的。map 仅适用于嵌套类型的列表。键值(参见 listMapKey )在列表中必须是唯一的。atomic 可适用于任何列表。如果配置为 atomic ,则在合并期间整个列表将被替换。在任何时间点,单个管理器拥有该列表。如果是 set 或 map ,不同的管理器可以单独管理条目。 |
//+listMapKey | x-kubernetes-list-map-keys | 字段名称列表,例如 ["port", "protocol"] | 仅当 +listType=map 时适用。一个字段名称列表,其值唯一标识列表中的条目。虽然可以有多个键,但 listMapKey 是单数,因为需要在 Go 类型中单独指定键。键字段必须是标量。 |
//+mapType | x-kubernetes-map-type | atomic /granular | 适用于映射。atomic 意味着映射只能由单个管理器完全替换。granular 意味着映射支持单独的管理器更新单个字段。 |
//+structType | x-kubernetes-map-type | atomic /granular | 适用于结构体;否则用法和 OpenAPI 注解与 //+mapType 相同。 |
如果缺少 listType
,API 服务器会将 patchStrategy=merge
标记解释为 listType=map
,将相应的 patchMergeKey
标记解释为 listMapKey
。
atomic
列表类型是递归的。
(在 Kubernetes 的 Go 代码中,这些标记被指定为注释,代码作者无需将其重复为字段标签)。
自定义资源和 Server-Side Apply
默认情况下,Server-Side Apply 将自定义资源视为非结构化数据。所有键都被视为与结构体字段相同,并且所有列表都被视为原子类型。
如果 CustomResourceDefinition 定义了包含在前述合并策略部分中定义的注解的模式,则在合并此类型的对象时将使用这些注解。
跨拓扑变更的兼容性
在极少数情况下,CustomResourceDefinition (CRD) 或内置资源的作者可能希望更改其资源中某个字段的特定拓扑,而无需增加其 API 版本。通过升级集群或更新 CRD 来更改类型拓扑,在更新现有对象时会产生不同的后果。有两种变更类别:一种是字段从 map
/set
/granular
变为 atomic
,另一种是相反。
当 listType
、mapType
或 structType
从 map
/set
/granular
变为 atomic
时,现有对象的整个列表、映射或结构体最终将由拥有这些类型元素的执行者拥有。这意味着对这些对象的任何进一步更改都会导致冲突。
当 listType
、mapType
或 structType
从 atomic
变为 map
/set
/granular
时,API 服务器无法推断这些字段的新所有权。因此,当对象更新这些字段时,不会产生冲突。因此,不建议将类型从 atomic
更改为 map
/set
/granular
。
例如,自定义资源
---
apiVersion: example.com/v1
kind: Foo
metadata:
name: foo-sample
managedFields:
- manager: "manager-one"
operation: Apply
apiVersion: example.com/v1
fieldsType: FieldsV1
fieldsV1:
f:spec:
f:data: {}
spec:
data:
key1: val1
key2: val2
在 spec.data
从 atomic
更改为 granular
之前,manager-one
拥有字段 spec.data
及其内部所有字段(key1
和 key2
)。当 CRD 更改为使 spec.data
变为 granular
时,manager-one
继续拥有顶级字段 spec.data
(这意味着没有其他管理器可以在不冲突的情况下删除名为 data
的映射),但它不再拥有 key1
和 key2
,因此另一个管理器可以修改或删除这些字段而不会产生冲突。
在控制器中使用 Server-Side Apply
作为控制器开发者,你可以使用 Server-Side Apply 来简化控制器的更新逻辑。与读-修改-写和/或补丁的主要区别如下:
- 应用的对象必须包含控制器关心的所有字段。
- 没有办法移除控制器之前未应用的字段(控制器仍然可以针对这些用例发送补丁或更新)。
- 无需事先读取对象;无需指定
resourceVersion
。
强烈建议控制器总是强制解决它们拥有和管理的对象上的冲突,因为它们可能无法解决或处理这些冲突。
转移所有权
除了冲突解决提供的并发控制之外,Server-Side Apply 还提供了协调字段所有权从用户转移到控制器的方法。
这最好通过示例来解释。让我们看看如何在使用 HorizontalPodAutoscaler 资源及其配套控制器为 Deployment 启用自动水平伸缩的同时,安全地将 replicas
字段的所有权从用户转移到控制器。
假设用户定义了一个将 replicas
设置为所需值的 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
用户已使用 Server-Side Apply 创建了 Deployment,如下所示
kubectl apply -f https://k8s.io/examples/application/ssa/nginx-deployment.yaml --server-side
随后,为 Deployment 启用了自动伸缩;例如
kubectl autoscale deployment nginx-deployment --cpu-percent=50 --min=1 --max=10
现在,用户希望从其配置中移除 replicas
,以免无意中与 HorizontalPodAutoscaler (HPA) 及其控制器冲突。然而,存在一个竞态条件:HPA 可能需要一些时间才会觉得有必要调整 .spec.replicas
;如果用户在 HPA 写入该字段并成为其所有者之前移除 .spec.replicas
,那么 API 服务器会将 .spec.replicas
设置为 1(Deployment 的默认副本数)。这并非用户希望发生的事情,即使是暂时的——它很可能会降低正在运行的工作负载的性能。
有两种解决方案
(基本)在配置中保留
replicas
;当 HPA 最终写入该字段时,系统会为此向用户报告冲突。此时,从配置中移除它是安全的。(更高级)然而,如果用户不想等待,例如他们希望集群配置对同事保持清晰易懂,那么他们可以采取以下步骤来安全地从配置中移除
replicas
:
首先,用户定义一个仅包含 replicas
字段的新清单
# Save this file as 'nginx-deployment-replicas-only.yaml'.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
注意
在这种情况下,SSA 的 YAML 文件只包含你想更改的字段。如果你只想使用 SSA 修改spec.replicas
字段,则无需提供一个完全符合规范的 Deployment 清单。用户使用一个私有的字段管理器名称应用该清单。在此示例中,用户选择了 handover-to-hpa
kubectl apply -f nginx-deployment-replicas-only.yaml \
--server-side --field-manager=handover-to-hpa \
--validate=false
如果应用结果与 HPA 控制器发生冲突,则不执行任何操作。冲突表明控制器在处理过程中比有时更早地声明了该字段。
此时用户可以从其清单中移除 replicas
字段
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
请注意,无论何时 HPA 控制器将 replicas
字段设置为新值,临时字段管理器将不再拥有任何字段,并将自动删除。无需进行进一步清理。
在管理器之间转移所有权
字段管理器可以通过在彼此应用的配置中将字段设置为相同的值来在彼此之间转移字段的所有权,从而共享字段的所有权。一旦管理器共享了该字段的所有权,其中一方可以从其应用的配置中移除该字段,以放弃所有权并将所有权转移给另一方字段管理器。
与客户端应用(Client-Side Apply)的比较
服务器端应用(Server-Side Apply)既是为了取代原有的客户端 kubectl apply
子命令实现,也是为了让控制器能够简单有效地执行其变更的机制。
与由 kubectl
管理的 last-applied
注解相比,服务器端应用使用了一种更具声明性的方法,它跟踪对象的字段管理情况,而非用户的最后应用状态。这意味着使用服务器端应用的一个副作用是,关于对象中每个字段由哪个字段管理器管理的信息也变得可用。
服务器端应用实现的冲突检测和解决的一个结果是,应用者(applier)在其本地状态中总是拥有最新的字段值。如果不是最新的,下次应用时就会遇到冲突。解决冲突的三种选项中的任何一种都会导致应用配置成为服务器上对象字段的最新子集。
这与客户端应用不同,在客户端应用中,被其他用户覆盖的过时值会保留在应用者的本地配置中。这些值只有在用户更新该特定字段时(如果更新的话)才会变得准确,并且应用者无法知道他们下次应用是否会覆盖其他用户的更改。
另一个区别是,使用客户端应用的应用者无法更改他们正在使用的 API 版本,但服务器端应用支持这种情况。
客户端应用与服务器端应用之间的迁移
从客户端应用升级到服务器端应用
使用 kubectl apply
管理资源的客户端应用用户,可以使用以下标志开始使用服务器端应用。
kubectl apply --server-side [--dry-run=server]
默认情况下,对象的字段管理会从客户端应用转移到 kubectl 服务器端应用,而不会遇到冲突。
注意
保持 last-applied-configuration
注解的最新状态。该注解推断客户端应用管理的字段。任何不由客户端应用管理的字段都会引发冲突。
例如,如果你在使用客户端应用后使用了 kubectl scale
更新了副本(replicas)字段,那么该字段不由客户端应用所有,并在 kubectl apply --server-side
时产生冲突。
此行为适用于使用 kubectl
字段管理器的服务器端应用。作为例外,你可以通过指定一个不同的、非默认的字段管理器来选择退出此行为,如下例所示。kubectl 服务器端应用的默认字段管理器是 kubectl
。
kubectl apply --server-side --field-manager=my-manager [--dry-run=server]
从服务器端应用降级到客户端应用
如果你使用 kubectl apply --server-side
管理资源,你可以直接使用 kubectl apply
降级到客户端应用。
降级之所以有效,是因为如果你使用 kubectl apply
,kubectl 服务器端应用会保持 last-applied-configuration
注解的最新状态。
此行为适用于使用 kubectl
字段管理器的服务器端应用。作为例外,你可以通过指定一个不同的、非默认的字段管理器来选择退出此行为,如下例所示。kubectl 服务器端应用的默认字段管理器是 kubectl
。
kubectl apply --server-side --field-manager=my-manager [--dry-run=server]
API 实现
支持服务器端应用的资源的 PATCH
动词可以接受非官方的 application/apply-patch+yaml
内容类型。服务器端应用的用户可以将部分指定的对象作为 YAML 发送到资源的 URI,作为 PATCH
请求的正文。应用配置时,应始终包含对结果(例如期望状态)重要的所有字段。
所有 JSON 消息都是有效的 YAML。因此,除了使用 YAML 请求正文进行服务器端应用请求外,你也可以使用 JSON 请求正文,因为它们也是有效的 YAML。无论哪种情况,HTTP 请求都应使用媒体类型 application/apply-patch+yaml
。
访问控制和权限
由于服务器端应用是一种 PATCH
操作,因此主体(例如 Kubernetes RBAC 的 Role)需要 patch 权限来编辑现有资源,还需要 create 动词权限才能使用服务器端应用创建新资源。
清除 managedFields
可以通过使用 patch(JSON 合并补丁、战略合并补丁、JSON 补丁)或 update(HTTP PUT
)覆盖来从对象中剥离所有 managedFields
;换句话说,通过除 apply 之外的所有写操作。这可以通过将 managedFields
字段用一个空条目覆盖来完成。两个例子是
PATCH /api/v1/namespaces/default/configmaps/example-cm
Accept: application/json
Content-Type: application/merge-patch+json
{
"metadata": {
"managedFields": [
{}
]
}
}
PATCH /api/v1/namespaces/default/configmaps/example-cm
Accept: application/json
Content-Type: application/json-patch+json
If-Match: 1234567890123456789
[{"op": "replace", "path": "/metadata/managedFields", "value": [{}]}]
这将用包含一个空条目的列表覆盖 managedFields
,然后导致 managedFields
从对象中完全剥离。请注意,将 managedFields
设置为空列表不会重置字段。这是故意的,以便不知晓此字段的客户端永远不会剥离 managedFields
。
在重置操作与更改 managedFields
以外的其他字段相结合的情况下,这将首先导致 managedFields
被重置,然后处理其他更改。结果是,应用者将拥有同一请求中更新的所有字段的所有权。
注意
服务器端应用无法正确跟踪不接收资源对象类型的子资源的所有权。如果你将服务器端应用用于此类子资源,更改的字段可能无法被跟踪。接下来
你可以在 Kubernetes API 参考中阅读关于顶级字段 metadata
内的 managedFields
的信息。