使用 Admission Webhook 自动配置 Pod DNSConfig
在 Kubernetes 集群中部署 NodeLocal DNSCache 后,还需要配置 Pod 优先使用本地 DNS 缓存服务。本文介绍如何通过自定义 Admission Webhook 实现 Pod DNSConfig 的自动注入。
1.需求背景
部署完成 NodeLocal DNSCache 后,关键的一步是让 Pod 使用 NodeLocal DNSCache 作为首选 DNS 服务器。
实现方式主要有三种:
- 方案一:修改 kubelet 的 dns nameserver 参数并重启节点。此方式存在业务中断风险,仅建议在测试环境使用。
- 方案二:创建 Pod 时手动指定 DNSConfig。操作繁琐,不适合大规模使用。
- 方案三:通过 Webhook 在 Pod 创建时自动注入 DNSConfig。推荐的生产环境方案。
方案一需要修改节点配置,新增节点时也需要重复操作。方案二则需要对每个 Pod 单独配置,工作量巨大。因此生产环境通常采用方案三,通过自定义 Webhook 实现自动化注入。
2.注入规则设计
Webhook 会在 Pod 创建和更新时进行拦截,自动注入 DNSConfig 字段。
作用范围
为了提供更灵活的配置能力,Webhook 仅对特定命名空间下的 Pod 进行注入。通过在命名空间上添加 node-local-dns-injection=enabled 标签来启用自动注入功能。
执行以下命令启用命名空间的自动注入:
kubectl label namespace <namespace-name> node-local-dns-injection=enabled
判定条件
Webhook 根据以下规则判断是否需要对 Pod 进行 DNSConfig 注入:
只有同时满足以下全部条件时,才会进行自动注入:
- 1)Pod 不属于 kube-system 和 kube-public 命名空间。
- 2)Pod 所在命名空间包含标签 node-local-dns-injection=enabled。
- 3)Pod 未被标记为禁用注入(标签 node-local-dns-injection=disabled)。
- 4)Pod 网络模式与 DNS 策略匹配:hostNetwork 模式下 DNSPolicy 为 ClusterFirstWithHostNet,或非 hostNetwork 模式下 DNSPolicy 为 ClusterFirst。
3.核心实现
源码:lixd/nodelocaldns-admission-webhook
配置定义
首先定义配置结构体,用于指定集群 DNS 和本地 DNS 的地址:
type DNSConfig struct {
ClusterDNS string
LocalDNS string
}
func LoadConfig(clusterDNS, localDNS string) DNSConfig {
if len(clusterDNS) == 0 {
clusterDNS = "10.96.0.10"
}
if len(localDNS) == 0 {
localDNS = "169.254.20.10"
}
return DNSConfig{
ClusterDNS: clusterDNS,
LocalDNS: localDNS,
}
}
启动参数配置:
flag.StringVar(&clusterDNS, "cluster-dns", "10.96.0.10", "Kubernetes cluster DNS service IP")
flag.StringVar(&localDNS, "local-dns", "169.254.20.10", "NodeLocal DNS cache service IP")
处理入口
Webhook 的 Handle 方法是请求处理的核心:
func (p *PodMutator) Handle(ctx context.Context, req admission.Request) admission.Response {
pod := &corev1.Pod{}
if err := p.Decoder.Decode(req, pod); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
klog.InfoS("Processing pod mutation",
"namespace", req.Namespace,
"name", pod.Name,
"operation", req.Operation)
// 检查是否需要注入
if !p.shouldMutate(pod) {
klog.InfoS("Skipping pod - does not meet mutation criteria",
"namespace", pod.Namespace,
"name", pod.Name)
return admission.Allowed("mutation not required")
}
// 执行注入操作
p.applyMutation(pod, p.Config)
mutatedPod, err := json.Marshal(pod)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.PatchResponseFromRaw(req.Object.Raw, mutatedPod)
}
条件判断
shouldMutate 方法根据预设规则判断是否需要注入:
func (p *PodMutator) shouldMutate(pod *corev1.Pod) bool {
namespace := pod.Namespace
if len(namespace) == 0 {
namespace = "default"
}
// 规则1:跳过系统命名空间
for _, ns := range systemNamespaces {
if namespace == ns {
klog.V(1).InfoS("Skipping system namespace", "namespace", namespace)
return false
}
}
// 获取命名空间信息
var ns corev1.Namespace
if err := p.Client.Get(context.Background(), client.ObjectKey{Name: pod.GetNamespace()}, &ns); err != nil {
klog.ErrorS(err, "Failed to retrieve namespace", "namespace", pod.Namespace)
return false
}
// 规则2:验证命名空间标签
if val, exists := ns.Labels[injectionLabel]; !exists || val != "enabled" {
return false
}
// 规则3:检查 Pod 是否禁用注入
if val, exists := pod.Labels[injectionLabel]; exists && val == "disabled" {
return false
}
// 规则4:验证 DNS 策略与网络模式匹配
if pod.Spec.HostNetwork && pod.Spec.DNSPolicy != corev1.DNSClusterFirstWithHostNet {
return false
}
if !pod.Spec.HostNetwork && pod.Spec.DNSPolicy != corev1.DNSClusterFirst {
return false
}
return true
}
注入逻辑
applyMutation 方法负责修改 Pod 的 DNSConfig:
func (p *PodMutator) applyMutation(pod *corev1.Pod, cfg DNSConfig) {
ns := pod.Namespace
if len(ns) == 0 {
ns = "default"
}
pod.Spec.DNSPolicy, pod.Spec.DNSConfig = p.buildDNSConfig(ns, cfg)
}
func (p *PodMutator) buildDNSConfig(namespace string, cfg DNSConfig) (corev1.DNSPolicy, *corev1.PodDNSConfig) {
serviceName := fmt.Sprintf("%s.svc.cluster.local", namespace)
return "None", &corev1.PodDNSConfig{
Nameservers: []string{cfg.LocalDNS, cfg.ClusterDNS},
Searches: []string{serviceName, "svc.cluster.local", "cluster.local"},
Options: []corev1.PodDNSConfigOption{
{
Name: "ndots",
Value: strPtr("3"),
},
{
Name: "attempts",
Value: strPtr("2"),
},
{
Name: "timeout",
Value: strPtr("1"),
},
},
}
}
func strPtr(s string) *string {
return &s
}
整个实现逻辑清晰:Webhook 接收 Pod 创建/更新请求后,首先根据规则判断是否需要注入,满足条件则根据配置生成 DNSConfig 并返回补丁。
4.部署步骤
部署包含两部分内容:Webhook 服务和 Kubernetes 准入配置。
服务部署
需要部署以下组件:
- cert-manager:自动管理 Webhook 证书
- RBAC:授权 Webhook 访问 Pod 和 Namespace 资源
- Deployment:运行 Webhook 服务
项目 /deploy 目录下提供了完整的部署清单。
部署过程:
- 使用 cert-manager 签发 TLS 证书
- 配置 RBAC 权限
- 部署 Webhook Deployment 和 Service
默认配置参数可通过启动参数修改:
- -cluster-dns:集群 DNS 地址,默认为 10.96.0.10
- -local-dns:NodeLocal DNS 地址,默认为 169.254.20.10
部署清单 webhook-deploy.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodelocaldns-webhook
namespace: kube-system
labels:
app: nodelocaldns
spec:
replicas: 1
selector:
matchLabels:
app: nodelocaldns
template:
metadata:
labels:
app: nodelocaldns
spec:
serviceAccountName: nodelocaldns-webhook
containers:
- name: nodelocaldns-webhook
image: lixd96/nodelocaldns-admission-webhook:v0.0.1
imagePullPolicy: IfNotPresent
command:
- /manager
args:
- "-cluster-dns=10.96.0.10"
- "-local-dns=169.254.20.10"
volumeMounts:
- name: webhook-certs
mountPath: /tmp/k8s-webhook-server/serving-certs
readOnly: true
volumes:
- name: webhook-certs
secret:
secretName: nodelocaldns-webhook
---
apiVersion: v1
kind: Service
metadata:
name: nodelocaldns-webhook
namespace: kube-system
labels:
app: nodelocaldns
spec:
ports:
- port: 443
targetPort: 9443
selector:
app: nodelocaldns
执行部署:
cd deploy
kubectl apply -f cert-manager
kubectl apply -f webhook-deploy.yaml
kubectl apply -f webhook-rbac.yaml
准入配置
MutatingWebhookConfiguration 配置清单:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: mutating-webhook-configuration
annotations:
cert-manager.io/inject-ca-from: kube-system/nodelocaldns-webhook
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
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:自动注入 CA 证书
- namespaceSelector:限制只对带标签的命名空间生效
- rules:仅处理 Pod 的创建和更新操作
部署准入配置:
cd deploy
kubectl apply -f webhook-config.yaml
5.验证测试
首先为命名空间启用注入:
kubectl label namespace default node-local-dns-injection=enabled
创建测试 Pod:
kubectl run busybox --image=busybox --restart=Never --namespace=default --command -- sleep infinity
查看 Pod 完整配置:
kubectl get pod busybox -o yaml
关键配置已注入:
spec:
dnsConfig:
nameservers:
- 169.254.20.10
options:
- name: ndots
value: "2"
searches:
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
dnsPolicy: None
验证未启用注入的命名空间:
kubectl create namespace myns
kubectl run busybox --image=busybox --restart=Never --namespace=myns --command -- sleep infinity
kubectl get pod busybox -o yaml -n myns
该 Pod 的 dnsConfig 未被修改,说明注入逻辑工作正常。
验证 DNS 解析功能:
kubectl run test-pod --image=busybox --restart=Never --namespace=default
kubectl exec -it test-pod -- nslookup kubernetes.default.svc.cluster.local
解析成功,Nameserver 显示为 169.254.20.10,证明 NodeLocalDNS 正常工作。
6.总结
通过自定义 Admission Webhook,实现了 Pod DNSConfig 的自动注入功能。Webhook 根据命名空间标签和 Pod 标签智能判断是否需要注入,满足条件的 Pod 会自动获得以 NodeLocalDNS 为首选 DNS 服务器的配置。整个过程自动化完成,无需手动修改 Pod YAML。