Admission Webhook 最佳实践
本页面提供了在 Kubernetes 中设计 准入 Webhook 时的最佳实践和注意事项。这些信息适用于运行准入 Webhook 服务器或修改或验证 API 请求的第三方应用程序的集群操作员。
阅读本页面之前,请确保你熟悉以下概念:
良好 Webhook 设计的重要性
任何创建、更新或删除请求发送到 Kubernetes API 时都会触发准入控制。准入控制器会拦截符合你定义的特定条件的请求。然后,这些请求会发送到 修改准入 Webhook 或 验证准入 Webhook。这些 Webhook 通常用于确保对象规约中的特定字段存在或具有特定的允许值。
Webhook 是扩展 Kubernetes API 的强大机制。设计糟糕的 Webhook 常常会导致工作负载中断,因为它们对集群中的对象具有很大的控制权。与其他 API 扩展机制一样,Webhook 难以进行大规模测试,以确保与所有工作负载、其他 Webhook、附加组件和插件的兼容性。
此外,在每个版本中,Kubernetes 都会添加或修改 API,引入新功能、将功能提升到 beta 或稳定状态以及废弃功能。即使是稳定的 Kubernetes API 也可能发生变化。例如,Pod
API 在 v1.29 中发生了变化,添加了Sidecar 容器功能。虽然 Kubernetes 对象因新的 Kubernetes API 进入损坏状态的情况很少见,但与早期 API 版本一起正常工作的 Webhook 可能无法协调该 API 的最新更改。这可能导致你将集群升级到较新版本后出现意外行为。
本页面介绍了常见的 Webhook 故障场景以及如何通过谨慎周到地设计和实现 Webhook 来避免这些问题。
确定你是否使用了准入 Webhook
即使你没有运行自己的准入 Webhook,你在集群中运行的一些第三方应用程序也可能使用修改或验证准入 Webhook。
要检查你的集群是否有修改准入 Webhook,请运行以下命令:
kubectl get mutatingwebhookconfigurations
输出会列出集群中的所有修改准入控制器。
要检查你的集群是否有验证准入 Webhook,请运行以下命令:
kubectl get validatingwebhookconfigurations
输出会列出集群中的所有验证准入控制器。
选择准入控制机制
Kubernetes 包含多种准入控制和策略实施选项。了解何时使用特定选项可以帮助你改进延迟和性能,减少管理开销,并在版本升级期间避免问题。下表描述了允许你在准入期间修改或验证资源的机制:
机制 | 描述 | 用例 |
---|---|---|
修改准入 Webhook | 在准入之前拦截 API 请求,并根据需要使用自定义逻辑进行修改。 |
|
修改准入策略 | 在准入之前拦截 API 请求,并根据需要使用通用表达语言 (CEL) 表达式进行修改。 |
|
验证准入 Webhook | 在准入之前拦截 API 请求,并根据复杂的策略声明进行验证。 |
|
验证准入策略 | 在准入之前拦截 API 请求,并根据 CEL 表达式进行验证。 |
|
通常,当你想使用可扩展的方式声明或配置逻辑时,请使用 Webhook 准入控制。当你想要声明更简单的逻辑且不产生运行 Webhook 服务器的开销时,请使用内置的基于 CEL 的准入控制。Kubernetes 项目建议你尽可能使用基于 CEL 的准入控制。
为 CustomResourceDefinition 使用内置验证和默认值
如果你使用了 自定义资源定义 (CustomResourceDefinitions),请不要使用准入 Webhook 来验证 CustomResource 规约中的值或设置字段的默认值。Kubernetes 允许你在创建 CustomResourceDefinition 时定义验证规则和默认字段值。
要了解更多信息,请参阅以下资源:
性能和延迟
本节介绍了改进性能和降低延迟的建议。总结如下:
- 合并 Webhook 并限制每个 Webhook 的 API 调用次数。
- 使用审计日志检查重复执行相同操作的 Webhook。
- 使用负载均衡来确保 Webhook 的可用性。
- 为每个 Webhook 设置较小的超时值。
- 在 Webhook 设计期间考虑集群可用性需求。
设计低延迟的准入 Webhook
修改准入 Webhook 是按顺序调用的。根据修改 Webhook 的设置,某些 Webhook 可能会被多次调用。每次修改 Webhook 调用都会增加准入过程的延迟。这与并行调用的验证 Webhook 不同。
设计修改 Webhook 时,请考虑你的延迟需求和容忍度。集群中的修改 Webhook 越多,延迟增加的可能性就越大。
考虑以下几点来降低延迟:
- 合并对不同对象执行类似修改的 Webhook。
- 减少修改 Webhook 服务器逻辑中进行的 API 调用次数。
- 限制每个修改 Webhook 的匹配条件,以减少特定 API 请求触发的 Webhook 数量。
- 将小型 Webhook 合并到一个服务器和配置中,以帮助进行排序和组织。
防止竞争控制器导致的循环
考虑集群中运行的任何其他组件,它们可能与你的 Webhook 进行的修改发生冲突。例如,如果你的 Webhook 添加了一个标签,而另一个控制器又将其移除,则你的 Webhook 会再次被调用。这会导致循环。
要检测这些循环,请尝试以下方法:
更新你的集群审计策略以记录审计事件。使用以下参数:
level
:RequestResponse
verbs
:["patch"]
omitStages
:RequestReceived
设置审计规则,为你的 Webhook 修改的特定资源创建事件。
检查审计事件,查看 Webhook 是否因对同一对象应用相同的补丁而被多次重新调用,或者某个对象的某个字段是否被多次更新和恢复。
设置较小的超时值
准入 Webhook 应尽快评估(通常在毫秒级),因为它们会增加 API 请求延迟。为 Webhook 使用较小的超时时间。
有关详细信息,请参见超时。
使用负载均衡确保 Webhook 可用性
准入 Webhook 应该利用某种形式的负载均衡来提供高可用性和性能优势。如果 Webhook 在集群内运行,你可以将多个 Webhook 后端部署在一个类型为 ClusterIP
的 Service 后面。
使用高可用性部署模型
设计 Webhook 时,请考虑集群的可用性需求。例如,在节点停机或区域中断期间,Kubernetes 会将 Pod 标记为 NotReady
,以便负载均衡器将流量重新路由到可用的区域和节点。这些对 Pod 的更新可能会触发你的修改 Webhook。根据受影响的 Pod 数量,修改 Webhook 服务器可能存在超时或导致 Pod 处理延迟的风险。因此,流量将无法如你所需的那样快速重新路由。
编写 Webhook 时,请考虑上述示例所示的情况。排除由于 Kubernetes 响应不可避免的事件而产生的操作。
请求过滤
本节提供了过滤哪些请求触发特定 Webhook 的建议。总结如下:
- 限制 Webhook 范围,以避免系统组件和只读请求。
- 将 Webhook 限制到特定的命名空间。
- 使用匹配条件执行细粒度的请求过滤。
- 匹配对象的所有版本。
限制每个 Webhook 的范围
只有当 API 请求与相应的 Webhook 配置匹配时,才会调用准入 Webhook。限制每个 Webhook 的范围,以减少对 Webhook 服务器的不必要调用。考虑以下范围限制:
- 避免匹配
kube-system
命名空间中的对象。如果你在kube-system
命名空间中运行自己的 Pod,请使用objectSelector
以避免修改关键工作负载。 - 不要修改节点租约,它们作为 Lease 对象存在于
kube-node-lease
系统命名空间中。修改节点租约可能导致节点升级失败。只有在你确信这些控制不会使集群面临风险时,才对该命名空间中的 Lease 对象应用验证控制。 - 不要修改 TokenReview 或 SubjectAccessReview 对象。这些始终是只读请求。修改这些对象可能会损坏你的集群。
- 使用
namespaceSelector
将每个 Webhook 限制在特定的命名空间。
使用匹配条件过滤特定请求
准入控制器支持多个字段,你可以使用这些字段来匹配符合特定条件的请求。例如,你可以使用 namespaceSelector
来过滤针对特定命名空间的请求。
要进行更细粒度的请求过滤,请在你的 Webhook 配置中使用 matchConditions
字段。该字段允许你编写多个 CEL 表达式,这些表达式必须评估为 true
才能触发你的准入 Webhook。使用 matchConditions
可以显著减少对 Webhook 服务器的调用次数。
有关详细信息,请参见匹配请求:matchConditions
。
匹配 API 的所有版本
默认情况下,准入 Webhook 在影响指定资源的任何 API 版本上运行。Webhook 配置中的 matchPolicy
字段控制此行为。在 matchPolicy
字段中指定值 Equivalent
或省略该字段,以允许 Webhook 在任何 API 版本上运行。
有关详细信息,请参见匹配请求:matchPolicy
。
修改范围和字段注意事项
本节提供了关于修改范围以及对象字段的特殊注意事项的建议。总结如下:
- 仅对需要打补丁的字段打补丁。
- 不要覆盖数组值。
- 尽可能避免修改中的副作用。
- 避免自我修改。
- 开放失败并验证最终状态。
- 规划未来版本中的字段更新。
- 防止 Webhook 自我触发。
- 不要修改不可变对象。
仅对必需字段打补丁
准入 Webhook 服务器发送 HTTP 响应,指示如何处理特定的 Kubernetes API 请求。此响应是一个 AdmissionReview 对象。修改 Webhook 可以通过在响应中使用 patchType
字段和 patch
字段来添加要修改的特定字段,然后允许准入。确保仅修改需要更改的字段。
例如,考虑一个配置为确保 web-server
Deployment 至少有三个副本的修改 Webhook。当创建 Deployment 对象的请求与你的 Webhook 配置匹配时,Webhook 应该只更新 spec.replicas
字段中的值。
不要覆盖数组值
Kubernetes 对象规约中的字段可能包含数组。一些数组包含键值对(例如容器规约中的 envVar
字段),而其他数组则没有键(例如 Pod 规约中的 readinessGates
字段)。在某些情况下,数组字段中值的顺序可能很重要。例如,容器规约中 args
字段中参数的顺序可能会影响容器。
修改数组时考虑以下几点:
- 尽可能使用
add
JSONPatch 操作而不是replace
,以避免意外替换必需值。 - 将不使用键值对的数组视为集合。
- 确保你修改的字段中的值不需要按特定顺序排列。
- 除非绝对必要,否则不要覆盖现有的键值对。
- 修改标签字段时要小心。意外的修改可能会导致标签选择器失效,从而产生意外行为。
避免副作用
确保你的 Webhook 仅对发送给它们的 AdmissionReview 内容进行操作,并且不进行带外更改。这些额外的更改称为 副作用,如果未正确协调,可能会在准入期间导致冲突。如果 Webhook 没有副作用,则应将 .webhooks[].sideEffects
字段设置为 None
。
如果在准入评估期间需要副作用,则在处理 dryRun
设置为 true
的 AdmissionReview 对象时必须抑制它们,并且 .webhooks[].sideEffects
字段应设置为 NoneOnDryRun
。
有关详细信息,请参见副作用。
避免自我修改
在集群内部运行的 Webhook 如果配置为拦截启动其自身的 Pod 所需的资源,可能会导致其自己的部署死锁。
例如,一个修改准入 Webhook 被配置为仅在 Pod 中设置了特定标签(例如 env: prod
)时才准入 创建 Pod 请求。Webhook 服务器运行在一个未设置 env
标签的 Deployment 中。
当运行 Webhook 服务器 Pod 的节点不健康时,Webhook Deployment 会尝试将 Pod 重新调度到另一个节点。然而,现有的 Webhook 服务器会拒绝请求,因为 env
标签未设置。结果,迁移无法发生。
使用 namespaceSelector
排除你的 Webhook 正在运行的命名空间。
避免依赖循环
依赖循环可能发生在以下场景中:
- 两个 Webhook 检查彼此的 Pod。如果两个 Webhook 同时变得不可用,则两个 Webhook 都无法启动。
- 你的 Webhook 拦截了你的 Webhook 所依赖的集群附加组件,例如网络插件或存储插件。如果 Webhook 和依赖的附加组件都变得不可用,则两者都无法正常工作。
为避免这些依赖循环,请尝试以下方法:
- 使用 ValidatingAdmissionPolicies 来避免引入依赖。
- 防止 Webhook 验证或修改其他 Webhook。考虑排除特定命名空间,使其不触发你的 Webhook。
- 通过使用
objectSelector
防止你的 Webhook 对依赖的附加组件进行操作。
开放失败并验证最终状态。
修改准入 Webhook 支持 failurePolicy
配置字段。此字段指示如果 Webhook 失败,API 服务器是应该准入还是拒绝请求。Webhook 故障可能由于超时或服务器逻辑错误而发生。
默认情况下,准入 Webhook 将 failurePolicy
字段设置为 Fail。如果 Webhook 失败,API 服务器会拒绝请求。但是,默认拒绝请求可能会导致在 Webhook 停机期间拒绝符合要求的请求。
通过将 failurePolicy
字段设置为 Ignore,让你的修改 Webhook "开放失败"。使用验证控制器检查请求的状态,以确保它们符合你的策略。
这种方法具有以下优点:
- 修改 Webhook 停机不会影响符合要求的资源部署。
- 策略实施发生在验证准入控制期间。
- 修改 Webhook 不会干扰集群中的其他控制器。
规划未来字段更新
通常,在设计 Webhook 时,应假设 Kubernetes API 可能在后续版本中发生变化。不要编写认为 API 稳定性是理所当然的服务器。例如,Kubernetes 中 sidecar 容器的发布向 Pod API 添加了 restartPolicy
字段。
防止你的 Webhook 自我触发
响应广泛 API 请求的修改 Webhook 可能会无意中自我触发。例如,考虑一个响应集群中所有请求的 Webhook。如果你将 Webhook 配置为为每次修改创建 Event 对象,则它将响应其自身的 Event 对象创建请求。
为避免此问题,请考虑在你创建的任何资源中设置一个唯一的标签。将此标签从你的 Webhook 匹配条件中排除。
不要修改不可变对象
API 服务器中的某些 Kubernetes 对象无法更改。例如,当你部署一个 静态 Pod 时,节点上的 kubelet 会创建一个 镜像 Pod 在 API 服务器中跟踪静态 Pod。然而,对镜像 Pod 的更改不会传播到静态 Pod。
在准入期间不要尝试修改这些对象。所有镜像 Pod 都有 kubernetes.io/config.mirror
注解。为了排除镜像 Pod 并降低忽略注解带来的安全风险,只允许静态 Pod 在特定命名空间中运行。
修改 Webhook 顺序和幂等性
本节提供了关于 Webhook 顺序和设计幂等 Webhook 的建议。总结如下:
- 不要依赖特定的执行顺序。
- 在准入之前验证修改。
- 检查修改是否被其他控制器覆盖。
- 确保修改 Webhook 的集合是幂等的,而不仅仅是单个 Webhook。
不要依赖修改 Webhook 的调用顺序
修改准入 Webhook 的运行顺序并不一致。各种因素可能会改变调用特定 Webhook 的时间。不要依赖你的 Webhook 在准入过程的特定点运行。其他 Webhook 仍然可以修改你已修改的对象。
以下建议可能有助于最大限度地降低意外更改的风险:
确保集群中的修改 Webhook 是幂等的
每个修改准入 Webhook 都应该是 幂等 的。Webhook 应该能够在它已经修改过的对象上运行,而不会在原有更改之外再进行额外的更改。
此外,集群中的所有修改 Webhook,作为一个集合,都应该是幂等的。准入控制的修改阶段结束后,每个单独的修改 Webhook 应该能够在对象上运行,而不会对该对象进行额外的更改。
根据你的环境,大规模确保幂等性可能具有挑战性。以下建议可能有所帮助:
- 使用验证准入控制器来验证关键工作负载的最终状态。
- 在预演集群中测试你的部署,查看是否有任何对象被同一 Webhook 多次修改。
- 确保每个修改 Webhook 的范围是具体和有限的。
以下示例展示了幂等修改逻辑:
对于 创建 Pod 请求,将 Pod 的
.spec.securityContext.runAsNonRoot
字段设置为 true。对于 创建 Pod 请求,如果容器的
.spec.containers[].resources.limits
字段未设置,则设置默认资源限制。对于 创建 Pod 请求,如果不存在名为
foo-sidecar
的容器,则注入一个名为foo-sidecar
的 sidecar 容器。
在这些情况下,Webhook 可以安全地重新调用,或准入已设置了字段的对象。
以下示例展示了非幂等修改逻辑:
对于 创建 Pod 请求,注入一个名为
foo-sidecar
并附加当前时间戳(例如foo-sidecar-19700101-000000
)的 sidecar 容器。重新调用 Webhook 可能导致同一 Sidecar 多次注入到 Pod 中,每次注入时容器名称都不同。类似地,如果用户提供的 Pod 中已存在 Sidecar,Webhook 也可能注入重复的容器。
对于 创建/更新 Pod 请求,如果 Pod 设置了标签
env
,则拒绝该请求;否则,为 Pod 添加env: prod
标签。重新调用 Webhook 会导致 Webhook 在自己的输出上失败。
对于 创建 Pod 请求,直接附加一个名为
foo-sidecar
的 Sidecar 容器,而不检查是否存在同名容器。重新调用 Webhook 将导致 Pod 中出现重复的容器,这会使请求无效并被 API 服务器拒绝。
变更测试和验证
本节提供有关测试变更型 Webhook 和验证变更后的对象的建议。总结如下:
- 在预生产环境中测试 Webhook。
- 避免产生违反验证规则的变更。
- 测试小版本升级以检查回归和冲突。
- 在准入前验证变更后的对象。
在预生产环境中测试 Webhook
健壮的测试应该是新 Webhook 或更新的 Webhook 发布周期的核心部分。如果可能,请在与你的生产集群非常相似的预生产环境中测试集群 Webhook 的任何更改。至少,可以考虑使用 minikube 或 kind 等工具创建小型测试集群以进行 Webhook 更改测试。
确保变更不违反验证规则
变更型 Webhook 不应该破坏任何在准入前适用于对象的验证规则。例如,考虑一个变更型 Webhook,它将 Pod 的默认 CPU request 设置为特定值。如果该 Pod 的 CPU limit 设置得低于变更后的 request 值,则该 Pod 将无法通过准入。
针对在你的集群中运行的验证规则测试每个变更型 Webhook。
测试小版本升级以确保一致的行为
在将生产集群升级到新的小版本之前,请在预生产环境中测试 Webhook 和工作负载。比较结果,确保 Webhook 在升级后继续按预期运行。
此外,使用以下资源来了解 API 变更:
在准入之前验证修改
变更型 Webhook 在所有验证型 Webhook 运行之前运行并完成。变更应用于对象的顺序是不稳定的。因此,你的变更可能会被稍后运行的变更型 Webhook 覆盖。
在你的集群中添加一个验证准入控制器,例如 ValidatingAdmissionWebhook 或 ValidatingAdmissionPolicy,以确保你的变更仍然存在。例如,考虑一个变更型 Webhook,它为特定的 init 容器插入 restartPolicy: Always
字段,使其作为 Sidecar 容器运行。你可以运行一个验证型 Webhook 来确保这些 init 容器在所有变更完成后保留了 restartPolicy: Always
配置。
有关详细信息,请参阅以下资源:
变更型 Webhook 部署
本节提供有关部署变更型准入 Webhook 的建议。总结如下:
- 按命名空间逐步推出 Webhook 配置并监控问题。
- 限制对编辑 Webhook 配置资源的访问。
- 如果 Webhook 服务器位于集群内,则限制对运行 Webhook 服务器的命名空间的访问。
安装并启用变更型 Webhook
准备好将变更型 Webhook 部署到集群时,请按以下操作顺序进行:
- 安装并启动 Webhook 服务器。
- 将 MutatingWebhookConfiguration 清单中的
failurePolicy
字段设置为Ignore
。这可以避免由配置错误的 Webhook 引起的中断。 - 将 MutatingWebhookConfiguration 清单中的
namespaceSelector
字段设置为测试命名空间。 - 将 MutatingWebhookConfiguration 部署到集群。
在测试命名空间中监控 Webhook 以检查是否存在任何问题,然后将 Webhook 推广到其他命名空间。如果 Webhook 拦截了它不应该拦截的 API 请求,请暂停推广并调整 Webhook 配置的范围。
限制对变更型 Webhook 的编辑访问
变更型 Webhook 是强大的 Kubernetes 控制器。使用 RBAC 或其他授权机制限制对 Webhook 配置和服务器的访问。对于 RBAC,请确保以下访问仅对可信实体可用:
- 动词(Verbs):create、update、patch、delete、deletecollection
- API 组(API group):
admissionregistration.k8s.io/v1
- API Kind:MutatingWebhookConfigurations
如果变更型 Webhook 服务器在集群中运行,则限制在该命名空间中创建或修改任何资源的访问。
优秀实现的示例
以下项目是“优秀”自定义 Webhook 服务器实现的示例。在设计自己的 Webhook 时,可以将其用作起点。请勿按原样使用这些示例;将它们作为起点,并设计你的 Webhook 以便在你的特定环境中良好运行。
下一步
本页上的项目提及提供 Kubernetes 所需功能的第三方产品或项目。Kubernetes 项目作者不对这些第三方产品或项目负责。有关详细信息,请参阅 CNCF 网站指南。
在提议添加额外第三方链接的更改之前,你应该阅读内容指南。