本文主要分享如何使用 基于 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=enabled
label 的命名空间中新建 Pod 的进行注入。
可以通过以下命令给命名空间打上Label标签:
1
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 地址,也提供了默认值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 ,
}
}
启动服务时可以指定
1
2
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 方法中就是核心逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
这里就是按照之前提到的注入规则进行判定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// NeedMutation Check whether the target resoured need to be mutated
func ( 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 [ NodeLocalDNSInjection ]; ! 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 [ NodeLocalDNSInjection ]; 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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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,大家可以直接使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 # 提供查询 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 : v1
kind : Service
metadata :
name : nodelocaldns-webhook
namespace : kube-system
labels :
app : nodelocaldns
spec :
ports :
- port : 443
targetPort : 9443
selector :
app : nodelocaldns
部署命令如下:
1
2
3
4
5
6
7
cd deploy
# 部署 CertManager 以及签发证书
kubectl apply -f cert-manager
# 部署 Webhook
kubectl apply -f webhook-deploy.yaml
kubectl apply -f webhook-rbac.yaml
MutatingWebhookConfiguration
yaml 大概是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
---
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 :
#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 证书。
1
2
annotations :
cert-manager.io/inject-ca-from : kube-system/nodelocaldns-webhook
限制生效范围
1
2
3
namespaceSelector : # 限制生效范围
matchLabels :
node-local-dns-injection : enabled
只关心 Pod 的 Create、Update 事件:
1
2
3
4
5
6
7
8
9
10
rules :
- apiGroups :
- ""
apiVersions :
- v1
operations :
- CREATE
- UPDATE
resources :
- pods
也是直接 apply 即可
1
2
cd deploy
kubectl apply -f webhook-config.yaml
5. 测试
首先给 default namespace 打上 node-local-dns-injection=enabled
label。
1
kubectl label namespace default node-local-dns-injection= enabled
创建一个 Pod,然后查看 yaml 看看 dnsConfig 是否被修改了。
1
kubectl run busybox --image= busybox --restart= Never --namespace= default --command -- sleep infinity
查看一下完整 Yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[ root@webhook ~] # k get po busybox -oyaml
apiVersion: v1
kind: Pod
metadata:
annotations:
cni.projectcalico.org/containerID: 2a4caca308b031f872c47ef334cf7e940d74646a2f0a8893c7786508d30ed488
cni.projectcalico.org/podIP: 172.25.233.215/32
cni.projectcalico.org/podIPs: 172.25.233.215/32
creationTimestamp: "2024-02-05T10:43:16Z"
labels:
run: nginx-pod
name: nginx-pod
namespace: default
resourceVersion: "19341"
uid: 2b107b50-e85c-462f-8919-a0c01114bae6
spec:
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: true
dnsConfig:
nameservers:
- 169.254.20.10
options:
- name: ndots
value: "2"
searches:
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
dnsPolicy: None
Dns 部分如下:
1
2
3
4
5
6
7
8
9
10
11
dnsConfig :
nameservers :
- 169.254.20.10
options :
- name : ndots
value : "2"
searches :
- default.svc.cluster.local
- svc.cluster.local
- cluster.local
dnsPolicy : None
可以看到,已经注入了我们的 NodeLocalDNS 了。
然后往没有打 Label 的命名空间创建 Pod
1
2
kubectl create namespace myns
kubectl run busybox --image= busybox --restart= Never --namespace= myns --command -- sleep infinity
查看 DNSConfig 是否被修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[ root@webhook ~] # kubectl -n myns get pod nginx-pod -oyaml
apiVersion: v1
kind: Pod
metadata:
annotations:
cni.projectcalico.org/containerID: 93a545988d7c7bbb88f0bc0e745226cd9e684bd63b78754dadd738861ed34512
cni.projectcalico.org/podIP: 172.25.233.218/32
cni.projectcalico.org/podIPs: 172.25.233.218/32
creationTimestamp: "2024-02-06T01:22:36Z"
labels:
run: nginx-pod
name: nginx-pod
namespace: myns
resourceVersion: "116195"
uid: 1f64a831-7470-49d5-b28e-1cd231ef5d8f
spec:
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: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
可以看到,并没有,说明我们的逻辑是没问题的,只会对打了 Label 的命名空间中的 Pod 进行注入。
最后测试能否正常解析
测试一下注入 DNSConfig 之后能否正常解析 DNS
1
kubectl run busybox-pod --image= busybox --restart= Never --namespace= default
进入 Pod 并测试解析 Service 记录
1
2
3
4
5
6
7
8
9
[ root@webhook ~] # k exec -it busybox-pod -- nslookup nodelocaldns-webhook.kube-system.svc.cluster.local
Server: 169.254.20.10
Address: 169.254.20.10:53
Name: nodelocaldns-webhook.kube-system.svc.cluster.local
Address: 10.105.137.213
[ root@webhook ~] # kk get svc nodelocaldns-webhook
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT( S) AGE
nodelocaldns-webhook ClusterIP 10.105.137.213 <none> 443/TCP 14h
可以看到,Nameserver 是 169.254.20.10,也就是我们的 NodeLocalDNS,然后能拿到正确的 IP,说明我们的 NodeLocalDNS 是没问题的。
6. 小结
本文主要分析了如何通过自定义一个 Admission Webhook 来自动化的修改 Pod 的 DNSConfig,使其优先使用 NodeLocalDNS。