准入 Webhook 最佳实践
本页面提供了在 Kubernetes 中设计**准入 Webhook** 时的最佳实践和注意事项。此信息适用于运行准入 Webhook 服务器或修改或验证你的 API 请求的第三方应用程序的集群操作员。
在阅读本页面之前,请确保你熟悉以下概念:
良好 Webhook 设计的重要性
当任何创建、更新或删除请求发送到 Kubernetes API 时,就会发生准入控制。准入控制器会拦截符合你定义的特定条件的请求。然后,这些请求会发送到变更准入 Webhook 或验证准入 Webhook。这些 Webhook 通常用于确保对象规范中的特定字段存在或具有特定的允许值。
Webhook 是扩展 Kubernetes API 的强大机制。设计不当的 Webhook 常常会导致工作负载中断,因为 Webhook 对集群中对象的控制权过大。像其他 API 扩展机制一样,Webhook 很难大规模测试其与所有工作负载、其他 Webhook、插件和附加组件的兼容性。
此外,在每个版本中,Kubernetes 都会通过新功能、将功能提升到 Beta 或稳定状态以及弃用功能来添加或修改 API。即使稳定的 Kubernetes API 也可能会发生变化。例如,Pod API 在 v1.29 中发生了变化,添加了边车容器功能。虽然 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 的准入控制。
对 CustomResourceDefinitions 使用内置验证和默认值
如果你使用CustomResourceDefinitions,请不要使用准入 Webhook 来验证 CustomResource 规范中的值或设置字段的默认值。Kubernetes 允许你在创建 CustomResourceDefinitions 时定义验证规则和默认字段值。
要了解更多信息,请参阅以下资源:
性能和延迟
本节描述了提高性能和减少延迟的建议。总而言之,这些建议如下:
- 整合 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 在集群内运行,你可以在类型为ClusterIP
的服务后面运行多个 Webhook 后端。
使用高可用性部署模型
在设计 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
字段来添加特定的字段进行变更,然后再允许准入。确保你只修改需要更改的字段。
例如,考虑一个变更 Webhook,它配置为确保web-server
Deployment 至少有三个副本。当创建 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 重新调度到另一个节点。但是,由于未设置env
标签,现有 Webhook 服务器会拒绝请求。结果,迁移无法发生。
使用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 中边车容器的发布在 Pod API 中添加了一个restartPolicy
字段。
防止你的 Webhook 自我触发
响应广泛 API 请求的变更 Webhook 可能会无意中自我触发。例如,考虑一个响应集群中所有请求的 Webhook。如果你将 Webhook 配置为为每次变更创建 Event 对象,它将响应其自己的 Event 对象创建请求。
为了避免这种情况,请考虑在你创建的任何资源中设置一个唯一的标签。将此标签从你的 Webhook 匹配条件中排除。
不要更改不可变对象
API 服务器中的某些 Kubernetes 对象无法更改。例如,当你部署静态 Pod时,节点上的 kubelet 会在 API 服务器中创建镜像 Pod来跟踪静态 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
的边车容器。
在这些情况下,Webhook 可以安全地重新调用,或者准入一个已经设置了字段的对象。
以下示例显示了非幂等变更逻辑:
对于**创建** Pod 请求,注入一个名为
foo-sidecar
并带有当前时间戳后缀的边车容器(例如foo-sidecar-19700101-000000
)。重新调用 Webhook 可能导致同一个边车被多次注入到 Pod 中,每次都使用不同的容器名称。类似地,如果边车已经存在于用户提供的 Pod 中,Webhook 可能会注入重复的容器。
对于**创建**/**更新** Pod 请求,如果 Pod 具有标签
env
,则拒绝,否则向 Pod 添加env: prod
标签。重新调用 Webhook 将导致 Webhook 在其自己的输出上失败。
对于**创建** Pod 请求,附加一个名为
foo-sidecar
的边车容器,而不检查是否存在foo-sidecar
容器。重新调用 Webhook 将导致 Pod 中出现重复的容器,从而使请求无效并被 API 服务器拒绝。
变更测试与验证
本节提供了关于测试变更 Webhook 和验证变更对象的建议。总而言之,这些建议如下:
- 在预演环境中测试 Webhook。
- 避免违反验证的变更。
- 测试小版本升级以检查回归和冲突。
- 在准入前验证变更对象。
在预演环境中测试 Webhook
健壮的测试应成为你的新或更新 Webhook 发布周期的核心部分。如果可能,请在与你的生产集群非常相似的预演环境中测试你集群 Webhook 的任何更改。至少,考虑使用minikube或kind等工具为 Webhook 更改创建一个小型测试集群。
确保变更不会违反验证
你的变更 Webhook 不应破坏在准入前适用于对象的任何验证。例如,考虑一个将 Pod 的默认 CPU 请求设置为特定值的变更 Webhook。如果该 Pod 的 CPU 限制设置为低于变更请求的值,则该 Pod 将无法准入。
根据在集群中运行的验证测试每个变更 Webhook。
测试小版本升级以确保行为一致
在将生产集群升级到新的小版本之前,请在预演环境中测试你的 Webhook 和工作负载。比较结果以确保你的 Webhook 在升级后继续按预期运行。
此外,请使用以下资源了解 API 更改:
在准入前验证变更
变更 Webhook 在任何验证 Webhook 运行之前完成运行。应用于对象的变更没有稳定的顺序。因此,你的变更可能会被稍后运行的变更 Webhook 覆盖。
向集群添加验证准入控制器,例如 ValidatingAdmissionWebhook 或 ValidatingAdmissionPolicy,以确保你的变更仍然存在。例如,考虑一个变更 Webhook,它将restartPolicy: Always
字段插入到特定的 init 容器中,使它们作为边车容器运行。你可以运行一个验证 Webhook 来确保这些 init 容器在所有变更完成后保留了restartPolicy: Always
配置。
有关详细信息,请参阅以下资源:
变更 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,确保以下访问仅对受信任的实体可用:
- 动词:**create**、**update**、**patch**、**delete**、**deletecollection**
- API 组:
admissionregistration.k8s.io/v1
- API 种类:MutatingWebhookConfigurations
如果你的变更 Webhook 服务器在集群中运行,请限制创建或修改该命名空间中任何资源的访问。
良好实现的示例
以下项目是“良好”自定义 Webhook 服务器实现的示例。你可以将它们作为设计自己的 Webhook 的起点。不要按原样使用这些示例;请将它们作为起点,并设计你的 Webhook 以在你的特定环境中良好运行。
下一步
本页上的项目引用了提供 Kubernetes 所需功能的第三方产品或项目。Kubernetes 项目作者不对这些第三方产品或项目负责。有关详细信息,请参阅CNCF 网站指南。
在提议添加额外第三方链接的更改之前,你应该阅读内容指南。