# 📡 Troubleshoot service và kết nối mạng trong cluster

Pod của bạn đã `Running` và `Ready`, nhưng gọi Service thì timeout. Đây là tình huống tôi gặp nhiều nhất trong lúc luyện thi, và cũng là dạng bài ăn điểm nhanh nếu bạn kiểm tra đúng thứ tự. Bài này tập trung vào một câu hỏi duy nhất: **luồng traffic đứt ở đâu?** Giữa pod và app có 4 chặng dễ đứt: DNS, Endpoints, port mapping và NetworkPolicy. Đi đúng thứ tự thì mỗi ca troubleshoot chỉ mất 2 đến 3 phút.

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

- Xác định được Service có endpoint hay không chỉ bằng 1 lệnh, và biết vì sao endpoint rỗng.
- Kiểm tra được chuỗi `port` → `targetPort` → `containerPort` khớp với app đang listen.
- Nhận diện NetworkPolicy chặn ingress hoặc chặn egress (đặc biệt là chặn DNS).
- Viết được lệnh test kết nối từ trong cluster bằng pod tạm, không phụ thuộc công cụ ngoài.
- Có quy trình 6 bước lặp lại được để dùng trong phòng thi.

## Ôn nhanh bài trước

Bài trước chốt lại cách đọc `describe`, `events` và `log` để xử lý `CrashLoopBackOff`, `ImagePullBackOff`, `Pending`. Điểm móc nối quan trọng: khi pod đã `1/1 Running` với restart count bằng 0 mà app vẫn không truy cập được, **lỗi gần như chắc chắn không nằm trong container nữa**, mà nằm ở lớp Service, DNS hoặc policy. Đừng đọc log app thêm lần nữa, hãy chuyển sang kiểm tra endpoints.

## Ví dụ mẫu: Service có endpoints rỗng

Bối cảnh: namespace `shop` có Deployment `api` chạy `nginx`, container listen port 80. Service `api-svc` phải trỏ tới nó. Thực tế đang lỗi, ta debug từng bước.

**Bước 1. Nhìn tổng thể trong một lệnh.**

```bash
kubectl -n shop get svc,ep,pods --show-labels
```

Kết quả đáng ngờ:

```text
NAME              TYPE        CLUSTER-IP     PORT(S)
service/api-svc   ClusterIP   10.96.14.22    80/TCP

NAME                   ENDPOINTS
endpoints/api-svc      <none>

NAME                   READY   STATUS    LABELS
pod/api-6d9f-abcde     1/1     Running   app=api-server,pod-template-hash=6d9f
```

`ENDPOINTS <none>` là manh mối số một. Service không có backend thì mọi thứ phía sau đều vô nghĩa.

**Bước 2. So selector của Service với label của pod.**

```bash
kubectl -n shop describe svc api-svc | grep -i selector
kubectl -n shop get pods --show-labels
```

Service đang chọn `app=api`, còn pod mang label `app=api-server`. Lệch một chữ là đủ để endpoint rỗng.

**Bước 3. Sửa selector, không sửa pod.**

Trong đề thi, câu hỏi thường quy định bên nào được phép sửa (đôi khi là "sửa Service", đôi khi là "sửa pod"). Ở đây đề cho sửa Service:

```bash
kubectl -n shop patch svc api-svc -p '{"spec":{"selector":{"app":"api-server"}}}'
```

**Bước 4. Xác nhận endpoint đã có IP.**

```bash
kubectl -n shop get endpoints api-svc
kubectl -n shop get endpointslices -l kubernetes.io/service-name=api-svc
```

Lần này phải thấy 1 hoặc 2 địa chỉ IP pod kèm port. Nếu vẫn `<none>`, kiểm tra pod đã `Ready` chưa: **pod chưa Ready thì không được đưa vào endpoints**.

**Bước 5. Test từ bên trong cluster, không test từ máy bạn.**

```bash
kubectl -n shop run tmp --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://api-svc.shop.svc.cluster.local/
```

Kết quả vẫn `timed out`. Endpoint có IP rồi mà vẫn hỏng, vậy điểm đứt nằm ở port mapping.

**Bước 6. So `port` với `targetPort` và `containerPort`.**

```bash
kubectl -n shop get svc api-svc -o yaml | grep -A5 ports
kubectl -n shop get deploy api -o jsonpath='{.spec.template.spec.containers[*].ports}'
```

Service khai `port: 80, targetPort: 8080`, nhưng nginx trong pod listen port 80. Traffic bị đẩy vào cổng không ai nghe. Sửa:

```bash
kubectl -n shop patch svc api-svc -p \
  '{"spec":{"ports":[{"port":80,"targetPort":80,"protocol":"TCP"}]}}'
```

Lưu ý: patch vào `ports` là **thay thế toàn bộ danh sách**, nên phải khai lại đầy đủ `port`, `targetPort`, `protocol`.

**Bước 7. Test lại.** Lần này `wget` trả về nội dung trang mặc định của nginx. Luồng traffic đã thông.

> [!TIP]
> Thứ tự kiểm tra cố định của tôi trong phòng thi: DNS → Endpoints → port/targetPort → NetworkPolicy → kube-proxy/CNI. Đi từ trên xuống, gặp chặng nào sai thì dừng lại sửa, không nhảy cóc.

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

Chuẩn bị chung cho cả 4 bài:

```bash
kubectl create namespace shop --dry-run=client -o yaml | kubectl apply -f -
```

Mỗi bài tập làm theo đúng 4 bước: **quan sát → khoanh vùng → sửa → kiểm chứng bằng lệnh test**. Đáp án nằm trong phần gập lại, hãy thử ít nhất 3 phút trước khi mở.

### Bài tập 1: Endpoint rỗng vì selector lệch

```bash
kubectl -n shop apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: shop
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-frontend
  template:
    metadata:
      labels:
        app: web-frontend
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-svc
  namespace: shop
spec:
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80
EOF
```

Nhiệm vụ: làm cho `http://web-svc` trả về nội dung từ trong cluster.

<details><summary>Xem gợi ý và đáp án</summary>

Gợi ý: `kubectl -n shop get endpoints web-svc` sẽ ra `<none>`. Đối chiếu `kubectl -n shop get pods --show-labels` với selector trong `kubectl -n shop describe svc web-svc`.

Đáp án (sửa Service):

```bash
kubectl -n shop patch svc web-svc -p '{"spec":{"selector":{"app":"web-frontend"}}}'
kubectl -n shop get endpoints web-svc
kubectl -n shop run t1 --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://web-svc.shop.svc.cluster.local/
```

Cách khác cũng hợp lệ nếu đề cho phép sửa pod: thêm label `app=web` vào pod template, sau đó rollout lại.
</details>

### Bài tập 2: Có endpoint nhưng connection refused

```bash
kubectl -n shop create deployment api --image=nginx:1.25 --replicas=1
kubectl -n shop expose deployment api --name=api-svc --port=80 --target-port=8080
```

Nhiệm vụ: `api-svc` phải phục vụ được trên port 80. Kiểm chứng bằng pod tạm.

<details><summary>Xem gợi ý và đáp án</summary>

Gợi ý: `kubectl -n shop get endpoints api-svc` **có** IP, nên vấn đề không phải selector. Test bằng busybox sẽ thấy connection refused thay vì timeout. Đọc `kubectl -n shop get svc api-svc -o yaml`, so `targetPort: 8080` với port nginx thực tế listen (80).

Đáp án:

```bash
kubectl -n shop patch svc api-svc -p \
  '{"spec":{"ports":[{"port":80,"targetPort":80,"protocol":"TCP"}]}}'
kubectl -n shop run t2 --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://api-svc/
```

Phân biệt triệu chứng: **timeout** thường là policy hoặc sai địa chỉ, **connection refused** thường là đúng IP nhưng sai port.
</details>

### Bài tập 3: NetworkPolicy chặn ingress

```bash
kubectl -n shop apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: shop
spec:
  podSelector: {}
  policyTypes:
  - Ingress
EOF
```

Nhiệm vụ: giữ nguyên policy `default-deny`, nhưng cho phép pod có label `access=granted` gọi được `web-svc` trên port 80. Test bằng pod tạm có label đó.

<details><summary>Xem gợi ý và đáp án</summary>

Gợi ý: `kubectl -n shop get networkpolicy` để thấy policy đang áp dụng. `podSelector: {}` nghĩa là áp lên **mọi pod** trong namespace. Đọc `kubectl -n shop describe networkpolicy default-deny` để xem chặng nào đang bị deny.

Đáp án:

```bash
kubectl -n shop apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-granted
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: web-frontend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          access: granted
    ports:
    - protocol: TCP
      port: 80
EOF
```

Test:

```bash
kubectl -n shop run t3 --labels="access=granted" --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://web-svc/
```

Chạy lại đúng lệnh trên nhưng bỏ `--labels`, bạn sẽ thấy bị chặn. Đó là bằng chứng policy hoạt động.
</details>

### Bài tập 4: DNS và egress policy

```bash
kubectl create namespace client --dry-run=client -o yaml | kubectl apply -f -
kubectl -n client apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-egress
  namespace: client
spec:
  podSelector: {}
  policyTypes:
  - Egress
EOF
```

Nhiệm vụ: pod trong namespace `client` phải gọi được `web-svc.shop.svc.cluster.local`, và DNS vẫn phải hoạt động. Không được xóa policy.

<details><summary>Xem gợi ý và đáp án</summary>

Gợi ý: chạy test trước, bạn sẽ thấy lỗi không phân giải được tên, dù CoreDNS hoàn toàn khỏe. `deny-egress` chặn luôn port 53 ra CoreDNS. Cần thêm 2 rule egress: một cho DNS, một cho traffic tới namespace `shop`.

Đáp án:

```bash
kubectl -n client apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-and-shop
  namespace: client
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: shop
    ports:
    - protocol: TCP
      port: 80
EOF
```

Test:

```bash
kubectl -n client run t4 --image=busybox:1.28 --rm -it --restart=Never -- \
  wget -qO- --timeout=3 http://web-svc.shop.svc.cluster.local/
```

Bài học lớn nhất ở đây: ==DNS fail trong cluster thường là lỗi NetworkPolicy egress, không phải lỗi CoreDNS==.
</details>

### Quy trình 6 bước áp dụng cho mọi đề

```steps
1. `kubectl -n <ns> get svc,ep,pods --show-labels`: tìm endpoint rỗng trước tiên.
2. `kubectl -n <ns> describe svc <name>`: đọc Selector, Port, TargetPort, Endpoints.
3. `kubectl -n <ns> get endpointslices -l kubernetes.io/service-name=<name>`: xác nhận IP và port đã resolve.
4. `kubectl -n <ns> get networkpolicy` + `describe`: loại trừ ingress/egress bị chặn.
5. Test bằng pod tạm trong đúng namespace: `wget`, `nc -zv` hoặc `nslookup`.
6. Nếu Service hỏng nhưng `kubectl port-forward pod/<pod> 8080:80` vẫn chạy: lỗi nằm ở lớp Service/policy, không phải app.
```

## Lỗi thường gặp và cách xử lý

| Triệu chứng | Nguyên nhân hay gặp | Lệnh kiểm tra |
| --- | --- | --- |
| Endpoints `<none>` | Service selector không khớp label pod, hoặc pod chưa Ready | `get svc,ep --show-labels`, `describe svc` |
| Timeout khi gọi Service | NetworkPolicy chặn, hoặc targetPort sai | `get networkpolicy`, `get svc -o yaml` |
| Connection refused | Đúng pod nhưng sai port | so `targetPort` với `containerPort` |
| Không phân giải được tên | Sai namespace, hoặc egress policy chặn port 53 | `nslookup <svc>.<ns>.svc.cluster.local` |
| Chỉ một số pod lỗi | Pod đó ở node mất CNI | `kubectl -n kube-system get pods -o wide` |
| NodePort không vào được từ ngoài | `externalTrafficPolicy: Local` và node không có pod backend | `describe svc`, `get pods -o wide` |

Một số bẫy cụ thể:

- **Service không có selector** (dùng cho Endpoints tự tạo hoặc ExternalName) thì endpoint không bao giờ tự sinh. Phải tạo object `Endpoints` cùng tên bằng tay.
- **Named port**: nếu `targetPort: http` mà `containerPort` không đặt `name: http`, endpoint sẽ không resolve được port. Kiểm tra bằng `get endpointslices ... -o yaml`.
- **Headless Service** (`clusterIP: None`) không có VIP, DNS trả về IP pod. App nào hard-code ClusterIP sẽ hỏng.
- **`kubectl get endpoints`** vẫn dùng được, nhưng trên cluster mới nên quen với `endpointslices`.
- **Test bằng `curl` trong image không có curl**: `nginx` không có curl, `busybox` không có curl. Dùng `wget -qO-` hoặc `nc -zv <host> <port>` cho chắc.

> [!WARNING]
> Đừng sửa cùng lúc nhiều thứ. Mỗi lần chỉ sửa một điểm rồi test lại. Trong phòng thi, sửa 3 chỗ một lúc mà vẫn lỗi thì bạn không biết chỗ nào đúng, và mất luôn khả năng quay lui.

## Góc chuyên gia

- **30 giây đầu tiên luôn dành cho endpoints.** Chín trên mười ca Service hỏng trong đề CKA là do selector hoặc targetPort, không phải do DNS.
- **Test từ đúng namespace.** Gọi tên ngắn `web-svc` chỉ đúng khi client ở cùng namespace. Khác namespace phải dùng `web-svc.shop.svc.cluster.local` hoặc ít nhất `web-svc.shop`.
- **Port-forward là máy đo chia đôi vùng lỗi.** `kubectl port-forward pod/<pod> 8080:80` chạy được nghĩa là app ổn, vấn đề ở Service hoặc policy. Port-forward tới `svc/` mà lỗi nghĩa là Service không có endpoint.
- **NetworkPolicy có tính cộng dồn**: nhiều policy, chỉ cần một policy cho phép là traffic đi qua. Đừng cố tìm "policy đúng duy nhất".
- **Mặc định không có NetworkPolicy nào thì mọi traffic được phép.** Nếu namespace có đúng một policy `deny-all`, hãy nhớ nó chỉ áp lên các pod khớp `podSelector`.
- **Kiểm tra image có sẵn trước khi chạy pod tạm.** Nếu `busybox` không pull được, hãy `kubectl exec` vào một pod đang chạy sẵn trong cluster. Đừng mất 5 phút chờ `ImagePullBackOff`.
- **Ghi lại bằng chứng vào đáp án khi được yêu cầu.** Vài đề yêu cầu dán output lệnh test, hãy chạy đúng lệnh mà đề mô tả.

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

Bốn dòng tôi tự nhắc trước giờ thi:

- Endpoint rỗng thì nhìn selector trước, đừng nhìn DNS.
- Endpoint có IP mà vẫn lỗi thì so `targetPort` với `containerPort`.
- Có policy trong namespace thì đọc `describe networkpolicy` trước khi nghi ngờ CNI.
- DNS fail mà CoreDNS vẫn chạy: nghĩ ngay tới egress policy chặn port 53.

Bài tiếp theo mở rộng sang **đọc log control plane và `journalctl`**: khi cả service lẫn pod đều ổn mà cluster vẫn có vấn đề, bạn sẽ cần đọc log của `kube-apiserver`, `etcd`, `kube-scheduler` và `journalctl -u kubelet` để tìm nguyên nhân gốc. Phần này cũng là nền để bạn xử lý các ca node `NotReady` nặng hơn.

Kiểm tra nhanh trước khi chuyển bài:

```cloze
1. Khi Service không có backend nào, lệnh đầu tiên nên chạy là `kubectl get {{c1::endpoints}} <tên-service>`.
2. Nếu Service khai `port: 80` mà bỏ trống `targetPort`, traffic sẽ đi tới container port {{c2::80}}.
3. Tên DNS đầy đủ của Service `api` trong namespace `shop` là `api.shop.{{c3::svc.cluster.local}}`.
4. NetworkPolicy với `podSelector: {}` và `policyTypes: [Ingress]` sẽ {{c4::chặn toàn bộ}} traffic ingress vào mọi pod trong namespace đó.
5. Nếu policy egress chặn hết, triệu chứng thường thấy là pod không {{c5::phân giải được DNS}} dù CoreDNS vẫn chạy bình thường.
6. Service có `clusterIP: None` được gọi là {{c6::headless}} Service.
```

```flashcard
[
  { "front": "Lệnh nhanh nhất để biết Service có backend hay không?", "back": "kubectl get endpoints <svc>, hoặc get endpointslices -l kubernetes.io/service-name=<svc>." },
  { "front": "Endpoints rỗng thường do nguyên nhân gì?", "back": "Selector của Service không khớp label pod, hoặc pod chưa Ready." },
  { "front": "Service tự sinh Endpoints khi nào?", "back": "Khi Service có selector khớp pod Ready; Service không có selector phải tạo Endpoints thủ công." },
  { "front": "targetPort bỏ trống thì lấy giá trị nào?", "back": "Mặc định bằng giá trị của port." },
  { "front": "Tên DNS đầy đủ của Service?", "back": "<service>.<namespace>.svc.cluster.local." },
  { "front": "Cách test kết nối từ trong cluster bằng pod tạm?", "back": "kubectl run tmp --image=busybox:1.28 --rm -it --restart=Never -- wget -qO- --timeout=3 http://<svc>:<port>." },
  { "front": "Port-forward tới pod chạy được nhưng tới Service thì không, kết luận gì?", "back": "App ổn; lỗi nằm ở Service, Endpoints hoặc NetworkPolicy." },
  { "front": "NetworkPolicy podSelector rỗng với policyTypes Ingress nghĩa là gì?", "back": "Chặn toàn bộ ingress vào mọi pod trong namespace." },
  { "front": "Vì sao policy egress chặn hết lại làm DNS fail?", "back": "Vì nó chặn luôn port 53 ra CoreDNS, dù CoreDNS vẫn khỏe." },
  { "front": "NetworkPolicy có tính cộng dồn không?", "back": "Có, chỉ cần một policy cho phép là traffic được đi qua." },
  { "front": "clusterIP: None nghĩa là gì?", "back": "Headless Service, DNS trả về IP của từng pod, không có VIP." },
  { "front": "Dấu hiệu CNI lỗi trên một node?", "back": "Pod trên node đó mất kết nối mạng và pod CNI trong kube-system không Running." },
  { "front": "externalTrafficPolicy: Local gây lỗi gì với NodePort?", "back": "Node không có pod backend sẽ drop traffic thay vì chuyển sang node khác." },
  { "front": "Phân biệt timeout và connection refused nói lên điều gì?", "back": "Timeout thường do policy hoặc sai địa chỉ; connection refused thường do đúng IP nhưng sai port." }
]
```
