点击查看目录
本文译自 Building a Kubernetes Mutating Admission Webhook。
当你在 Kubernetes 中创建 Pod 的时候,是否注意到在容器的 /var/run/secrets/kubernetes.io/serviceaccount/token 路径上存放了一个用于认证的 token 文件?你可以通过如下命令,在 Kubernetes 集群中验证下:
$ kubectl run busybox --image=busybox --restart=Never -it --rm -- ls -l /var/run/secrets/kubernetes.io/serviceaccount/token
# output
/var/run/secrets/kubernetes.io/serviceaccount/token
注:在 Kubernetes 1.6 以上的版本,你可以 取消 这一个自动注入的功能。
现在假设有这样一个场景,需要添加一个“hello.txt”文件到所有(或某组)pod 的容器文件系统内,不能通过在 pod spec 中显式指定 volumeMount 的方式,我们有没有什么方法达到目的呢?
为了让实验更具趣味性,我们用一个 ASCII“小作品”(用这个工具生成的)来作为我们的“hello.txt”文件:
何为 Admission Webhook
实现该注入的功能的方式之一,就是我们上文中提到的,使用 Kubernetes Admission Webhooks。这是何方神圣?来看下官方文档给的定义:
Admission webhook 是一种用于接收准入请求并对其进行处理的 HTTP 回调机制。可以定义两种类型的 admission webhook,即 validating admission webhook 和 mutating admission webhook。Mutating admission webhook 会先被调用。它们可以更改发送到 API 服务器的对象以执行自定义的设置默认值操作。
下文这张借用自 Kubernetes.io blog post 的流程图可以帮助我们理解 Admission Webhook 的概念。
接下来,本文将采用 Kubernetes 提供的 Mutating Admission Webhook 这一机制,来实现注入“hello.txt”文件到 Pod 容器中,我们每次发送请求调用 API 创建 Pod 的时候,Pod 的 spec 信息会被先修改,再存储。如此一来,工作节点上的 Kublet 创建 Pod 的时候,将会预置“hello.txt”文件。文件的创建流程是全自动的。一起来试试!
创建 Admission Webhook
笔者已经把下文提到的代码和命令都上传到了 github 仓库。读者可以跟着边看边操作。
首先需要有一个正常运行的 Kubernetes 集群。读者可以通过 Kind 快速起一个集群。
接着,定义一个包含了“hello.txt”文件内容的 ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: hello-configmap
data:
hello.txt: "\n /$$$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$
\ /$$ /$$$$$$ /$$$$$$ \n|__ $$__/| $$ | $$|_ $$_/ /$$__ $$ |_ $$_/
/$$__ $$ | $$ /$$/ /$$__ $$ /$$__ $$\n | $$ | $$ | $$ | $$ | $$
\ \\__/ | $$ | $$ \\__/ | $$ /$$/ | $$ \\ $$| $$ \\__/\n | $$
\ | $$$$$$$$ | $$ | $$$$$$ | $$ | $$$$$$ | $$$$$/ | $$$$$$/|
\ $$$$$$ \n | $$ | $$__ $$ | $$ \\____ $$ | $$ \\____ $$ |
$$ $$ >$$__ $$ \\____ $$\n | $$ | $$ | $$ | $$ /$$ \\ $$ |
$$ /$$ \\ $$ | $$\\ $$ | $$ \\ $$ /$$ \\ $$\n | $$ | $$ | $$ /$$$$$$|
\ $$$$$$/ /$$$$$$| $$$$$$/ | $$ \\ $$| $$$$$$/| $$$$$$/\n |__/
\ |__/ |__/|______/ \\______/ |______/ \\______/ |__/ \\__/ \\______/
\ \\______/ \n \n
\ \n
\ \n"
为了构建 webhook,我们写一个简洁的 Go API 服务端。http handler 是实现 webhook 代码的最重要部分:
func (app *App) HandleMutate(w http.ResponseWriter, r *http.Request) {
admissionReview := &admissionv1.AdmissionReview{}
// read the AdmissionReview from the request json body
err := readJSON(r, admissionReview)
if err != nil {
app.HandleError(w, r, err)
return
}
// unmarshal the pod from the AdmissionRequest
pod := &corev1.Pod{}
if err := json.Unmarshal(admissionReview.Request.Object.Raw, pod); err != nil {
app.HandleError(w, r, fmt.Errorf("unmarshal to pod: %v", err))
return
}
// add the volume to the pod
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
Name: "hello-volume",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "hello-configmap",
},
},
},
})
// add volume mount to all containers in the pod
for i := 0; i < len(pod.Spec.Containers); i++ {
pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, corev1.VolumeMount{
Name: "hello-volume",
MountPath: "/etc/config",
})
}
containersBytes, err := json.Marshal(&pod.Spec.Containers)
if err != nil {
app.HandleError(w, r, fmt.Errorf("marshall containers: %v", err))
return
}
volumesBytes, err := json.Marshal(&pod.Spec.Volumes)
if err != nil {
app.HandleError(w, r, fmt.Errorf("marshall volumes: %v", err))
return
}
// build json patch
patch := []JSONPatchEntry{
JSONPatchEntry{
OP: "add",
Path: "/metadata/labels/hello-added",
Value: []byte(`"OK"`),
},
JSONPatchEntry{
OP: "replace",
Path: "/spec/containers",
Value: containersBytes,
},
JSONPatchEntry{
OP: "replace",
Path: "/spec/volumes",
Value: volumesBytes,
},
}
patchBytes, err := json.Marshal(&patch)
if err != nil {
app.HandleError(w, r, fmt.Errorf("marshall jsonpatch: %v", err))
return
}
patchType := admissionv1.PatchTypeJSONPatch
// build admission response
admissionResponse := &admissionv1.AdmissionResponse{
UID: admissionReview.Request.UID,
Allowed: true,
Patch: patchBytes,
PatchType: &patchType,
}
respAdmissionReview := &admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: admissionResponse,
}
jsonOk(w, &respAdmissionReview)
}
上面这部分代码,和 Kubernetes 内部代码有诸多类似,都使用了 源自 https://github.com/kubernetes/api 和 https://github.com/kubernetes/apimachinery 的 schema 类型。上述代码主要做了如下事情:
- 将来自 Http 请求中的 AdmissionReview json 输入反序列化。
- 读取 Pod 的 spec 信息。
- 将 hello-configmap 作为数据源,添加 hello-volume 卷到 Pod。
- 挂载卷至 Pod 容器中。
- 以 JSON PATCH 的形式记录变更信息,包括卷的变更,卷挂载信息的变更。顺道为容器添加一个“hello-added=true”的标签。
- 构建 json 格式的响应结果,结果中包含了这次请求中的被修改的部分。
笔者 此处 还为这个 handler 编写了单元/功能测试,以确保它的功能实现符合我们的预期。
加点改进:TLS
Webhook API 服务器需要通过 TLS 方式通信。如果想将其部署至 Kubernetes 集群内,我们还需要证书。笔者通过 New Relic 这个软件来生成 Webhook 证书。笔者创建了一个 个人分支,对代码做了点改动,以确保其可以 Job 方式部署:
apiVersion: batch/v1
kind: Job
metadata:
name: webhook-cert-setup
spec:
template:
spec:
serviceAccountName: webhook-cert-sa
containers:
- name: webhook-cert-setup
# This is a minimal kubectl image based on Alpine Linux that signs certificates using the k8s extension api server
image: quay.io/didil/k8s-webhook-cert-manager:0.13.19-1-a
command: ["./generate_certificate.sh"]
args:
- "--service"
- "hello-webhook-service"
- "--webhook"
- "hello-webhook.leclouddev.com"
- "--secret"
- "hello-tls-secret"
- "--namespace"
- "default"
restartPolicy: OnFailure
backoffLimit: 3
其他 YAML
完成 webhook API 服务端的镜像构建后,将其推送至镜像仓库,并将其作为一个 Deployment 部署到集群。
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-webhook-deployment
labels:
app: hello-webhook
spec:
replicas: 1
selector:
matchLabels:
app: hello-webhook
template:
metadata:
labels:
app: hello-webhook
spec:
containers:
- name: hello-webhook
image: CONTAINER_IMAGE
ports:
- containerPort: 8000
volumeMounts:
- name: hello-tls-secret
mountPath: "/tls"
readOnly: true
resources:
limits:
memory: "128Mi"
cpu: "500m"
volumes:
- name: hello-tls-secret
secret:
secretName: hello-tls-secret
然后是一个 ClusterIP 类型的 Service:
apiVersion: v1
kind: Service
metadata:
name: hello-webhook-service
spec:
type: ClusterIP
selector:
app: hello-webhook
ports:
- protocol: TCP
port: 443
targetPort: 8000
接着,创建一个 MutatingWebhookConfiguration 将我们创建的 webhook 信息注册到 Kubernetes API server:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: "hello-webhook.leclouddev.com"
webhooks:
- name: "hello-webhook.leclouddev.com"
objectSelector:
matchLabels:
hello: "true"
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
scope: "Namespaced"
clientConfig:
service:
namespace: "default"
name: "hello-webhook-service"
path: /mutate
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
timeoutSeconds: 10
如上述的清单信息所示,我们要求 Kubernetes 把(部署了 MutatingWebhookConfiguration)命名空间中所有的 Pod 创建请求,只要匹配上“hello=true”标签的,就将其转发到 hello-webhook-service 的“/mutate”路径下,交给其处理。标签是可选的。此处笔者通过标签匹配来说明,如果标签不匹配或者不具备标签的请求,就可以绕开 Mutating Webhook 的预处理。
这篇文章 中提到了“caBundle”,然而我们上面文件中“clientConfig”中却不存在“caBundle”key 字段。不必感到奇怪,那是因为 webhook-cert-setup Job 会为我们自动创建这个 key。
部署 Webhook
项目差不多就绪可以部署了。我们用 Makefile 和 Kustomize 来部署。
$ make k8s-deploy
# output
kustomize build k8s/other | kubectl apply -f -
configmap/hello-configmap created
service/hello-webhook-service created
mutatingwebhookconfiguration.admissionregistration.k8s.io/hello-webhook.leclouddev.com created
kustomize build k8s/csr | kubectl apply -f -
serviceaccount/webhook-cert-sa created
clusterrole.rbac.authorization.k8s.io/webhook-cert-cluster-role created
clusterrolebinding.rbac.authorization.k8s.io/webhook-cert-cluster-role-binding created
job.batch/webhook-cert-setup created
Waiting for cert creation ...
kubectl certificate approve hello-webhook-service.default
certificatesigningrequest.certificates.k8s.io/hello-webhook-service.default approved
kustomize build k8s/csr | kubectl apply -f -
serviceaccount/webhook-cert-sa unchanged
clusterrole.rbac.authorization.k8s.io/webhook-cert-cluster-role unchanged
clusterrolebinding.rbac.authorization.k8s.io/webhook-cert-cluster-role-binding unchanged
job.batch/webhook-cert-setup unchanged
Waiting for cert creation ...
kubectl certificate approve hello-webhook-service.default
certificatesigningrequest.certificates.k8s.io/hello-webhook-service.default approved
(cd k8s/deployment && \
kustomize edit set image CONTAINER_IMAGE=quay.io/didil/hello-webhook:0.1.8)
kustomize build k8s/deployment | kubectl apply -f -
deployment.apps/hello-webhook-deployment created
运行一个带有“hello=true”标签的 busybox 容器,检查看我们的 mutating webhook 是否在正常运行。
$ kubectl run busybox-1 --image=busybox --restart=Never -l=app=busybox,hello=true -- sleep 3600
看看容器内的文件系统是否有 hello.txt:
$ kubectl exec busybox-1 -it -- sh -c "ls /etc/config/hello.txt"
# output
/etc/config/hello.txt
再检查下文件内容:
$ kubectl exec busybox-1 -it -- sh -c "cat /etc/config/hello.txt"
接下来再创建第二个容器,不带“hello=true”标签的:
$ kubectl run busybox-2 --image=busybox --restart=Never -l=app=busybox -- sleep 3600
# output
pod/busybox-2 created
$ kubectl exec busybox-2 -it -- sh -c "ls /etc/config/hello.txt"
# output
ls: /etc/config/hello.txt: No such file or directory
和我们预期的一致,第一次创建的 busybox 容器,匹配上了 webhook 的标签,注入了文件。第二次创建的 busybox 容器则没有。
再来检查下是否只有 buxybox-1 容器具备“hello-added”标签:
$ kubectl get pod -l=app=busybox -L=hello-added
# output
NAME READY STATUS RESTARTS AGE HELLO-ADDED
busybox-1 1/1 Running 0 3m7s OK
busybox-2 1/1 Running 0 53s
Mutating Webhook 生效了!
总结
我们尝试用 Mutating Admission Webhooks 对 Kubernetes 进行了初次拓展。文中没有提及 Validating Admission Webhooks,如果你需要对 OpenAPI schemas 之外的资源进行校验,你可以进一步深入了解。
希望本文对你有所帮助,如果你有任何问题和评论,可以联系我。下篇文章,我们将讨论另一种拓展 Kubernetes 的方式:实现一个 Kubernetes Operator。