翻译自 《Diving into Kubernetes MutatingAdmissionWebhook》 原文链接:https://medium.com/ibm-cloud/diving-into-kubernetes-mutatingadmissionwebhook-6ef3c5695f74
对于在数据持久化之前,拦截到 Kubernetes API server 的请求, Admissioncontrollers
是非常有用的工具。然而,由于其需要由集群管理员在 kube-apiserver 中编译成二进制文件,所以使用起来不是很灵活。从 Kubernetes 1.7 起,引入了 Initializers
和 ExternalAdmissionWebhooks
,用以解决这个问题。在 Kubernetes 1.9 中, Initializers
依然停留在 alpha 版本,然而 ExternalAdmissionWebhooks
被提升到了 beta 版,且被分成了 MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
。
MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
二者合起来就是一个特殊类型的 admission controllers
,一个处理资源更改,一个处理验证。验证是通过匹配到 MutatingWebhookConfiguration
中定义的规则完成的。
在这篇文章中,我会深入剖析 MutatingAdmissionWebhook
的细节,并一步步实现一个可用的 webhook admission server
。
Webhooks 的好处
Kubernetes 集群管理员可以使用 webhooks
来创建额外的资源更改及验证准入插件,这些准入插件可以通过 apiserver 的准入链来工作,而不需要重新编译 apiserver。这使得开发者可以对于很多动作都可以自定义准入逻辑,比如对任何资源的创建、更新、删除,给开发者提供了很大的自由和灵活度。可以使用的应用数量巨大。一些常见的使用常见包括:
- 在创建资源之前做些更改。Istio 是个非常典型的例子,在目标 pods 中注入 Envoy sidecar 容器,来实现流量管理和规则执行。
- 自动配置
StorageClass
。监听PersistentVolumeClaim
资源,并按照事先定好的规则自动的为之增添对应的StorageClass
。使用者无需关心StorageClass
的创建。 - 验证复杂的自定义资源。确保只有其被定义后且所有的依赖项都创建好并可用,自定义资源才可以创建。
- namespace 的限制。在多租户系统中,避免资源在预先保留的 namespace 中被创建。
除了以上列出来的使用场景,基于 webhooks
还可以创建更多应用。
Webhooks 和 Initializers
基于社区的反馈,以及对 ExternalAdmissionWebhooks
和 Initializers
的 alpha 版本的使用案例,Kubernetes 社区决定将 webhooks 升级到 beta 版,并且将其分成两种 webhooks( MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
)。这些更新使得 webhooks 与其他 admission controllers
保持一致,并且强制 mutate-before-validate
。 Initializers
可以在 Kubernetes 资源创建前更改,从而实现动态准入控制。如果你对 Initializers
不熟悉,可以参考这篇文章。
所以,到底 Webhooks
和 Initializers
之间的区别是什么呢?
-
Webhooks
可以应用于更多操作,包括对于资源 “增删改” 的 “mutate” 和 “admit”;然而Initializers
不可以对 “删” 资源进行 “admit”。 -
Webhooks
在创建资源前不允许查询;然而Initializers
可以监听未初始化的资源,通过参数?includeUninitialized=true
来实现。 - 由于
Initializers
会把 “预创建” 状态也持久化到 etcd,因此会引入高延迟且给 etcd 带来负担,尤其在 apiserver 升级或失败时;然而 Webhooks 消耗的内存和计算资源更少。 -
Webhooks
比Initializers
对失败的保障更强大。Webhooks
的配置中可以配置失败策略,用以避免资源在创建的时候被 hang 住。然而Initializers
在尝试创建资源的时候可能会 block 住所有的资源。
除了上面列举的不同点, Initializer
在较长一段开发时间内还存在很多已知问题,包括配额补充错误等。 Webhooks
升级为 beta 版也就预示着在未来 Webhooks
会是开发目标。如果你需要更稳定的操作,我推荐使用 Webhooks
。
MutatingAdmissionWebhook 如何工作
MutatingAdmissionWebhook
在资源被持久化到 etcd 前,根据规则将其请求拦截,拦截规则定义在 MutatingWebhookConfiguration
中。 MutatingAdmissionWebhook
通过对 webhook server
发送准入请求来实现对资源的更改。而 webhook server
只是一个简单的 http 服务器。
下面这幅图详细描述了 MutatingAdmissionWebhook
如何工作:
MutatingAdmissionWebhook
需要三个对象才能运行:
MutatingWebhookConfiguration
MutatingAdmissionWebhook
需要根据 MutatingWebhookConfiguration
向 apiserver 注册。在注册过程中, MutatingAdmissionWebhook
需要说明:
- 如何连接
webhook admission server
; - 如何验证
webhook admission server
; -
webhook admission server
的 URL path; - webhook 需要操作对象满足的规则;
-
webhook admission server
处理时遇到错误时如何处理。
MutatingAdmissionWebhook 本身
MutatingAdmissionWebhook
是一种插件形式的 admission controller
,且可以配置到 apiserver 中。 MutatingAdmissionWebhook
插件可以从 MutatingWebhookConfiguration
中获取所有感兴趣的 admission webhooks
。
然后 MutatingAdmissionWebhook
监听 apiserver 的请求,拦截满足条件的请求,并并行执行。
Webhook Admission Server
WebhookAdmissionServer
只是一个附着到 k8s apiserver 的 http server。对于每一个 apiserver 的请求, MutatingAdmissionWebhook
都会发送一个 admissionReview
到相关的 webhook admission server
。 webhook admission server
再决定如何更改资源。
MutatingAdmissionWebhook 教程
编写一个完整的 WebhookAdmissionServer
可能令人生畏。为了方便起见,我们编写一个简单的 WebhookAdmissionServer
来实现注入 nginx sidecar 容器以及挂载 volume。完整代码在 kube-mutating-webhook-tutorial。这个项目参考了 Kubernetes webhook 示例和 Istio sidecar 注入实现。
在接下来的段落里,我会向你展示如何编写可工作的容器化 webhook admission server
,并将其部署到 Kubernetes 集群中。
前置条件
MutatingAdmissionWebhook
要求 Kubernetes 版本为 1.9.0 及以上,其 admissionregistration.k8s.io/v1beta1
API 可用。确保下面的命令:
其输出为:
另外, MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
准入控制器需要以正确的顺序加入到 kube-apiserver
的 admission-control
标签中。
编写 Webhook Server
WebhookAdmissionServer
是一个简单的 http 服务器,遵循 Kubernetes API。我粘贴部分伪代码来描述主逻辑:
以上代码的详解:
-
sidecarCfgFile
包含了 sidecar 注入器模板,其在下面的 ConfigMap 中定义; -
certFile
和keyFile
是秘钥对,会在webhook server
和 apiserver 之间的 TLS 通信中用到; - 19 行开启了 https server,以监听 443 端口路径为
'/mutate'
。
接下来我们关注处理函数 serve
的主要逻辑:
函数 serve
是一个简单的 http 处理器,参数为 http request
和 response writer
。
- 首先将请求组装为
AdmissionReview
,其中包括object
、oldobject
及userInfo
… - 然后触发 Webhook 主函数
mutate
来创建patch
,以实现注入 sidecar 容器及挂载 volume。 - 最后,将
admission decision
和额外patch
组装成响应,并发送回给 apiserver。
对于函数 mutate
的实现,你可以随意发挥。我就以我的实现方式做个例子:
从上述代码中可以看出,函数 mutate
请求了 mutationRequired
来决定这个改动是否被允许。对于被允许的请求,函数 mutate
从另一个函数 createPatch
中获取到修改体 'patch'
。注意这里函数 mutationRequired
的诡计,我们跳过了带有注解 sidecar-injector-webhook.morven.me/inject:true
。这里稍后会在部署 deployment
的时候提到。完整代码请参考这里。
编写 Dockerfile 并构建
创建构建脚本:
以下面为依赖编写 Dockerfile 文件:
在手动构建容器前,你需要 Docker ID 账号并将 image name 和 tag (Dockerfile 和 deployment.yaml 文件中)修改成你自己的,然后执行以下命令:
编写 Sidecar 注入配置
现在我们来创建一个 Kubernetes ConfigMap
,包含需要注入到目标 pod 中的容器和 volume
信息:
从上面的清单中看,这里需要另一个包含 nginx conf
的 ConfigMap
。完整 yaml 参考 nginxconfigmap.yaml。
然后将这两个 ConfigMap 部署到集群中:
创建包含秘钥对的 Secret
由于准入控制是一个高安全性操作,所以对外在的 webhook server
提供 TLS 是必须的。作为流程的一部分,我们需要创建由 Kubernetes CA 签名的 TLS 证书,以确保 webhook server
和 apiserver 之间通信的安全性。对于 CSR 创建和批准的完整步骤,请参考 这里 。
简单起见,我们参考了 Istio 的脚本并创建了一个类似的名为 webhook-create-signed-cert.sh
的脚本,来自动生成证书及秘钥对并将其加入到 secret
中。
运行脚本后,包含证书和秘钥对的 secret
就被创建出来了:
创建 Sidecar 注入器的 Deployment 和 Service
deployment 带有一个 pod,其中运行的就是 sidecar-injector
容器。该容器以特殊参数运行:
-
sidecarCfgFile
指的是 sidecar 注入器的配置文件,挂载自上面创建的 ConfigMapsidecar-injector-webhook-configmap
。 -
tlsCertFile
和tlsKeyFile
是秘钥对,挂载自 Secretinjector-webhook-certs
。 -
alsologtostderr
、v=4
和2>&1
是日志参数。
Service 暴露带有 app=sidecar-injector
label 的 pod,使之在集群中可访问。这个 Service 会被 MutatingWebhookConfiguration
中定义的 clientConfig
部分访问,默认的端口 spec.ports.port
需要设置为 443。
然后将上述 Deployment 和 Service 部署到集群中,并且验证 sidecar 注入器的 webhook server
是否 running:
动态配置 webhook 准入控制器
MutatingWebhookConfiguration
中具体说明了哪个 webhook admission server
是被使用的并且哪些资源受准入服务器的控制。建议你在创建 MutatingWebhookConfiguration
之前先部署 webhook admission server
,并确保其正常工作。否则,请求会被无条件接收或根据失败规则被拒。
现在,我们根据下面的内容创建 MutatingWebhookConfiguration
:
第 8 行: name
- webhook 的名字,必须指定。多个 webhook 会以提供的顺序排序; 第 9 行: clientConfig
- 描述了如何连接到 webhook admission server
以及 TLS 证书; 第 15 行: rules
- 描述了 webhook server
处理的资源和操作。在我们的例子中,只拦截创建 pods 的请求; 第 20 行: namespaceSelector
- namespaceSelector
根据资源对象是否匹配 selector
决定了是否针对该资源向 webhook server
发送准入请求。
在部署 MutatingWebhookConfiguration
前,我们需要将 ${CA_BUNDLE}
替换成 apiserver 的默认 caBundle
。我们写个脚本来自动匹配:
然后执行:
我们看不到任何日志描述 webhook server
接收到准入请求,似乎该请求并没有发送到 webhook server
一样。所以有一种可能性是这是被 MutatingWebhookConfiguration
中的配置触发的。再确认一下 MutatingWebhookConfiguration
我们会发现下面的内容:
通过 namespaceSelector 控制 sidecar 注入器
我们在 MutatingWebhookConfiguration
中配置了 namespaceSelector
,也就意味着只有在满足条件的 namespace 下的资源能够被发送到 webhook server
。于是我们将 default 这个 namespace 打上标签 sidecar-injector=enabled
:
现在我们配置的 MutatingWebhookConfiguration
会在 pod 创建的时候就注入 sidecar 容器。将运行中的 pod 删除并确认是否创建了新的带 sidecar 容器的 pod:
可以看到,sidecar 容器和 volume 被成功注入到应用中。至此,我们成功创建了可运行的带 MutatingAdmissionWebhook
的 sidecar 注入器。通过 namespaceSelector
,我们可以轻易的控制在特定的 namespace 中的 pods 是否需要被注入 sidecar 容器。
但这里有个问题,根据以上的配置,在 default 这个 namespace 下的所有 pods 都会被注入 sidecar 容器,无一例外。
通过注解控制 sidecar 注入器
多亏了 MutatingAdmissionWebhook
的灵活性,我们可以轻易的自定义变更逻辑来筛选带有特定注解的资源。还记得上面提到的注解 sidecar-injector-webhook.morven.me/inject:"true"
吗?在 sidecar 注入器中这可以当成另一种控制方式。在 webhook server
中我写了一段逻辑来跳过那行不带这个注解的 pod。
我们来尝试一下。在这种情况下,我们创建另一个 sleep 应用,其 podTemplateSpec
中不带注解 sidecar-injector-webhook.morven.me/inject:"true"
:
然后确认 sidecar 注入器是否跳过了这个 pod:
结果显示,这个 sleep 应用只包含一个容器,没有额外的容器和 volume 注入。然后我们将这个 deployment 增加注解,并确认其重建后是否被注入 sidecar:
与预期一致,pod 被注入了额外的 sidecar 容器。至此,我们就获得了一个可工作的 sidecar 注入器,可由 namespaceSelector
或更细粒度的由注解控制。
总结
MutatingAdmissionWebhook
是 Kubernetes 扩展功能中最简单的方法之一,工作方式是通过全新规则控制、资源更改。
此功能使得更多的工作模式变成了可能,并且支持了更多生态系统,包括服务网格平台 Istio。从 Istio 0.5.0 开始,Istio 的自动注入部分的代码被重构,实现方式从 initializers
变更为 MutatingAdmissionWebhook
。
参考文献
- http://blog.kubernetes.io/2018/01/extensible-admission-is-beta.html
- https://docs.google.com/document/d/1c4kdkY3ha9rm0OIRbGleCeaHknZ-NR1nNtDp-i8eH8E/view
- https://v1-8.docs.kubernetes.io/docs/admin/extensible-admission-controllers/
- https://github.com/kubernetes/kubernetes/tree/release-1.9/test/images/webhook