翻译自 《Diving into Kubernetes MutatingAdmissionWebhook》
原文链接:https://medium.com/ibm-cloud/diving-into-kubernetes-mutatingadmissionwebhook-6ef3c5695f74
对于在数据持久化之前,拦截到 Kubernetes API server 的请求,Admission controllers
是非常有用的工具。然而,由于其需要由集群管理员在 kube-apiserver 中编译成二进制文件,所以使用起来不是很灵活。从 Kubernetes 1.7 起,引入了 Initializers
和 External Admission Webhooks
,用以解决这个问题。在 Kubernetes 1.9 中,Initializers
依然停留在 alpha 版本,然而 External Admission Webhooks
被提升到了 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
基于社区的反馈,以及对 External Admission Webhooks
和 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
Webhook Admission Server
只是一个附着到 k8s apiserver 的 http server。对于每一个 apiserver 的请求,MutatingAdmissionWebhook
都会发送一个 admissionReview
到相关的 webhook admission server
。webhook admission server
再决定如何更改资源。
MutatingAdmissionWebhook 教程
编写一个完整的 Webhook Admission Server
可能令人生畏。为了方便起见,我们编写一个简单的 Webhook Admission Server
来实现注入 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 可用。确保下面的命令:
kubectl api-versions | grep admissionregistration.k8s.io/v1beta1
其输出为:
admissionregistration.k8s.io/v1beta1
另外,MutatingAdmissionWebhook
和 ValidatingAdmissionWebhook
准入控制器需要以正确的顺序加入到 kube-apiserver
的 admission-control
标签中。
编写 Webhook Server
Webhook Admission Server
是一个简单的 http 服务器,遵循 Kubernetes API。我粘贴部分伪代码来描述主逻辑:
sidecarConfig, err := loadConfig(parameters.sidecarCfgFile)
pair, err := tls.LoadX509KeyPair(parameters.certFile, parameters.keyFile)
whsvr := &WebhookServer {
sidecarConfig: sidecarConfig,
server: &http.Server {
Addr: fmt.Sprintf(":%v", 443),
TLSConfig: &tls.Config{Certificates: []tls.Certificate{pair}},
},
}
// define http server and server handler
mux := http.NewServeMux()
mux.HandleFunc("/mutate", whsvr.serve)
whsvr.server.Handler = mux
// start webhook server in new rountine
go func() {
if err := whsvr.server.ListenAndServeTLS("", ""); err != nil {
glog.Errorf("Filed to listen and serve webhook server: %v", err)
}
}()
以上代码的详解:
sidecarCfgFile
包含了 sidecar 注入器模板,其在下面的 ConfigMap 中定义;certFile
和keyFile
是秘钥对,会在webhook server
和 apiserver 之间的 TLS 通信中用到;- 19 行开启了 https server,以监听 443 端口路径为
'/mutate'
。
接下来我们关注处理函数 serve
的主要逻辑:
// Serve method for webhook server
func (whsvr *WebhookServer) serve(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
var reviewResponse *v1beta1.AdmissionResponse
ar := v1beta1.AdmissionReview{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
glog.Error(err)
reviewResponse = toAdmissionResponse(err)
} else {
reviewResponse = mutate(ar)
}
response := v1beta1.AdmissionReview{}
if reviewResponse != nil {
response.Response = reviewResponse
response.Response.UID = ar.Request.UID
}
// reset the Object and OldObject, they are not needed in a response.
ar.Request.Object = runtime.RawExtension{}
ar.Request.OldObject = runtime.RawExtension{}
resp, err := json.Marshal(response)
if err != nil {
glog.Error(err)
}
if _, err := w.Write(resp); err != nil {
glog.Error(err)
}
}
函数 serve
是一个简单的 http 处理器,参数为 http request
和 response writer
。
- 首先将请求组装为
AdmissionReview
,其中包括object
、oldobject
及userInfo
... - 然后触发 Webhook 主函数
mutate
来创建patch
,以实现注入 sidecar 容器及挂载 volume。 - 最后,将
admission decision
和额外patch
组装成响应,并发送回给 apiserver。
对于函数 mutate
的实现,你可以随意发挥。我就以我的实现方式做个例子:
// main mutation process
func (whsvr *WebhookServer) mutate(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
req := ar.Request
var pod corev1.Pod
if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
glog.Errorf("Could not unmarshal raw object: %v", err)
return &v1beta1.AdmissionResponse {
Result: &metav1.Status {
Message: err.Error(),
},
}
}
// determine whether to perform mutation
if !mutationRequired(ignoredNamespaces, &pod.ObjectMeta) {
glog.Infof("Skipping mutation for %s/%s due to policy check", pod.Namespace, pod.Name)
return &v1beta1.AdmissionResponse {
Allowed: true,
}
}
annotations := map[string]string{admissionWebhookAnnotationStatusKey: "injected"}
patchBytes, err := createPatch(&pod, whsvr.sidecarConfig, annotations)
return &v1beta1.AdmissionResponse {
Allowed: true,
Patch: patchBytes,
PatchType: func() *v1beta1.PatchType {
pt := v1beta1.PatchTypeJSONPatch
return &pt
}(),
}
}
从上述代码中可以看出,函数 mutate
请求了 mutationRequired
来决定这个改动是否被允许。对于被允许的请求,函数 mutate
从另一个函数 createPatch
中获取到修改体 'patch'
。注意这里函数 mutationRequired
的诡计,我们跳过了带有注解 sidecar-injector-webhook.morven.me/inject: true
。这里稍后会在部署 deployment
的时候提到。完整代码请参考这里。
编写 Dockerfile 并构建
创建构建脚本:
dep ensure
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o kube-mutating-webhook-tutorial .
docker build --no-cache -t morvencao/sidecar-injector:v1 .
rm -rf kube-mutating-webhook-tutorial
docker push morvencao/sidecar-injector:v1
以下面为依赖编写 Dockerfile 文件:
FROM alpine:latest
ADD kube-mutating-webhook-tutorial /kube-mutating-webhook-tutorial
ENTRYPOINT ["./kube-mutating-webhook-tutorial"]
在手动构建容器前,你需要 Docker ID 账号并将 image name 和 tag (Dockerfile 和 deployment.yaml 文件中)修改成你自己的,然后执行以下命令:
[root@mstnode kube-mutating-webhook-tutorial]# ./build
Sending build context to Docker daemon 44.89MB
Step 1/3 : FROM alpine:latest
---> 3fd9065eaf02
Step 2/3 : ADD kube-mutating-webhook-tutorial /kube-mutating-webhook-tutorial
---> 432de60c2b3f
Step 3/3 : ENTRYPOINT ["./kube-mutating-webhook-tutorial"]
---> Running in da6e956d1755
Removing intermediate container da6e956d1755
---> 619faa936145
Successfully built 619faa936145
Successfully tagged morvencao/sidecar-injector:v1
The push refers to repository [docker.io/morvencao/sidecar-injector]
efd05fe119bb: Pushed
cd7100a72410: Layer already exists
v1: digest: sha256:7a4889928ec5a8bcfb91b610dab812e5228d8dfbd2b540cd7a341c11f24729bf size: 739
编写 Sidecar 注入配置
现在我们来创建一个 Kubernetes ConfigMap
,包含需要注入到目标 pod 中的容器和 volume
信息:
apiVersion: v1
kind: ConfigMap
metadata:
name: sidecar-injector-webhook-configmap
data:
sidecarconfig.yaml: |
containers:
- name: sidecar-nginx
image: nginx:1.12.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx
volumes:
- name: nginx-conf
configMap:
name: nginx-configmap
从上面的清单中看,这里需要另一个包含 nginx conf
的 ConfigMap
。完整 yaml 参考 nginxconfigmap.yaml。
然后将这两个 ConfigMap 部署到集群中:
[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/nginxconfigmap.yaml
configmap "nginx-configmap" created
[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/configmap.yaml
configmap "sidecar-injector-webhook-configmap" created
创建包含秘钥对的 Secret
由于准入控制是一个高安全性操作,所以对外在的 webhook server
提供 TLS 是必须的。作为流程的一部分,我们需要创建由 Kubernetes CA 签名的 TLS 证书,以确保 webhook server
和 apiserver 之间通信的安全性。对于 CSR 创建和批准的完整步骤,请参考 这里 。
简单起见,我们参考了 Istio 的脚本并创建了一个类似的名为 webhook-create-signed-cert.sh
的脚本,来自动生成证书及秘钥对并将其加入到 secret
中。
#!/bin/bash
while [[ $# -gt 0 ]]; do
case ${1} in
--service)
service="$2"
shift
;;
--secret)
secret="$2"
shift
;;
--namespace)
namespace="$2"
shift
;;
esac
shift
done
[ -z ${service} ] && service=sidecar-injector-webhook-svc
[ -z ${secret} ] && secret=sidecar-injector-webhook-certs
[ -z ${namespace} ] && namespace=default
csrName=${service}.${namespace}
tmpdir=$(mktemp -d)
echo "creating certs in tmpdir ${tmpdir} "
cat <<EOF >> ${tmpdir}/csr.conf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${service}
DNS.2 = ${service}.${namespace}
DNS.3 = ${service}.${namespace}.svc
EOF
openssl genrsa -out ${tmpdir}/server-key.pem 2048
openssl req -new -key ${tmpdir}/server-key.pem -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf
# clean-up any previously created CSR for our service. Ignore errors if not present.
kubectl delete csr ${csrName} 2>/dev/null || true
# create server cert/key CSR and send to k8s API
cat <<EOF | kubectl create -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
name: ${csrName}
spec:
groups:
- system:authenticated
request: $(cat ${tmpdir}/server.csr | base64 | tr -d '\n')
usages:
- digital signature
- key encipherment
- server auth
EOF
# verify CSR has been created
while true; do
kubectl get csr ${csrName}
if [ "$?" -eq 0 ]; then
break
fi
done
# approve and fetch the signed certificate
kubectl certificate approve ${csrName}
# verify certificate has been signed
for x in $(seq 10); do
serverCert=$(kubectl get csr ${csrName} -o jsonpath='{.status.certificate}')
if [[ ${serverCert} != '' ]]; then
break
fi
sleep 1
done
if [[ ${serverCert} == '' ]]; then
echo "ERROR: After approving csr ${csrName}, the signed certificate did not appear on the resource. Giving up after 10 attempts." >&2
exit 1
fi
echo ${serverCert} | openssl base64 -d -A -out ${tmpdir}/server-cert.pem
# create the secret with CA cert and server cert/key
kubectl create secret generic ${secret} \
--from-file=key.pem=${tmpdir}/server-key.pem \
--from-file=cert.pem=${tmpdir}/server-cert.pem \
--dry-run -o yaml |
kubectl -n ${namespace} apply -f -
运行脚本后,包含证书和秘钥对的 secret
就被创建出来了:
[root@mstnode kube-mutating-webhook-tutorial]# ./deployment/webhook-create-signed-cert.sh
creating certs in tmpdir /tmp/tmp.wXZywp0wAF
Generating RSA private key, 2048 bit long modulus
...........................................+++
..........+++
e is 65537 (0x10001)
certificatesigningrequest "sidecar-injector-webhook-svc.default" created
NAME AGE REQUESTOR CONDITION
sidecar-injector-webhook-svc.default 0s https://mycluster.icp:9443/oidc/endpoint/OP#admin Pending
certificatesigningrequest "sidecar-injector-webhook-svc.default" approved
secret "sidecar-injector-webhook-certs" created
创建 Sidecar 注入器的 Deployment 和 Service
deployment 带有一个 pod,其中运行的就是 sidecar-injector
容器。该容器以特殊参数运行:
sidecarCfgFile
指的是 sidecar 注入器的配置文件,挂载自上面创建的 ConfigMapsidecar-injector-webhook-configmap
。tlsCertFile
和tlsKeyFile
是秘钥对,挂载自 Secretinjector-webhook-certs
。alsologtostderr
、v=4
和2>&1
是日志参数。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sidecar-injector-webhook-deployment
labels:
app: sidecar-injector
spec:
replicas: 1
template:
metadata:
labels:
app: sidecar-injector
spec:
containers:
- name: sidecar-injector
image: morvencao/sidecar-injector:v1
imagePullPolicy: IfNotPresent
args:
- -sidecarCfgFile=/etc/webhook/config/sidecarconfig.yaml
- -tlsCertFile=/etc/webhook/certs/cert.pem
- -tlsKeyFile=/etc/webhook/certs/key.pem
- -alsologtostderr
- -v=4
- 2>&1
volumeMounts:
- name: webhook-certs
mountPath: /etc/webhook/certs
readOnly: true
- name: webhook-config
mountPath: /etc/webhook/config
volumes:
- name: webhook-certs
secret:
secretName: sidecar-injector-webhook-certs
- name: webhook-config
configMap:
name: sidecar-injector-webhook-configmap
Service 暴露带有 app=sidecar-injector
label 的 pod,使之在集群中可访问。这个 Service 会被 MutatingWebhookConfiguration
中定义的 clientConfig
部分访问,默认的端口 spec.ports.port
需要设置为 443。
apiVersion: v1
kind: Service
metadata:
name: sidecar-injector-webhook-svc
labels:
app: sidecar-injector
spec:
ports:
- port: 443
targetPort: 443
selector:
app: sidecar-injector
然后将上述 Deployment 和 Service 部署到集群中,并且验证 sidecar 注入器的 webhook server
是否 running:
[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/deployment.yaml
deployment "sidecar-injector-webhook-deployment" created
[root@mstnode kube-mutating-webhook-tutorial]# kubectl create -f ./deployment/service.yaml
service "sidecar-injector-webhook-svc" created
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
sidecar-injector-webhook-deployment 1 1 1 1 2m
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pod
NAME READY STATUS RESTARTS AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj 1/1 Running 0 3m
动态配置 webhook 准入控制器
MutatingWebhookConfiguration
中具体说明了哪个 webhook admission server
是被使用的并且哪些资源受准入服务器的控制。建议你在创建 MutatingWebhookConfiguration
之前先部署 webhook admission server
,并确保其正常工作。否则,请求会被无条件接收或根据失败规则被拒。
现在,我们根据下面的内容创建 MutatingWebhookConfiguration
:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: sidecar-injector-webhook-cfg
labels:
app: sidecar-injector
webhooks:
- name: sidecar-injector.morven.me
clientConfig:
service:
name: sidecar-injector-webhook-svc
namespace: default
path: "/mutate"
caBundle: ${CA_BUNDLE}
rules:
- operations: [ "CREATE" ]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchLabels:
sidecar-injector: enabled
第 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
。我们写个脚本来自动匹配:
#!/bin/bash
set -o errexit
set -o nounset
set -o pipefail
ROOT=$(cd $(dirname $0)/../../; pwd)
export CA_BUNDLE=$(kubectl get configmap -n kube-system extension-apiserver-authentication -o=jsonpath='{.data.client-ca-file}' | base64 | tr -d '\n')
if command -v envsubst >/dev/null 2>&1; then
envsubst
else
sed -e "s|\${CA_BUNDLE}|${CA_BUNDLE}|g"
fi
然后执行:
[root@mstnode kube-mutating-webhook-tutorial]# cat ./deployment/mutatingwebhook.yaml |\
> ./deployment/webhook-patch-ca-bundle.sh >\
> ./deployment/mutatingwebhook-ca-bundle.yaml
我们看不到任何日志描述 webhook server
接收到准入请求,似乎该请求并没有发送到 webhook server
一样。所以有一种可能性是这是被 MutatingWebhookConfiguration
中的配置触发的。再确认一下 MutatingWebhookConfiguration
我们会发现下面的内容:
namespaceSelector:
matchLabels:
sidecar-injector: enabled
通过 namespaceSelector 控制 sidecar 注入器
我们在 MutatingWebhookConfiguration
中配置了 namespaceSelector
,也就意味着只有在满足条件的 namespace 下的资源能够被发送到 webhook server
。于是我们将 default 这个 namespace 打上标签 sidecar-injector=enabled
:
[root@mstnode kube-mutating-webhook-tutorial]# kubectl label namespace default sidecar-injector=enabled
namespace "default" labeled
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get namespace -L sidecar-injector
NAME STATUS AGE sidecar-injector
default Active 1d enabled
kube-public Active 1d
kube-system Active 1d
现在我们配置的 MutatingWebhookConfiguration
会在 pod 创建的时候就注入 sidecar 容器。将运行中的 pod 删除并确认是否创建了新的带 sidecar 容器的 pod:
[root@mstnode kube-mutating-webhook-tutorial]# kubectl delete pod sleep-6d79d8dc54-r66vz
pod "sleep-6d79d8dc54-r66vz" deleted
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pods
NAME READY STATUS RESTARTS AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj 1/1 Running 0 29m
sleep-6d79d8dc54-b8ztx 0/2 ContainerCreating 0 3s
sleep-6d79d8dc54-r66vz 1/1 Terminating 0 11m
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pod sleep-6d79d8dc54-b8ztx -o yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
kubernetes.io/psp: default
sidecar-injector-webhook.morven.me/inject: "true"
sidecar-injector-webhook.morven.me/status: injected
labels:
app: sleep
pod-template-hash: "2835848710"
name: sleep-6d79d8dc54-b8ztx
namespace: default
spec:
containers:
- command:
- /bin/sleep
- infinity
image: tutum/curl
imagePullPolicy: IfNotPresent
name: sleep
resources: {}
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-d7t2r
readOnly: true
- image: nginx:1.12.2
imagePullPolicy: IfNotPresent
name: sidecar-nginx
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /etc/nginx
name: nginx-conf
volumes:
- name: default-token-d7t2r
secret:
defaultMode: 420
secretName: default-token-d7t2r
- configMap:
defaultMode: 420
name: nginx-configmap
name: nginx-conf
...
可以看到,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"
:
[root@mstnode kube-mutating-webhook-tutorial]# kubectl delete deployment sleep
deployment "sleep" deleted
[root@mstnode kube-mutating-webhook-tutorial]# cat <<EOF | kubectl create -f -
apiVersion: extensions/v1beta1
> kind: Deployment
> metadata:
> name: sleep
> spec:
> replicas: 1
> template:
> metadata:
> labels:
> app: sleep
> spec:
> containers:
> - name: sleep
> image: tutum/curl
> command: ["/bin/sleep","infinity"]
> imagePullPolicy: IfNotPresent
> EOF
deployment "sleep" created
然后确认 sidecar 注入器是否跳过了这个 pod:
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
sidecar-injector-webhook-deployment 1 1 1 1 45m
sleep 1 1 1 1 17s
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pod
NAME READY STATUS RESTARTS AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj 1/1 Running 0 45m
sleep-776b7bcdcd-4bz58 1/1 Running 0 21s
结果显示,这个 sleep 应用只包含一个容器,没有额外的容器和 volume 注入。然后我们将这个 deployment 增加注解,并确认其重建后是否被注入 sidecar:
[root@mstnode kube-mutating-webhook-tutorial]# kubectl patch deployment sleep -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar-injector-webhook.morven.me/inject": "true"}}}}}'
deployment "sleep" patched
[root@mstnode kube-mutating-webhook-tutorial]# kubectl delete pod sleep-776b7bcdcd-4bz58
pod "sleep-776b7bcdcd-4bz58" deleted
[root@mstnode kube-mutating-webhook-tutorial]# kubectl get pods
NAME READY STATUS RESTARTS AGE
sidecar-injector-webhook-deployment-bbb689d69-fdbgj 1/1 Running 0 49m
sleep-3e42ff9e6c-6f87b 0/2 ContainerCreating 0 18s
sleep-776b7bcdcd-4bz58 1/1 Terminating 0 3m
与预期一致,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