CoreDNS: Gọi Nhau Bằng Tên Trong Cluster
Bài 14 nối xong mạng pod: pod nhận IP, ping được nhau xuyên node. Nhưng các IP đó là phù du — pod chết và tái sinh thì nhận IP khác, nên không ai viết IP pod vào cấu hình. Thứ giữ ổn định là tên: ta muốn gọi kube-dns.kube-system hay my-app.default và để hệ thống tự dịch ra IP hiện thời. Đó là việc của DNS trong cluster, và bản hiện thực mặc định là CoreDNS.
Điểm thú vị: CoreDNS không phải một thành phần đặc biệt nằm ngoài. Nó là một workload bình thường, vài pod đứng sau một Service, chạy trên chính cái cluster mà ta vừa dựng. Nó dùng mạng pod của Bài 14 và cơ chế Service của Bài 12. Nói cách khác, dựng được CoreDNS cũng là một phép thử rằng những bài trước đã đúng.
Mảnh ghép đã chờ sẵn
Nhớ lại Bài 11: trong KubeletConfiguration ta đã đặt clusterDNS: 10.32.0.10. Từ đó, mọi pod kubelet tạo ra đều nhận /etc/resolv.conf trỏ nameserver 10.32.0.10. Suốt mấy bài qua địa chỉ đó chưa có gì trả lời. Việc của bài này là đặt một thứ vào đúng 10.32.0.10 để nó phản hồi: một Service tên kube-dns với ClusterIP cố định bằng đúng con số ấy, đứng trước các pod CoreDNS.
pod bất kỳ
│ /etc/resolv.conf: nameserver 10.32.0.10
▼
Service kube-dns (ClusterIP 10.32.0.10)
│ kube-proxy DNAT (Bài 12)
▼
pod CoreDNS ──┬── tên *.svc.cluster.local ─► hỏi api-server, trả ClusterIP
└── tên ngoài ─► forward tới upstream (resolv.conf của node)
CoreDNS trả lời hai loại câu hỏi: tên trong cluster (Service, pod) thì nó tự biết nhờ theo dõi api-server; tên ngoài (ví dụ github.com) thì nó chuyển tiếp lên DNS upstream của node.
Bước 1 — ServiceAccount và RBAC
CoreDNS cần đọc danh sách Service, Endpoint, Pod, Namespace để biết tên nào ứng với IP nào. Như mọi thứ chạy trong cluster, nó xác thực bằng một ServiceAccount, và quyền cấp qua RBAC. Tạo một ServiceAccount coredns trong kube-system, một ClusterRole chỉ-đọc đúng các tài nguyên ấy, và binding nối hai cái:
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: coredns
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: system:coredns
rules:
- apiGroups: [""]
resources: ["endpoints", "services", "pods", "namespaces"]
verbs: ["list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:coredns
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:coredns
subjects:
- kind: ServiceAccount
name: coredns
namespace: kube-system
EOF
verbs: ["list", "watch"] là vừa đủ, vì CoreDNS chỉ quan sát, không bao giờ ghi. Quyền endpointslices (nhóm discovery.k8s.io) cần cho các bản Kubernetes gần đây, nơi thông tin endpoint nằm ở EndpointSlice thay cho Endpoints cũ.
Bước 2 — Corefile trong một ConfigMap
Cấu hình CoreDNS là một "Corefile", đặt trong ConfigMap để mount vào pod. Mỗi dòng là một plugin:
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
EOF
Hai plugin đáng chú ý nhất:
kubernetes cluster.local ...— đây là phần làm CoreDNS hiểu Kubernetes. Nó theo dõi Service/Endpoint qua api-server và phân giải mọi tên dướicluster.local(và các vùng reverse).pods insecurecho phép phân giải cả tên dạng pod-IP. Đây là plugin dùng tới RBAC ở Bước 1.forward . /etc/resolv.conf— mọi tên không thuộccluster.localđược chuyển tiếp tới nameserver ghi trong/etc/resolv.confcủa chính pod CoreDNS. Chi tiết này dẫn thẳng tới một cái bẫy ở bước sau.
Các plugin còn lại làm nền: health/ready cho probe, cache giữ kết quả 30 giây, loop phát hiện vòng lặp chuyển tiếp, reload tự nạp lại khi Corefile đổi, loadbalance xáo thứ tự bản ghi A.
Bước 3 — Deployment, và cái bẫy dnsPolicy
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: coredns
namespace: kube-system
labels:
k8s-app: kube-dns
spec:
replicas: 2
selector:
matchLabels:
k8s-app: kube-dns
template:
metadata:
labels:
k8s-app: kube-dns
spec:
priorityClassName: system-cluster-critical
serviceAccountName: coredns
dnsPolicy: Default
containers:
- name: coredns
image: registry.k8s.io/coredns/coredns:v1.12.4
args: ["-conf", "/etc/coredns/Corefile"]
ports:
- { containerPort: 53, name: dns, protocol: UDP }
- { containerPort: 53, name: dns-tcp, protocol: TCP }
- { containerPort: 9153, name: metrics, protocol: TCP }
resources:
limits: { memory: 170Mi }
requests: { cpu: 100m, memory: 70Mi }
livenessProbe:
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 60
readinessProbe:
httpGet: { path: /ready, port: 8181 }
volumeMounts:
- { name: config-volume, mountPath: /etc/coredns, readOnly: true }
volumes:
- name: config-volume
configMap:
name: coredns
items:
- { key: Corefile, path: Corefile }
EOF
Dòng dễ bỏ sót nhưng quan trọng nhất là dnsPolicy: Default. Mặc định pod dùng dnsPolicy: ClusterFirst — resolv.conf của nó trỏ 10.32.0.10, tức trỏ về chính CoreDNS. Nếu để vậy cho pod CoreDNS, thì forward . /etc/resolv.conf ở Bước 2 sẽ chuyển câu hỏi ngoài cluster về lại chính nó, thành một vòng lặp, và plugin loop sẽ phát hiện rồi cho pod crash ngay khi khởi động. dnsPolicy: Default bảo kubelet cấp cho pod CoreDNS bản resolv.conf của node (upstream thật, ở cụm này là 10.0.0.2 của VPC), nên forward chuyển tiếp ra đúng upstream. Đây là lỗi thường gặp khi tự dựng CoreDNS; nhớ nó để khỏi mất buổi chiều đọc log crash.
priorityClassName: system-cluster-critical xếp CoreDNS vào nhóm ưu tiên cao để không bị evict trước các workload thường. replicas: 2 để DNS không phụ thuộc một pod duy nhất.
Bước 4 — Service kube-dns ở đúng 10.32.0.10
Mảnh khóa: một Service với ClusterIP cố định đúng bằng clusterDNS của kubelet.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: kube-dns
namespace: kube-system
labels:
k8s-app: kube-dns
spec:
clusterIP: 10.32.0.10
selector:
k8s-app: kube-dns
ports:
- { name: dns, port: 53, protocol: UDP }
- { name: dns-tcp, port: 53, protocol: TCP }
- { name: metrics, port: 9153, protocol: TCP }
EOF
Khác với Service thường (để Kubernetes tự cấp ClusterIP), ở đây ta chỉ định clusterIP: 10.32.0.10, phải khớp con số đã nhúng vào kubelet ở Bài 11, nếu không pod sẽ hỏi DNS ở một địa chỉ chẳng ai trả lời. selector: k8s-app=kube-dns gắn Service này với hai pod CoreDNS.
Kiểm tra mọi thứ đã lên:
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system get svc kube-dns
NAME READY STATUS RESTARTS AGE IP NODE
coredns-596dd76cbc-4zxph 1/1 Running 0 7s 10.200.1.3 worker-1
coredns-596dd76cbc-9nlpq 1/1 Running 0 7s 10.200.0.3 worker-0
NAME TYPE CLUSTER-IP PORT(S) AGE
kube-dns ClusterIP 10.32.0.10 53/UDP,53/TCP,9153/TCP 7s
Hai pod Running, mỗi node một cái (scheduler tự rải), và Service đứng ở 10.32.0.10. Pod CoreDNS nhận IP từ dải pod Bài 14 (10.200.0.3, 10.200.1.3), bằng chứng nó là workload bình thường dùng đúng mạng ta dựng.
Bước 5 — Phân giải tên từ một pod
Tạo một pod thử và xem nó được cấp resolv.conf gì:
kubectl run dnstest --image=busybox:1.36 --restart=Never --command -- sleep 3600
kubectl exec dnstest -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local ap-southeast-1.compute.internal
nameserver 10.32.0.10
options ndots:5
kubelet đã bơm vào: nameserver 10.32.0.10 (CoreDNS), một danh sách search domain, và ndots:5. Phần search là thứ cho phép viết tên ngắn: gõ kube-dns.kube-system thì resolver thử nối lần lượt .svc.cluster.local, .cluster.local... cho tới khi trúng. Thử phân giải tên đầy đủ của hai Service:
kubectl exec dnstest -- nslookup kube-dns.kube-system.svc.cluster.local
kubectl exec dnstest -- nslookup kubernetes.default.svc.cluster.local
Name: kube-dns.kube-system.svc.cluster.local
Address: 10.32.0.10
Name: kubernetes.default.svc.cluster.local
Address: 10.32.0.1
Tên Service được dịch đúng thành ClusterIP: kube-dns ra 10.32.0.10, kubernetes ra 10.32.0.1. Quy ước tên là <service>.<namespace>.svc.cluster.local. Tên ngoài cluster cũng phân giải được, qua forward:
kubectl exec dnstest -- nslookup one.one.one.one
Non-authoritative answer:
Name: one.one.one.one
Address: 2606:4700:4700::1111
CoreDNS không tự biết one.one.one.one, nên nó chuyển câu hỏi lên 10.0.0.2 (upstream của node) và trả kết quả về, đúng vai trò dnsPolicy: Default đã sắp đặt.
Một lưu ý về busybox. Nếu thử
nslookup kubernetes.default(tên ngắn) bằng busybox, bạn sẽ thấyNXDOMAIN. Đó không phải lỗi CoreDNS:nslookupcủa busybox không áp dụng danh sáchsearchnhư resolver chuẩn. Bằng chứng: cùng pod đó,ping kubernetes.defaultlại in raPING kubernetes.default (10.32.0.1), vìpingdùnggetaddrinfocủa libc, có ápsearch. Khi gỡ rối DNS trong cluster, nhớ phân biệt lỗi của công cụ với lỗi của hệ thống.
🧹 Dọn dẹp
Xóa pod thử; CoreDNS thì giữ lại, vì từ giờ nó là thành phần thường trú và các bài sau dựa vào nó:
kubectl delete pod dnstest
CoreDNS chạy trong cluster nên không có gì phải dọn trên node hay VPC. Nếu stop/start cụm, Deployment tự dựng lại pod; chỉ nhớ luật masquerade ở Bài 14 cần chạy lại, nếu không CoreDNS sẽ phân giải được tên nội bộ nhưng không forward ra ngoài được (gói tới 10.0.0.2 không được SNAT).
Manifest đầy đủ ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 15-coredns.
Tổng kết
Cluster giờ có DNS nội bộ: pod gọi nhau bằng tên Service ổn định thay vì IP phù du, và tên ngoài cũng phân giải được. Đáng nhớ nhất là cách CoreDNS tự nó là một workload bình thường (Deployment sau một Service) chứ không phải hạ tầng đặc biệt, cùng cái bẫy dnsPolicy: Default để pod CoreDNS không hỏi DNS ở chính mình. Mảnh clusterDNS ta gài từ Bài 11 đến đây mới khớp vào ổ.
Tới đây mọi thành phần của một cluster tối thiểu đã đủ mặt: control plane, worker, mạng pod, Service, DNS. Bài 16 gom tất cả lại thành một smoke test có hệ thống: triển khai một ứng dụng thật qua Deployment, lộ ra bằng Service, gọi nó bằng tên, kiểm tra kubectl logs/exec/port-forward, để xác nhận từng đường dây ta dựng suốt series thực sự hoạt động cùng nhau.