本文主要分享如何使用 基於 Admission Webhook 實現自動修改 Pod DNSConfig,使其優先使用 NodeLocalDNS 。
<!--more-->
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標籤:
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 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。
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/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
部署命令如下:
cd deploy
# 部署 CertManager 以及簽發證書
kubectl apply -f cert-manager
# 部署 Webhook
kubectl apply -f webhook-deploy.yaml
kubectl apply -f webhook-rbac.yaml
MutatingWebhookConfiguration
yaml 大概是這樣的:
---
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 證書。
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.yaml
5. 測試
首先給 default namespace 打上 node-local-dns-injection=enabled label。
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
[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 部分如下:
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
kubectl create namespace myns
kubectl run busybox --image=busybox --restart=Never --namespace=myns --command -- sleep infinity
查看 DNSConfig 是否被修改
[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
kubectl run busybox-pod --image=busybox --restart=Never --namespace=default
進入 Pod 並測試解析 Service 記錄
[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 是沒問題的。
【Kubernetes 系列】持續更新中,搜索公眾號【探索雲原生】訂閲,閲讀更多文章。
6. 小結
本文主要分析瞭如何通過自定義一個 Admission Webhook 來自動化的修改 Pod 的 DNSConfig,使其優先使用 NodeLocalDNS。