# 🧠 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

```mermaid
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.**

```bash
kubectl exec -n shop web -- nslookup api
```

```text
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.**

```bash
kubectl exec -n shop web -- cat /etc/resolv.conf
```

```text
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 đủ.**

```bash
kubectl exec -n shop web -- nslookup api.backend.svc.cluster.local
```

```text
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.**

```bash
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:**

```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:

```bash
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

```bash
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
```

<details><summary>Kết quả mong đợi và câu hỏi</summary>

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`.
</details>

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

```bash
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
```

<details><summary>Kết quả mong đợi</summary>

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`.
</details>

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

```bash
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
```

<details><summary>Kết quả mong đợi</summary>

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.
</details>

### 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.

```bash
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
```

<details><summary>Kết quả mong đợi</summary>

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.
</details>

### 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.

```bash
kubectl -n kube-system get configmap coredns -o yaml
```

```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.

```bash
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

```steps
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.

```bash
# 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:

```bash
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.

```flashcard
[
  { "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.
