fdfv232 发表于 2025-2-7 00:00:07

基于 Admission Webhook 实现 Pod DNSConfig 自动注入


本文主要分享如何使用 基于 Admission Webhook 实现自动修改 Pod DNSConfig,使其优先使用 NodeLocalDNS 。
1.背景

上一篇部署好 NodeLocal DNSCache,但是还差了很重要的一步,配置 pod 使用 NodeLocal DNSCache 作为优先的 DNS 服务器。
有以下几种方式:

[*]方式一:修改 kubelet 中的 dns nameserver 参数,并重启节点 kubelet。存在业务中断风险,不推荐使用此方式。

[*]测试时可以用这个方式,比较简单

[*]方式二:创建 Pod 时手动指定 DNSConfig,比较麻烦,不推荐。
[*]方式三:借助 DNSConfig 动态注入控制器在 Pod 创建时配置 DNSConfig 自动注入,推荐使用此方式。

[*]需要自己实现一个 webhook,相当于把方式二自动化了

第一种方式存在业务中断风险,而且后续新增节点时也需要修改 kubelet 配置,比较麻烦。
而第二种方式则每个创建的 Pod 都需要手动指定 DNSConfig 就更繁琐了。
因此一般是推荐使用第三种方式,实现一个 Webhook,由该 Webhook 来自动修改 Pod 的 DNSConfig。
2. 自动注入规则

Admission Webhook 用于自动注入 DNSConfig 到新建的 Pod 中,避免您手工配置 Pod YAML进行注入。
注入范围

为了使应用更灵活,我们指定,只对携带node-local-dns-injection=enabledlabel 的命名空间中新建 Pod 的进行注入。
可以通过以下命令给命名空间打上Label标签:
kubectl label namespace <namespace-name> node-local-dns-injection=enabled注入规则

Webhook 则是在所有 Pod 创建、更新前都会进行检测,如果 Pod 所在 Namespace 满足条件,或者 Pod 也满足条件则自动注入 DNSConfig,将 NodeLocalDNS 作为 Pod 的优先 DNS 服务。
具体规则如下:
Pod 在同时满足以下条件时,才会自动注入 DNS 缓存。如果您的 Pod 容器未注入 DNS 缓存服务器的 IP 地址,请检查 Pod 是否未满足以下条件。

[*]1)新建 Pod 不位于 kube-system 和 kube-public 命名空间。
[*]2)新建 Pod 所在命名空间的 Labels 标签包含 node-local-dns-injection=enabled。
[*]3)新建 Pod 没有被打上禁用 DNS 注入 node-local-dns-injection=disabled 标签。
[*]4)新建 Pod 的网络为 hostNetwork 且 DNSPolicy 为 ClusterFirstWithHostNet,或 Pod 为非 hostNetwork 且 DNSPolicy 为 ClusterFirst。
3. Admission Webhook 实现

源码:lixd/nodelocaldns-admission-webhook
配置文件

我们可以通过配置文件来执行 KubeDNS 地址和 NodeLocalDNS 地址,也提供了默认值。
const (        DefaultKubeDNS= "10.96.0.10"        DefaultLocalDNS = "169.254.20.10")func NewDNSConfig(kubedns, localdns string) Config {        if kubedns == "" {                kubedns = DefaultKubeDNS        }        if localdns == "" {                localdns = DefaultLocalDNS        }        return Config{                KubeDNS:kubedns,                LocalDNS: localdns,        }}启动服务时可以指定
        flag.StringVar(&kubedns, "kube-dns", "10.96.0.10", "The service ip of kube dns.")        flag.StringVar(&localdns, "local-dns", "169.254.20.10", "The virtual ip of node local dns.")注入 DNSConfig

Webhook Handle 方法中就是核心逻辑。
func (a *PodAnnotator) Handle(ctx context.Context, req admission.Request) admission.Response {        pod := &corev1.Pod{}        err := a.Decoder.Decode(req, pod)        if err != nil {                return admission.Errored(http.StatusBadRequest, err)        }        klog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v (%v) UID=%v patchOperation=%v UserInfo=%v",                req.Kind, req.Namespace, req.Name, pod.Name, req.UID, req.Operation, req.UserInfo)        // determine whether to perform mutation        if !a.NeedMutation(pod) {                klog.Infof("Skipping mutation for %s/%s due to policy check", pod.Namespace, pod.Name)                return admission.Allowed("not need mutation,skip")        }        // mutate the fields in pod        mutation(pod, a.Config)        marshaledPod, err := json.Marshal(pod)        if err != nil {                return admission.Errored(http.StatusInternalServerError, err)        }        return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)}首先通过 NeedMutation 判断是否满足条件,如果不需要注入则跳过
如果需要则执行 mutation 方法修改 Pod 的 DNSConfig 字段。
NeedMutation

这里就是按照之前提到的注入规则进行判定
// NeedMutation Check whether the target resoured need to be mutatedfunc (a *PodAnnotator) NeedMutation(pod *corev1.Pod) bool {        if pod.Namespace == "" {                pod.Namespace = "default"        }        /*           Pod will automatically inject DNS cache when all of the following conditions are met:           1. The newly created Pod is not in the kube-system and kube-public namespaces.           2. The Labels of the namespace where the new Pod is located contain node-local-dns-injection=enabled.           3. The newly created Pod is not labeled with the disabled DNS injection node-local-dns-injection=disabled label.           4. The network of the newly created Pod is hostNetwork and DNSPolicy is ClusterFirstWithHostNet, or the Pod is non-hostNetwork and DNSPolicy is ClusterFirst.        */        //1. The newly created Pod is not in the kube-system and kube-public namespaces.        for _, namespace := range ignoredNamespaces {                if pod.Namespace == namespace {                        klog.V(1).Infof("Skip mutation for %v for it's in special namespace: %v", pod.Name, pod.Namespace)                        return false                }        }        // Fetch the namespace where the Pod is located.        var ns corev1.Namespace        err := a.Client.Get(context.Background(), client.ObjectKey{Name: pod.GetNamespace()}, &ns)        if err != nil {                klog.V(1).ErrorS(err, "Failed to fetch namespace: %v", pod.Namespace)                return false        }        //2. The Labels of the namespace where the new Pod is located contain node-local-dns-injection=enabled.        if v, ok := ns.Labels; !ok || v != "enabled" {                return false        }        //3. The newly created Pod is not labeled with the disabled DNS injection node-local-dns-injection=disabled label.        if v, ok := pod.Labels; ok && v == "disabled" {                return false        }        //4. The network of the newly created Pod is hostNetwork and DNSPolicy is ClusterFirstWithHostNet, or the Pod is non-hostNetwork and DNSPolicy is ClusterFirst.        // The network of the Pod is hostNetwork, so DNSPolicy should be ClusterFirstWithHostNet.        if pod.Spec.HostNetwork && pod.Spec.DNSPolicy != corev1.DNSClusterFirstWithHostNet {                return false        }        // The network of the Pod is not hostNetwork, so DNSPolicy should be ClusterFirst.        if !pod.Spec.HostNetwork && pod.Spec.DNSPolicy != corev1.DNSClusterFirst {                return false        }        // If all conditions are met, return true.        return true}mutation

mutation 则是根据配置文件组装好 DNSConfig 并注入到 Pod。
func mutation(pod *corev1.Pod, conf Config) {        ns := pod.Namespace        if ns == "" {                ns = "default"        }        pod.Spec.DNSPolicy, pod.Spec.DNSConfig = loadCustomDnsConfig(ns, conf)}func loadCustomDnsConfig(namespace string, config Config) (corev1.DNSPolicy, *corev1.PodDNSConfig) {        nsSvc := fmt.Sprintf("%s.svc.cluster.local", namespace)        return "None", &corev1.PodDNSConfig{                Nameservers: []string{config.LocalDNS, config.KubeDNS},                Searches:    []string{nsSvc, "svc.cluster.local", "cluster.local"},                Options: []corev1.PodDNSConfigOption{                        {                                Name:"ndots",                                Value: StringPtr("3"),                        },                        {                                Name:"attempts",                                Value: StringPtr("2"),                        },                        {                                Name:"timeout",                                Value: StringPtr("1"),                        },                },        }}至此,核心逻辑就结束了,还是比较简单的,对于每个 Pod 创建、更新请求,Webhook 中都判断该 Pod 是否需要注入,不满足条件则直接跳过,满足条件则根据配置生成 DNSConfig 并注入到 Pod 中。
4. 部署

包含两部分:

[*]1)Webhook 本身部署
[*]2)K8s 中增加 Webhook 配置
Webhook 部署

需要部署以下几部分内容:

[*]Cert-manager : 由于 Webhook 需要配置证书,建议使用 cert-manager 来自动注入,减少手动操作。
[*]RBAC:Webhook 需要查询 Pod、Namespace 等信息,因此需要授权
[*]Deploy:Webhook 本身以 Deploy 方式部署。
具体文件都在 /deploy 目录下,直接使用即可。
在 deploy 目录提供了部署相关 yaml,apply 即可。

[*]1)部署 cert-manager 用于管理证书
[*]2)创建 Issuer、Certificate 对象,让 cert-manager 签发证书并存放到 Secret
[*]3)创建 rbac 并部署 Webhook, 挂载 2 中的 Secret 到容器中以开启 TLS

[*]可以修改启动命令中的 -kube-dns 和 -local-dns 参数来调整 KubeDNS 和 NodeLocalDNS 地址,默认为 10.96.0.10 和 169.254.20.10。

webhook-deploy.yaml 如下,就是一个普通的 Deployment:
镜像已经推送到了 Dockerhub,大家可以直接使用
apiVersion: apps/v1kind: Deploymentmetadata:name: nodelocaldns-webhooknamespace: kube-systemlabels:    app: nodelocaldnsspec:replicas: 1selector:    matchLabels:      app: nodelocaldnstemplate:    metadata:      labels:      app: nodelocaldns    spec:      serviceAccountName: nodelocaldns-webhook # 提供查询 namespace 信息的权限      containers:      - name: nodelocaldns-webhook          image: lixd96/nodelocaldns-admission-webhook:v0.0.1          imagePullPolicy: IfNotPresent          command:            - /manager          args:            - "-kube-dns=10.96.0.10"            - "-local-dns=169.254.20.10"          volumeMounts:            - name: webhook-certs            mountPath: /tmp/k8s-webhook-server/serving-certs # Webhook 证书默认路径            readOnly: true      volumes:      - name: webhook-certs          secret:            secretName: nodelocaldns-webhook---apiVersion: v1kind: Servicemetadata:name: nodelocaldns-webhooknamespace: kube-systemlabels:    app: nodelocaldnsspec:ports:    - port: 443      targetPort: 9443selector:    app: nodelocaldns部署命令如下:
cd deploy# 部署 CertManager 以及签发证书kubectl apply -f cert-manager# 部署 Webhookkubectl apply -f webhook-deploy.yamlkubectl apply -f webhook-rbac.yamlMutatingWebhookConfiguration

yaml 大概是这样的:
---apiVersion: admissionregistration.k8s.io/v1kind: MutatingWebhookConfigurationmetadata:name: mutating-webhook-configurationannotations:    cert-manager.io/inject-ca-from: kube-system/nodelocaldns-webhookwebhooks:- admissionReviewVersions:      - v1    clientConfig:      #caBundle: ""      service:      name: nodelocaldns-webhook      namespace: kube-system      path: /mutate-v1-pod    failurePolicy: Fail    name: nodelocaldns-webhook.kube-system.svc    namespaceSelector: # 限制生效范围      matchLabels:      node-local-dns-injection: enabled    rules:      - apiGroups:          - ""      apiVersions:          - v1      operations:          - CREATE          - UPDATE      resources:          - pods    sideEffects: None增加 cert-manager.io/inject-ca-from annotation 让 CertManager 自动注入 CA 证书。
annotations:    cert-manager.io/inject-ca-from: kube-system/nodelocaldns-webhook限制生效范围
    namespaceSelector: # 限制生效范围      matchLabels:      node-local-dns-injection: enabled只关心 Pod 的 Create、Update 事件:
    rules:      - apiGroups:          - ""      apiVersions:          - v1      operations:          - CREATE          - UPDATE      resources:          - pods也是直接 apply 即可
cd deploy kubectl apply -f webhook-config.yaml5. 测试

首先给 default namespace 打上 node-local-dns-injection=enabledlabel。
kubectl label namespace default node-local-dns-injection=enabled创建一个 Pod,然后查看 yaml 看看 dnsConfig 是否被修改了。
kubectl run busybox --image=busybox --restart=Never --namespace=default --command -- sleep infinity查看一下完整 Yaml
# k get po busybox -oyamlapiVersion: v1kind: Podmetadata:annotations:    cni.projectcalico.org/containerID: 2a4caca308b031f872c47ef334cf7e940d74646a2f0a8893c7786508d30ed488    cni.projectcalico.org/podIP: 172.25.233.215/32    cni.projectcalico.org/podIPs: 172.25.233.215/32creationTimestamp: "2024-02-05T10:43:16Z"labels:    run: nginx-podname: nginx-podnamespace: defaultresourceVersion: "19341"uid: 2b107b50-e85c-462f-8919-a0c01114bae6spec:containers:- image: nginx    imagePullPolicy: Always    name: nginx-pod    resources: {}    terminationMessagePath: /dev/termination-log    terminationMessagePolicy: File    volumeMounts:    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount      name: kube-api-access-4wf2n      readOnly: truednsConfig:    nameservers:    - 169.254.20.10    options:    - name: ndots      value: "2"    searches:    - default.svc.cluster.local    - svc.cluster.local    - cluster.localdnsPolicy: NoneDns 部分如下:
dnsConfig:    nameservers:    - 169.254.20.10    options:    - name: ndots      value: "2"    searches:    - default.svc.cluster.local    - svc.cluster.local    - cluster.localdnsPolicy: None可以看到,已经注入了我们的 NodeLocalDNS 了。
然后往没有打 Label 的命名空间创建 Pod
kubectl create namespace mynskubectl run busybox --image=busybox --restart=Never --namespace=myns --command -- sleep infinity查看 DNSConfig 是否被修改
# kubectl -n myns get pod nginx-pod -oyamlapiVersion: v1kind: Podmetadata:annotations:    cni.projectcalico.org/containerID: 93a545988d7c7bbb88f0bc0e745226cd9e684bd63b78754dadd738861ed34512    cni.projectcalico.org/podIP: 172.25.233.218/32    cni.projectcalico.org/podIPs: 172.25.233.218/32creationTimestamp: "2024-02-06T01:22:36Z"labels:    run: nginx-podname: nginx-podnamespace: mynsresourceVersion: "116195"uid: 1f64a831-7470-49d5-b28e-1cd231ef5d8fspec:containers:- image: nginx    imagePullPolicy: Always    name: nginx-pod    resources: {}    terminationMessagePath: /dev/termination-log    terminationMessagePolicy: File    volumeMounts:    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount      name: kube-api-access-shk68      readOnly: truednsPolicy: ClusterFirstenableServiceLinks: true可以看到,并没有,说明我们的逻辑是没问题的,只会对打了 Label 的命名空间中的 Pod 进行注入。
最后测试能否正常解析
测试一下注入 DNSConfig 之后能否正常解析 DNS
kubectl run busybox-pod --image=busybox --restart=Never --namespace=default进入 Pod 并测试解析 Service 记录
# k exec -it busybox-pod -- nslookup nodelocaldns-webhook.kube-system.svc.cluster.localServer:                169.254.20.10Address:      169.254.20.10:53Name:      nodelocaldns-webhook.kube-system.svc.cluster.localAddress: 10.105.137.213# kk get svc nodelocaldns-webhookNAME                   TYPE      CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGEnodelocaldns-webhook   ClusterIP   10.105.137.213   <none>      443/TCP   14h可以看到,Nameserver 是 169.254.20.10,也就是我们的 NodeLocalDNS,然后能拿到正确的 IP,说明我们的 NodeLocalDNS 是没问题的。
6. 小结

本文主要分析了如何通过自定义一个 Admission Webhook 来自动化的修改 Pod 的 DNSConfig,使其优先使用 NodeLocalDNS。
页: [1]
查看完整版本: 基于 Admission Webhook 实现 Pod DNSConfig 自动注入