CoreDNS: Gọi Nhau Bằng Tên Trong Cluster

K
Kai··8 min read

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ưới cluster.local (và các vùng reverse). pods insecure cho 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ộc cluster.local được chuyển tiếp tới nameserver ghi trong /etc/resolv.conf củ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ấy NXDOMAIN. Đó không phải lỗi CoreDNS: nslookup của busybox không áp dụng danh sách search như resolver chuẩn. Bằng chứng: cùng pod đó, ping kubernetes.default lại in ra PING kubernetes.default (10.32.0.1), vì ping dùng getaddrinfo của libc, có áp search. 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.