🧠 Cluster DNS và troubleshoot service discovery

Mục tiêu bài học

Sau bài này, bạn phải làm được 3 việc trong khoảng 25 phút, đúng kiểu bài thi CKA:

  • Đọc và giải thích được /etc/resolv.conf bên trong Pod: nameserver, search, ndots.
  • Viết đúng FQDN để Pod này gọi Service ở namespace khác, không đoán mò.
  • Khi tên không phân giải được, khoanh vùng lỗi trong tối đa 5 phút: lỗi tên, lỗi CoreDNS, lỗi endpoints, hay lỗi network.

Mình đỗ CKA và lỗi làm mình mất thời gian nhất trong phòng thi không phải là YAML khó, mà là ngồi nslookup sai tên suốt 10 phút. Bài này là để bạn không lặp lại chuyện đó.

Ôn nhanh: nối tiếp bài trước

Bài trước bạn đã chốt: Service ClusterIP chỉ là một IP ảo, traffic thật đi tới các Pod đứng sau Service thông qua endpoints (kube-proxy DNAT). Bài này trả lời câu hỏi ngay sau đó: làm sao Pod biết được IP ảo đó? Câu trả lời là Cluster DNS, mặc định chạy bằng CoreDNS trong namespace kube-system.

[!NOTE] Nhớ kỹ ranh giới: DNS chỉ trả về IP. DNS không biết Pod đích có sống hay không. Phân giải được tên chưa có nghĩa là kết nối được.

Luồng phân giải tên trong cluster

flowchart LR
    Pod["📦<br/>Pod client"]
    Resolv["📄<br/>/etc/resolv.conf<br/>nameserver 10.96.0.10"]
    Svc["🔗<br/>Service kube-dns<br/>ClusterIP 10.96.0.10"]
    CoreDNS["🧩<br/>CoreDNS Pod<br/>kube-system"]
    API["☸️<br/>kube-apiserver"]
    EP["🎯<br/>Endpoints của Service đích"]

    Pod -->|"1. Hỏi tên api.backend"| Resolv
    Resolv -->|"2. Query UDP/TCP 53"| Svc
    Svc -->|"3. kube-proxy DNAT"| CoreDNS
    CoreDNS -->|"4. Watch Service và Endpoints"| API
    CoreDNS -->|"5. Trả A record là ClusterIP"| Pod
    Pod -->|"6. Kết nối tới ClusterIP"| EP

Ba mảnh ghép cần thuộc:

Thành phần Giá trị điển hình Xem bằng lệnh
DNS server mà Pod dùng ClusterIP của Service kube-dns kubectl get svc -n kube-system kube-dns
Search domain <ns>.svc.cluster.local svc.cluster.local cluster.local kubectl exec <pod> -- cat /etc/resolv.conf
Ứng dụng DNS CoreDNS Deployment, thường 2 replicas kubectl get deploy,pod -n kube-system -l k8s-app=kube-dns

Ví dụ mẫu: từ "bad address" tới FQDN đúng

Tình huống: namespace shop có Pod web gọi http://api:8080 tới Service api nằm ở namespace backend. Log báo Could not resolve host: api.

Bước 1. Xác nhận đây là lỗi DNS, không phải lỗi mạng.

kubectl exec -n shop web -- nslookup api
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

nslookup: can't resolve 'api'

Server trả lời được (10.96.0.10), nghĩa là Pod vẫn nói chuyện được với CoreDNS. Vấn đề nằm ở cái tên, không nằm ở đường mạng.

Bước 2. Đọc /etc/resolv.conf để hiểu vì sao tên bị sai.

kubectl exec -n shop web -- cat /etc/resolv.conf
nameserver 10.96.0.10
search shop.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

api có 0 dấu chấm, nhỏ hơn ndots:5, nên resolver thử lần lượt:

  1. api.shop.svc.cluster.local → NXDOMAIN (Service không nằm ở shop)
  2. api.svc.cluster.local → NXDOMAIN
  3. api.cluster.local → NXDOMAIN

Cả ba đều fail nên mới có can't resolve. Tên api chỉ đúng khi client cùng namespace với Service.

Bước 3. Dùng FQDN đầy đủ.

kubectl exec -n shop web -- nslookup api.backend.svc.cluster.local
Name:      api.backend.svc.cluster.local
Address 1: 10.96.44.12 api.backend.svc.cluster.local

Ra IP là xong phần DNS. Mẹo: viết thêm dấu chấm cuối (api.backend.svc.cluster.local.) để resolver coi đó là tên tuyệt đối và bỏ qua search list, đỡ 3 lần query rác.

Bước 4. Kiểm tra kết nối thật, vì DNS xong chưa chắc đã nối được.

kubectl get endpointslices -n backend -l kubernetes.io/service-name=api
kubectl exec -n shop web -- wget -qO- --timeout=3 http://api.backend.svc.cluster.local:8080/healthz

Nếu ENDPOINTS rỗng thì Service vẫn có DNS record nhưng không có ai nhận traffic. Lúc đó lỗi là ở selector hoặc targetPort, không phải DNS.

Bước 5. Sửa gốc: đổi biến môi trường của app từ API_HOST=api thành API_HOST=api.backend.svc.cluster.local (hoặc ngắn hơn là api.backend).

Tự kiểm tra nhanh bằng cloze:

FQDN đầy đủ của Service `api` trong namespace `backend` là {{c1::api.backend.svc.cluster.local}}.
CoreDNS được expose qua Service tên {{c2::kube-dns}} nằm trong namespace {{c3::kube-system}}.
Dòng `options ndots:5` khiến tên có ít hơn 5 dấu chấm được thử với các {{c4::search domain}} trước.
dnsPolicy mặc định của Pod là {{c5::ClusterFirst}}, còn Pod dùng hostNetwork phải đặt {{c6::ClusterFirstWithHostNet}}.

Thực hành có hướng dẫn

Chuẩn bị môi trường, chạy một lần rồi dùng cho cả 5 lab:

kubectl create namespace dns-lab
kubectl create deployment web --image=nginx:1.25 --replicas=2 -n dns-lab
kubectl expose deployment web --name=web --port=80 -n dns-lab
kubectl run client --image=busybox:1.28 -n dns-lab --restart=Never -- sleep 3600
kubectl get pod,svc -n dns-lab

Lab 1 (4 phút): đọc resolv.conf và phân giải tên cùng namespace

kubectl exec -n dns-lab client -- cat /etc/resolv.conf
kubectl exec -n dns-lab client -- nslookup web
kubectl exec -n dns-lab client -- nslookup web.dns-lab
kubectl exec -n dns-lab client -- nslookup web.dns-lab.svc.cluster.local
kubectl exec -n dns-lab client -- nslookup kubernetes.default
Kết quả mong đợi và câu hỏi

Cả bốn lệnh đầu đều trả về cùng một ClusterIP của Service web. Lệnh cuối trả về IP của Service kubernetes trong namespace default, tức là bạn đang nói chuyện với API server qua DNS.

Câu hỏi: vì sao web.dns-lab vẫn chạy dù thiếu svc.cluster.local? Vì search domain thứ hai là svc.cluster.local, resolver ghép vào thành web.dns-lab.svc.cluster.local.

Lab 2 (6 phút): gọi Service ở namespace khác

kubectl create namespace app-b
kubectl create deployment api --image=nginx:1.25 -n app-b
kubectl expose deployment api --name=api --port=8080 --target-port=80 -n app-b

kubectl exec -n dns-lab client -- nslookup api
kubectl exec -n dns-lab client -- nslookup api.app-b
kubectl exec -n dns-lab client -- nslookup api.app-b.svc.cluster.local
Kết quả mong đợi

Lệnh 1 fail với can't resolve 'api'. Lệnh 2 và 3 thành công và trả về cùng một IP.

Bài học: short name chỉ đúng trong cùng namespace. Cross-namespace thì ít nhất phải có ten-service.ten-namespace.

Lab 3 (4 phút): headless Service trả về nhiều A record

kubectl expose deployment web --name=web-hl --clusterip=None --port=80 -n dns-lab
kubectl exec -n dns-lab client -- cat /etc/resolv.conf
kubectl exec -n dns-lab client -- nslookup web-hl
kubectl exec -n dns-lab client -- nslookup web-hl.dns-lab.svc.cluster.local
kubectl get pod -n dns-lab -l app=web -o wide
Kết quả mong đợi

Service web-hl không có ClusterIP. nslookup trả về IP của từng Pod đứng sau Service, không phải một IP ảo. So sánh các IP này với cột IP ở lệnh cuối để thấy khớp.

Đây là nền cho StatefulSet ở các bài sau: client tự chọn Pod, không đi qua kube-proxy.

Lab 4 (5 phút): làm sập CoreDNS và quan sát lỗi

[!WARNING] Chỉ làm trong lab cá nhân. Không chạy lệnh này trên cluster dùng chung, và tuyệt đối không làm trong lúc thi.

kubectl scale deployment coredns -n kube-system --replicas=0
kubectl exec -n dns-lab client -- nslookup web
kubectl scale deployment coredns -n kube-system --replicas=2
kubectl get pod -n kube-system -l k8s-app=kube-dns -w
Kết quả mong đợi

Khi CoreDNS bị scale về 0, lệnh nslookup báo connection timed out; no servers could be reached hoặc treo vài giây rồi fail. Đây là dấu hiệu không tới được DNS server, khác hoàn toàn với NXDOMAIN.

Sau khi scale lại 2, chờ 2 Pod Running và Ready rồi test lại. Nếu vẫn lỗi, thử kubectl exec -n dns-lab client -- nslookup web lần nữa sau khoảng 30 giây, vì kube-proxy cần thời gian cập nhật.

Lab 5 (6 phút): sửa CoreDNS bị sai cluster domain

Đây là dạng bài rất giống đề thi: cho một Corefile hỏng, sửa cho chạy.

kubectl -n kube-system get configmap coredns -o yaml
Corefile: |
  .:53 {
      errors
      health {
        lameduck 5s
      }
      ready
      kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
      }
      prometheus :9153
      forward . /etc/resolv.conf
      cache 30
      loop
      reload
      loadbalance
  }

Giả sử ai đó đổi kubernetes cluster.local thành kubernetes wrong.local. Khi đó mọi tên *.cluster.local sẽ trả NXDOMAIN dù Pod CoreDNS vẫn Running và vẫn nhận query.

kubectl -n kube-system edit configmap coredns
# sửa lại thành: kubernetes cluster.local in-addr.arpa ip6.arpa
kubectl -n kube-system rollout restart deployment coredns
kubectl rollout status deployment coredns -n kube-system
kubectl exec -n dns-lab client -- nslookup web.dns-lab.svc.cluster.local

[!TIP] Corefile có plugin reload nên ConfigMap thay đổi thường tự nạp sau khoảng 30 giây. Nhưng trong phòng thi, rollout restart là cách chắc ăn và dễ chứng minh kết quả hơn.

Lỗi thường gặp và cách troubleshoot

Bảng chẩn đoán theo triệu chứng

Triệu chứng Nguyên nhân hay gặp Lệnh kiểm tra đầu tiên
NXDOMAIN / can't resolve Sai tên, sai namespace, thiếu FQDN kubectl get svc -n <ns>
connection timed out; no servers could be reached CoreDNS down, NetworkPolicy chặn 53, clusterDNS sai kubectl get pod -n kube-system -l k8s-app=kube-dns
Phân giải ra IP nhưng connection refused Endpoints rỗng, targetPort sai kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=<svc>
Chỉ Pod trên một node bị lỗi DNS clusterDNS trong config kubelet của node đó sai grep clusterDNS /var/lib/kubelet/config.yaml
Pod hostNetwork không có DNS dnsPolicy là Default kubectl get pod <pod> -o jsonpath='{.spec.dnsPolicy}'

Checklist 5 bước khi tên không phân giải được

1. Chạy `kubectl exec -n <ns> <pod> -- nslookup <tên>` để xem lỗi thuộc nhóm NXDOMAIN hay timeout.
2. Chạy `kubectl exec -n <ns> <pod> -- cat /etc/resolv.conf`, kiểm tra `nameserver` và `search`.
3. Chạy `nslookup <svc>.<ns>.svc.cluster.local` để loại trừ lỗi viết tên.
4. Chạy `kubectl get pod,svc,endpointslices -n kube-system | grep kube-dns` để kiểm tra CoreDNS và Service `kube-dns`.
5. Chạy `kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=<svc>` để chắc Service có backend.

Năm lỗi mình từng mắc

  1. Test bằng curl từ node thay vì từ Pod. Node không dùng Cluster DNS, kết quả test vô nghĩa. Luôn kubectl exec vào Pod.
  2. Dùng image busybox bản mới. Từ busybox 1.30 trở đi, nslookup xử lý ndots và search list khác đi, kết quả dễ gây nhiễu. Dùng busybox:1.28 cho giống tài liệu và đề thi.
  3. Quên -n kube-system. kubectl get svc kube-dns ở namespace mặc định sẽ báo NotFound và bạn tưởng DNS hỏng.
  4. Sửa Corefile nhưng không restart, rồi kết luận "sửa không được". Kiểm tra lại bằng kubectl -n kube-system logs -l k8s-app=kube-dns xem CoreDNS có load config mới không.
  5. Đồng nhất "resolve được" với "chạy được". Service không có Pod nào vẫn có A record. Nếu client báo connection refused thì mở endpoints ra xem, đừng đổ lỗi cho DNS.
# Bộ lệnh gom nhanh khi vào phòng thi
kubectl get svc -n kube-system kube-dns -o wide
kubectl get pod -n kube-system -l k8s-app=kube-dns -o wide
kubectl get endpointslices -n kube-system -l kubernetes.io/service-name=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=30

Góc chuyên gia (mẹo thi)

  • Một lệnh test DNS vạn năng, dùng đi dùng lại, khỏi nhớ dài dòng:
kubectl run dnstest --image=busybox:1.28 --rm -it --restart=Never -- nslookup kubernetes.default
  • Luôn ưu tiên FQDN <svc>.<ns>.svc.cluster.local khi viết manifest, ConfigMap hay biến môi trường trong bài thi. Nó đúng ở mọi namespace và không phụ thuộc search list. Đây là cách giảm rủi ro nhanh nhất.

  • Đừng đoán, hãy in ra. kubectl get svc -n kube-system kube-dns -o jsonpath='{.spec.clusterIP}' cho bạn đúng giá trị phải xuất hiện ở dòng nameserver. Nếu hai giá trị khác nhau thì lỗi nằm ở kubelet, không phải CoreDNS.

  • CoreDNS không phải chỗ duy nhất chứa DNS config. Với cluster cài bằng kubeadm, clusterDNS và clusterDomain nằm trong /var/lib/kubelet/config.yaml. Bài thi hay cho tình huống "Pod mới trên node này không resolve được, Pod cũ thì vẫn ổn": đó là dấu hiệu sửa kubelet chứ không phải sửa CoreDNS.

[!IMPORTANT] Quy tắc vàng về thời gian: nếu một câu DNS làm bạn mất hơn 5 phút, ghi chú lại, đánh dấu cờ, rồi đi tiếp. CKA tính điểm theo từng task, không có điểm cho việc hy sinh 15 phút cho một câu.

  • Phân biệt nhanh 2 họ lỗi, thuộc lòng để không mất thời gian suy nghĩ:
Lỗi Nghĩa là Đi kiểm tra
NXDOMAIN, can't resolve Tới được DNS, nhưng tên không tồn tại Tên, namespace, Corefile
timed out, no servers could be reached Không tới được DNS server CoreDNS Pod, Service kube-dns, NetworkPolicy, kubelet

Tóm tắt và bước tiếp theo

Chốt lại 4 dòng, đúng kiểu mình hay ghi ra giấy nháp trước khi vào thi:

  • FQDN: <service>.<namespace>.svc.cluster.local, dùng nó mặc định cho mọi cấu hình cross-namespace.
  • Pod luôn có /etc/resolv.conf trỏ tới Service kube-dns trong kube-system; đọc file này trước khi nghi ngờ bất cứ thứ gì.
  • ndots:5 là lý do short name chỉ đúng trong cùng namespace.
  • Phân giải được tên là chuyện của DNS, kết nối được là chuyện của endpoints. Hai lỗi khác nhau, hai cách sửa khác nhau.
[
  { "front": "FQDN đầy đủ của Service `api` trong namespace `backend`?", "back": "api.backend.svc.cluster.local" },
  { "front": "CoreDNS được expose qua Service nào, ở namespace nào?", "back": "Service `kube-dns` trong namespace `kube-system`." },
  { "front": "Dòng `nameserver` trong /etc/resolv.conf của Pod trỏ tới đâu?", "back": "ClusterIP của Service `kube-dns`." },
  { "front": "`options ndots:5` có tác dụng gì?", "back": "Tên có ít hơn 5 dấu chấm sẽ được thử ghép với các search domain trước khi query tuyệt đối." },
  { "front": "dnsPolicy mặc định của Pod là gì?", "back": "ClusterFirst." },
  { "front": "Pod dùng hostNetwork nên đặt dnsPolicy là gì?", "back": "ClusterFirstWithHostNet." },
  { "front": "Lệnh xem file phân giải tên bên trong Pod?", "back": "kubectl exec <pod> -n <ns> -- cat /etc/resolv.conf" },
  { "front": "Vì sao nên dùng image busybox:1.28 để test DNS?", "back": "Bản 1.28 có `nslookup` xử lý ndots và search list giống tài liệu, các bản mới cho kết quả gây nhiễu." },
  { "front": "Triệu chứng `connection timed out; no servers could be reached` nói lên điều gì?", "back": "Không tới được DNS server: CoreDNS down, NetworkPolicy chặn port 53, hoặc clusterDNS trong kubelet sai." },
  { "front": "Triệu chứng `NXDOMAIN` nói lên điều gì?", "back": "Tới được DNS nhưng tên không tồn tại: sai tên, sai namespace, hoặc Corefile cấu hình sai cluster domain." },
  { "front": "Service không có Pod nào đứng sau thì DNS trả gì?", "back": "Vẫn trả A record là ClusterIP, nhưng kết nối sẽ thất bại vì endpoints rỗng." },
  { "front": "Headless Service (`clusterIP: None`) trả về bản ghi gì?", "back": "A record là IP của từng Pod đứng sau Service, không phải một ClusterIP ảo." },
  { "front": "Cấu hình CoreDNS nằm ở đâu?", "back": "ConfigMap `coredns` trong namespace `kube-system`, key `Corefile`." },
  { "front": "Sau khi sửa Corefile, làm gì để chắc chắn áp dụng?", "back": "kubectl -n kube-system rollout restart deployment coredns, rồi kiểm tra lại bằng nslookup." },
  { "front": "ClusterDNS và clusterDomain của kubeadm nằm ở file nào?", "back": "/var/lib/kubelet/config.yaml, phần clusterDNS và clusterDomain." }
]

Bài tiếp theo: Ingress và Ingress controller. Bạn đã biết traffic đi từ Pod tới Service qua DNS và ClusterIP. Bài sau mở rộng ra biên của cluster: traffic từ ngoài Internet vào tới Service bên trong qua Ingress, cùng các lỗi thường gặp khi Ingress có rule đúng nhưng vẫn trả 404 hoặc 503.