# 🔌 Service types, ClusterIP, NodePort và endpoints

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

Sau bài này, bạn phải làm được ba việc sau trong khoảng 2 đến 3 phút mỗi việc:

- Chọn đúng `type` cho Service khi đề bài yêu cầu (ClusterIP, NodePort, LoadBalancer, ExternalName).
- Tạo Service bằng lệnh imperative nhanh, rồi sửa YAML khi cần.
- Nhìn ra ngay vì sao `kubectl get endpoints` trả về `<none>` và sửa được trong 1 phút.

> [!NOTE]
> Đây là bài mở đầu của module Networking. Mình sẽ không nói về DNS và CoreDNS ở đây (bài sau), chỉ tập trung vào Service object và Endpoints object.

## Ôn lại nhanh

Ở module Workloads bạn đã tạo Deployment, và Pod do Deployment sinh ra luôn có label do `spec.selector.matchLabels` quy định. Service **không** tìm Pod theo tên, nó tìm theo **label selector**. Đây là gốc rễ của 90% lỗi trong bài này, nên nhớ kỹ: Service là một "bộ lọc label" cộng với một "địa chỉ ảo".

## Bốn loại Service, nhìn một bảng là đủ

| Type | Có ClusterIP? | Truy cập từ đâu? | Dùng khi nào |
| --- | --- | --- | --- |
| `ClusterIP` (mặc định) | Có | Chỉ trong cluster | Giao tiếp nội bộ giữa các service |
| `NodePort` | Có | Từ ngoài qua `nodeIP:nodePort` | Cần expose ra ngoài không có cloud LB |
| `LoadBalancer` | Có | Qua IP của LB do cloud cấp | Cluster trên cloud (GKE, EKS, AKS) |
| `ExternalName` | Không | Qua CNAME DNS | Trỏ Service tới một DNS name bên ngoài |

Điểm quan trọng: **NodePort và LoadBalancer đều là ClusterIP ở bên dưới**. Chúng chỉ thêm một lớp phía trước. Nên nếu bạn hiểu ClusterIP thì hiểu luôn hai loại kia.

```mermaid
flowchart LR
    Client["👤 Client ngoài cluster"] -->|"nodeIP:30080"| NP["🚪 NodePort<br/>mọi node"]
    Client2["📦 Pod trong cluster"] -->|"web-svc:8080"| SVC
    NP --> SVC["🎯 Service ClusterIP<br/>10.96.0.10"]
    SVC --> EP["📋 Endpoints<br/>danh sách IP:port"]
    EP --> P1["📦 Pod 1"]
    EP --> P2["📦 Pod 2"]
    EP --> P3["📦 Pod 3"]
```

Ba khái niệm port phải phân biệt được, đây là chỗ đề thi hay gài:

- `port`: port của **Service**, client gọi vào đây.
- `targetPort`: port **trong container** của Pod, traffic được đẩy tới đây.
- `nodePort`: port mở trên **mỗi node**, mặc định thuộc dải {{c1::30000-32767}}.

## Ví dụ mẫu từng bước

Đề bài kiểu này rất hay gặp: "Tạo Deployment tên `web` dùng image nginx với 3 replicas. Expose nó bằng một Service tên `web-svc` lắng nghe port 8080 và chuyển tiếp vào port 80 của container."

**Bước 1: Tạo workload.**

```bash
kubectl create deployment web --image=nginx --replicas=3
kubectl get pods -l app=web
```

**Bước 2: Sinh Service bằng `kubectl expose`.** Lệnh này tự lấy selector từ Deployment nên ít lỗi hơn viết YAML tay.

```bash
kubectl expose deployment web \
  --name=web-svc \
  --port=8080 \
  --target-port=80
```

**Bước 3: Kiểm tra Service và Endpoints.** Kiểm tra Endpoints là bước bắt buộc, đừng bỏ qua.

```bash
kubectl get svc web-svc -o wide
kubectl get endpoints web-svc
```

Kết quả mong đợi:

```
NAME      TYPE        CLUSTER-IP     PORT(S)    AGE
web-svc   ClusterIP   10.96.142.31   8080/TCP   15s

NAME      ENDPOINTS                                      AGE
web-svc   10.244.1.5:80,10.244.2.4:80,10.244.3.6:80     15s
```

Đọc kết quả: có 3 địa chỉ IP, đúng bằng 3 replicas, và mỗi địa chỉ đi kèm port `80` (là `targetPort`, không phải `port` 8080).

**Bước 4: Test từ bên trong cluster.** ClusterIP không truy cập được từ máy host của bạn, phải test từ một Pod.

```bash
kubectl run tmp --rm -it --image=busybox:1.28 --restart=Never -- \
  wget -qO- http://web-svc:8080
```

Nếu thấy HTML của nginx là xong. Nếu treo, thoát bằng `Ctrl+C` rồi quay lại mục troubleshoot.

**Bước 5: Đổi sang NodePort và test từ ngoài.** Không cần xóa Service cũ, chỉ cần sửa type.

```bash
kubectl patch svc web-svc -p '{"spec":{"type":"NodePort"}}'
kubectl get svc web-svc
```

```
NAME      TYPE       CLUSTER-IP     PORT(S)          AGE
web-svc   NodePort   10.96.142.31   8080:31234/TCP   3m
```

Giờ `curl <IP-node>:31234` từ máy bạn sẽ ra nginx. Nếu đề yêu cầu nodePort cụ thể, phải khai báo tường minh:

```bash
kubectl patch svc web-svc -p '{"spec":{"ports":[{"port":8080,"targetPort":80,"nodePort":30080}]}}'
```

> [!TIP]
> Luôn dùng `--dry-run=client -o yaml` để sinh YAML rồi sửa, nhanh hơn viết từ đầu và ít sai indent.
> ```bash
> kubectl expose deployment web --name=web-svc --port=8080 --target-port=80 \
>   --type=NodePort --dry-run=client -o yaml > svc.yaml
> ```

## Luyện tập có hướng dẫn

Làm đúng thứ tự, mỗi bài tự bấm giờ 2 đến 3 phút. Đáp án nằm trong phần gập lại, chỉ mở sau khi đã thử.

### Bài 1: ClusterIP và đọc endpoints

Tạo Deployment `api` dùng image `nginx:1.25` với 2 replicas. Expose thành Service `api-svc` port 80, targetPort 80. Sau đó in ra danh sách endpoints.

```bash
kubectl create deployment api --image=nginx:1.25 --replicas=2
# viết tiếp ở đây
```

<details><summary>Xem đáp án</summary>

```bash
kubectl expose deployment api --name=api-svc --port=80 --target-port=80
kubectl get endpoints api-svc
```

Phải thấy đúng 2 IP kèm `:80`. Nếu thấy `<none>`, chuyển sang bài 3.

</details>

### Bài 2: NodePort với port chỉ định

Sửa `api-svc` thành NodePort, giữ nguyên `port` 80 và ép `nodePort` là `30081`.

<details><summary>Xem đáp án</summary>

```bash
kubectl patch svc api-svc -p \
  '{"spec":{"type":"NodePort","ports":[{"port":80,"targetPort":80,"nodePort":30081}]}}'
kubectl get svc api-svc
```

Kiểm tra cột `PORT(S)` phải hiện `80:30081/TCP`. Nhớ: dải hợp lệ là 30000 đến 32767, đặt 8081 sẽ bị API server từ chối.

</details>

### Bài 3: Cố tình làm hỏng selector

Đây là bài quan trọng nhất. Tạo một Service trỏ sai label rồi tự sửa.

```bash
kubectl create service clusterip broken-svc --tcp=80:80
```

Lệnh này tạo Service với selector `app=broken-svc`, trong khi Pod của bạn có label `app=api`.

```bash
kubectl get endpoints broken-svc
kubectl get pods --show-labels
```

<details><summary>Xem đáp án</summary>

`kubectl get endpoints broken-svc` trả về `<none>`. Sửa selector:

```bash
kubectl patch svc broken-svc -p '{"spec":{"selector":{"app":"api"}}}'
kubectl get endpoints broken-svc
```

Endpoints xuất hiện ngay, không cần restart Pod hay Service. Đây là câu thần chú: **Service selector sửa được bất cứ lúc nào, không immutable**.

</details>

### Bài 4: Pod Running nhưng endpoints rỗng

Thêm một `readinessProbe` trỏ vào đường dẫn không tồn tại, quan sát endpoints biến mất, rồi sửa lại.

```bash
kubectl patch deployment api -p \
  '{"spec":{"template":{"spec":{"containers":[{"name":"nginx","readinessProbe":{"httpGet":{"path":"/healthz","port":80}}}]}}}}'
kubectl get pods -w
```

<details><summary>Xem đáp án</summary>

nginx trả 404 cho `/healthz` nên probe fail, Pod chuyển sang `READY 0/1` nhưng vẫn `Running`. Endpoints của `api-svc` sẽ rỗng dần.

```bash
kubectl get endpoints api-svc     # <none>
kubectl get pods                  # READY 0/1
```

Sửa bằng cách đổi path về `/`:

```bash
kubectl patch deployment api -p \
  '{"spec":{"template":{"spec":{"containers":[{"name":"nginx","readinessProbe":{"httpGet":{"path":"/","port":80}}}]}}}}'
```

Bài học: **Pod chỉ vào endpoints khi ở trạng thái Ready**. `Running` chưa đủ.

</details>

### Bài 5: Service không selector, endpoints thủ công

Đề bài hay cho tình huống: "Trỏ một Service tới database nằm ngoài cluster có IP 10.0.0.50 port 3306". Service không có selector, bạn tự tạo Endpoints.

<details><summary>Xem đáp án</summary>

```yaml
apiVersion: v1
kind: Service
metadata:
  name: external-db
spec:
  ports:
  - port: 3306
    targetPort: 3306
---
apiVersion: v1
kind: Endpoints
metadata:
  name: external-db
subsets:
- addresses:
  - ip: 10.0.0.50
  ports:
  - port: 3306
```

```bash
kubectl apply -f external-db.yaml
kubectl get endpoints external-db
```

Quy tắc: Endpoints phải **cùng tên, cùng namespace** với Service. Không có selector thì Kubernetes không ghi đè Endpoints của bạn.

</details>

### Bài 6: Nhận diện type qua YAML

Cho nhanh, đọc ba đoạn YAML sau và gọi tên type, không cần apply.

```yaml
# A
spec:
  type: ExternalName
  externalName: db.prod.example.com
```

```yaml
# B
spec:
  clusterIP: None
  selector:
    app: api
```

```yaml
# C
spec:
  type: NodePort
  ports:
  - port: 80
    nodePort: 30080
```

<details><summary>Xem đáp án</summary>

- A: `ExternalName`, không có ClusterIP, không có endpoints, chỉ trả về CNAME.
- B: Headless Service (`clusterIP: None`), DNS trả về IP của từng Pod thay vì một VIP.
- C: NodePort, mở port 30080 trên mọi node.

</details>

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

**Endpoints rỗng (`<none>`).** Kiểm tra theo đúng thứ tự này, đừng nhảy bước:

```bash
kubectl get svc <name> -o yaml | grep -A3 selector   # 1. selector là gì
kubectl get pods --show-labels                        # 2. label Pod có khớp không
kubectl get pods                                      # 3. Pod có READY 1/1 không
kubectl describe svc <name>                           # 4. xem Events và Endpoints
```

Ba nguyên nhân theo tần suất: selector sai hoặc sai namespace, Pod chưa Ready (readiness probe fail), và Pod chưa chạy (ImagePullBackOff, Pending, CrashLoopBackOff).

**Nhầm `port` với `targetPort`.** App trong container nghe 8080 nhưng bạn để `targetPort: 80` thì kết nối sẽ bị từ chối. Mẹo: `kubectl exec` vào Pod chạy `ss -tlnp` hoặc xem `containerPort` trong Deployment để biết app nghe port nào.

**Quên namespace.** `kubectl expose` và `kubectl get endpoints` mặc định dùng namespace hiện tại. Trong đề thi hầu như luôn có namespace riêng, nên thêm `-n <ns>` ngay từ lệnh đầu tiên.

**Đặt `nodePort` ngoài dải.** Giá trị phải nằm trong {{c2::30000-32767}}. Nếu đề cho 3xxxx cụ thể thì bắt buộc khai báo, còn không thì để Kubernetes tự cấp và đọc lại bằng `kubectl get svc`.

**Cố ping ClusterIP.** ClusterIP là địa chỉ ảo do kube-proxy lập trình bằng iptables/IPVS, nó không trả lời ICMP. `ping 10.96.x.x` fail là bình thường, hãy test bằng TCP: `wget`, `curl`, hoặc `nc -zv`.

**Sửa Service rồi tưởng phải restart Pod.** Không cần. Ngược lại, nếu sửa label của Pod thì endpoints tự cập nhật trong vài giây.

**Nhầm `Endpoints` với `EndpointSlice`.** Cluster mới dùng EndpointSlice làm nguồn chính, Endpoints chỉ là view tương thích. Khi debug, `kubectl get endpoints` vẫn đủ dùng, nhưng nếu cần chi tiết thì:

```bash
kubectl get endpointslices -l kubernetes.io/service-name=<ten-svc>
```

> [!WARNING]
> Đừng đặt tên Service bắt đầu bằng số hoặc chứa dấu chấm. Tên Service phải là DNS-1035 label: chữ thường, số, dấu gạch ngang, bắt đầu bằng chữ cái.

## Góc chuyên gia

**Câu thần chú khi thi:** "Service không có endpoints thì 90% là selector hoặc readiness". Hai lệnh này giải quyết gần như mọi ca:

```bash
kubectl get svc,endpoints -o wide
kubectl get pods --show-labels
```

**Ưu tiên imperative, sửa bằng patch.** Với Service, `kubectl expose` cộng `kubectl patch` nhanh hơn hẳn việc gõ YAML từ đầu. Chỉ viết YAML khi cần Service không selector hoặc cần cấu hình đặc biệt.

**Đọc đề thật kỹ phần port.** Đề CKA rất hay viết: "expose the deployment on port 8080, the container listens on 80, use nodePort 30080". Ba con số đó map chính xác vào `port`, `targetPort`, `nodePort`. Đọc sai một số là mất điểm cả câu.

**Test đúng chỗ.** ClusterIP test từ trong Pod, NodePort test từ node. Không phân biệt được hai môi trường này là nguyên nhân phổ biến khiến bạn tưởng mình làm sai.

**Giới hạn thời gian.** Một câu Service không nên quá 3 phút. Nếu quá, ghi chú lại, đánh dấu cờ trong giao diện thi rồi làm tiếp. Quay lại sau.

**Đừng quên `kubectl get svc` sau cùng.** Rất nhiều bạn tạo xong, test xong, nhưng không kiểm tra lại `PORT(S)`, nên không phát hiện nodePort bị auto-assign khác yêu cầu.

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

Chốt lại bốn dòng:

- `ClusterIP` là mặc định, chỉ dùng trong cluster; `NodePort` và `LoadBalancer` đều dựa trên nó.
- Ba port phải phân biệt: `port` của Service, `targetPort` của container, `nodePort` trên node.
- Endpoints được sinh tự động từ **selector khớp label** và **Pod đang Ready**.
- Endpoints rỗng thì đi theo thứ tự: selector, label, trạng thái Pod, rồi mới đến kube-proxy.

Bài sau chúng ta sẽ đi sâu vào phần mà bài này vừa chạm tới: **Cluster DNS và troubleshoot service discovery**. Cụ thể là vì sao `web-svc` gọi được mà `web-svc.default.svc.cluster.local` lại fail, cách đọc `/etc/resolv.conf` trong Pod, và cách kiểm tra CoreDNS khi cả cluster mất phân giải tên.

```flashcard
[
  { "front": "Service type mặc định khi không khai báo `type` là gì?", "back": "ClusterIP." },
  { "front": "Dải nodePort mặc định là bao nhiêu?", "back": "30000 đến 32767." },
  { "front": "`port` trong Service có nghĩa là gì?", "back": "Port mà Service lắng nghe, client gọi vào đây." },
  { "front": "`targetPort` trong Service có nghĩa là gì?", "back": "Port của container trong Pod mà traffic được chuyển tới." },
  { "front": "Lệnh xem endpoints của một Service?", "back": "kubectl get endpoints <ten-svc> (viết tắt: kubectl get ep)." },
  { "front": "Nguyên nhân phổ biến nhất khiến endpoints rỗng?", "back": "Selector của Service không khớp label của Pod đang Ready." },
  { "front": "Pod đang Running nhưng chưa Ready thì có nằm trong endpoints không?", "back": "Không, chỉ Pod ở trạng thái Ready mới được đưa vào endpoints." },
  { "front": "Service type nào chỉ trả về CNAME và không có ClusterIP?", "back": "ExternalName." },
  { "front": "Service type nào được cloud provider cấp IP ngoài và có nodePort bên dưới?", "back": "LoadBalancer." },
  { "front": "Lệnh imperative tạo Service từ một Deployment?", "back": "kubectl expose deployment <ten> --port=... --target-port=... --type=..." },
  { "front": "Có ping được ClusterIP không?", "back": "Không, ClusterIP là IP ảo xử lý ở tầng TCP/UDP, phải test bằng curl hoặc wget." },
  { "front": "Service không có selector thì lấy endpoints từ đâu?", "back": "Từ Endpoints object tạo thủ công cùng tên và cùng namespace." },
  { "front": "Headless Service khai báo thế nào?", "back": "spec.clusterIP: None." },
  { "front": "EndpointSlice gắn với Service qua label nào?", "back": "kubernetes.io/service-name." }
]
```

Ôn nhanh bằng cloze:

- Mặc định Service được tạo với type {{c3::ClusterIP}}.
- Pod chỉ được đưa vào endpoints khi vượt qua {{c4::readinessProbe}}.
- `ExternalName` dùng bản ghi {{c5::CNAME}} để trỏ ra ngoài cluster.
