# 🌐 Service và Ingress: định tuyến traffic vào workload

## Mục tiêu học tập

Sau bài này, bạn sẽ:

- Chọn đúng loại Service giữa `ClusterIP`, `NodePort`, `LoadBalancer` (và biết khi nào dùng `Headless`).
- Giải thích được EndpointSlice và kube-proxy làm gì khi một request đi vào Service.
- Viết được YAML Service khớp đúng `selector` với `labels` của Pod.
- Cấu hình Ingress rule cho một domain, và biết vì sao Ingress "không chạy" nếu thiếu controller.

## Ôn lại nhanh

Bài trước bạn đã đi qua chuỗi Deployment → ReplicaSet → Pod, và nhấn mạnh rằng Pod là thứ có thể chết bất cứ lúc nào: bị evict, bị rollout thay thế, IP đổi liên tục. Đó chính là lý do Service tồn tại. Bạn cũng đã biết Pod mang `labels`, và label selector là cách Kubernetes "nhóm" các object lại với nhau. Bài này dùng lại đúng khái niệm đó.

## Lý thuyết

### Vì sao cần Service?

Pod IP là ephemeral. Một Deployment 3 replicas sau vài lần rolling update có thể có 3 Pod với 3 IP hoàn toàn khác. Nếu client hard-code IP Pod thì hệ thống vỡ ngay lần deploy sau.

==Service không trỏ tới Pod cụ thể nào, nó trỏ tới một tập Pod qua label selector.== Service cho bạn một địa chỉ (và một DNS name) ổn định, cộng thêm load balancing ở tầng L4.

### Bốn loại Service

| Type | Truy cập từ đâu | Ghi chú |
| --- | --- | --- |
| `ClusterIP` | Trong cluster | Mặc định. IP ảo, chỉ có ý nghĩa bên trong cluster |
| `NodePort` | Ngoài cluster qua `IP_node:port` | Mở port cố định trên mọi node, dải mặc định 30000-32767 |
| `LoadBalancer` | Ngoài cluster qua LB của cloud | Thực chất là NodePort + xin external load balancer |
| `ExternalName` | Trong cluster | Chỉ là CNAME DNS, không có proxy, không có endpoint |

Một biến thể bạn sẽ gặp nhiều: Service có `clusterIP: None`, gọi là **Headless Service**. Khi đó không có IP ảo, DNS trả về IP của từng Pod, dùng cho StatefulSet hoặc khi client tự load balance.

### Bên dưới: EndpointSlice và kube-proxy

Service không tự forward traffic. Chuỗi thực tế diễn ra như sau:

1. Bạn tạo Service với `selector: app=web`.
2. **EndpointSlice controller** (trong control plane) thấy selector này, tìm các Pod có label `app=web` và đang **Ready**, rồi ghi IP + port của chúng vào object EndpointSlice.
3. Trên mỗi node, **kube-proxy** watch Service và EndpointSlice, lập trình rules vào `iptables` (hoặc IPVS) để biến ClusterIP thành một địa chỉ ảo được DNAT sang một Pod backend.
4. Kernel chọn một backend theo xác suất, nên load balancing là ngẫu nhiên ở tầng kết nối, không phải round-robin chính xác.

Hệ quả thực tế: ClusterIP của bạn là một IP ảo, không ping được, không `curl` được từ ngoài cluster, và không tồn tại trên interface nào cả. Đừng hoang mang khi `ping 10.96.0.10` thất bại.

<details><summary>Đào sâu: port, targetPort, nodePort</summary>

- `port`: port mà Service lắng nghe (client gọi port này).
- `targetPort`: port của container trong Pod. Có thể là số hoặc **tên port** khai báo trong `containerPort`.
- `nodePort`: chỉ dùng với `NodePort`/`LoadBalancer`, port mở trên node.

Nhầm lẫn phổ biến nhất là để `targetPort` khác port mà app thực sự listen. App nghe 8080 nhưng `targetPort: 80` thì Service vẫn có Endpoint nhưng connection bị refused.

</details>

### DNS của Service

Mỗi Service có FQDN dạng `<service>.<namespace>.svc.cluster.local`. Trong cùng namespace, bạn chỉ cần gọi `web`. Khác namespace thì `web.backend`. Đây là cách các app nói chuyện với nhau mà không cần biết IP.

### Ingress: lớp L7 phía trước Service

Service là L4: nó không hiểu HTTP, không đọc được Host header hay path. Ingress giải quyết phần đó:

- Định tuyến theo `host` và `path` (ví dụ `/api` sang service `api`, `/` sang `web`).
- Terminate TLS tại một chỗ duy nhất.
- Gom nhiều domain về một điểm vào (một IP công khai) thay vì mỗi service một LoadBalancer.

> [!IMPORTANT]
> Object Ingress chỉ là **khai báo mong muốn**. Nó không làm gì cả nếu cluster không có **Ingress Controller** (nginx, Traefik, ALB Controller...). Triệu chứng kinh điển: `kubectl get ingress` chạy mãi mà cột ADDRESS để trống.

```mermaid
flowchart LR
    User["🧑<br/>Người dùng"] --> IC["🚪<br/>Ingress Controller"]
    IC --> Svc["🔀<br/>Service ClusterIP"]
    Svc --> P1["📦<br/>Pod A"]
    Svc --> P2["📦<br/>Pod B"]
    KP["⚙️<br/>kube-proxy"] -.-> Svc
    EDS["📋<br/>EndpointSlice"] -.-> KP
```

Luồng đầy đủ: request vào Ingress Controller, controller đọc Host/path, chọn Service đích, kernel DNAT tới một Pod trong EndpointSlice.

## Ví dụ có hướng dẫn

Giả sử bạn đã có Deployment tên `web` với `labels: app: web`, container listen port `8080`.

**Bước 1: Service ClusterIP**

```yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web        # phải khớp labels của Pod
  ports:
    - port: 80          # client gọi web:80
      targetPort: http  # tên port trong container (hoặc 8080)
```

**Bước 2: kiểm chứng backend**

```bash
kubectl get pods -l app=web --show-labels
kubectl get endpointslices -l kubernetes.io/service-name=web
```

Nếu EndpointSlice rỗng: hoặc selector sai, hoặc Pod chưa Ready. Đây là bước debug số một của mọi lỗi "Service không kết nối được".

**Bước 3: Ingress cho domain**

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  rules:
    - host: web.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
```

`pathType: Prefix` khớp theo tiền tố đường dẫn, `Exact` khớp chính xác chuỗi. Thiếu `pathType` thì API server từ chối.

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

**Bài 1.** Viết Service `web` type `NodePort`, `port: 80`, `targetPort: 8080`, nodePort `30080`. Sau đó kiểm tra bằng `curl http://<IP-node>:30080`.

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

```yaml
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080
```

Kiểm tra: `kubectl get svc web` phải thấy `80:30080/TCP`. Nếu connection refused, kiểm tra tiếp `kubectl get endpointslices` và xem app có thực sự listen `0.0.0.0:8080` không (listen `127.0.0.1` là lỗi rất hay gặp).
</details>

**Bài 2.** Không muốn mở NodePort, bạn muốn test nhanh từ laptop. Dùng lệnh nào?

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

```bash
kubectl port-forward svc/web 8080:80
# rồi: curl http://localhost:8080
```

`port-forward` đi qua API server và kubelet, không cần NodePort, rất tiện cho debug. Nhớ là nó chỉ dùng để dev/test, không phải giải pháp cho production.
</details>

**Bài 3.** Bạn apply Ingress ở trên nhưng `kubectl get ingress` mãi không có ADDRESS. Nêu 2 nguyên nhân và cách kiểm tra.

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

1. Chưa cài Ingress Controller: `kubectl get pods -A | grep -i ingress`. Không có pod nào thì không có gì xử lý Ingress.
2. Controller có nhưng không watch IngressClass này, hoặc bạn thiếu `ingressClassName`. Kiểm tra `kubectl get ingressclass` và `kubectl describe ingress web` để xem event.
3. Nếu chỉ test trên máy local với domain giả, thêm dòng `127.0.0.1 web.example.com` vào `/etc/hosts` hoặc dùng `curl -H "Host: web.example.com" http://<IP-controller>`.
</details>

## Góc chuyên gia

- **Selector sai là lỗi phổ biến nhất.** Luôn chạy `kubectl get endpointslices -l kubernetes.io/service-name=<svc>` sau khi tạo Service. Rỗng nghĩa là chưa có backend.
- **Readiness probe quyết định Endpoint.** Pod Running nhưng chưa Ready thì không được thêm vào EndpointSlice, và đó thường là hành vi bạn muốn.
- **Đừng dùng `NodePort` cho production.** Nó lộ port trên mọi node và dải port là tài nguyên dùng chung của cluster.
- **Một Ingress, nhiều domain.** Gộp rule vào cùng object để giảm số IP công khai và dễ quản TLS.
- **Headless Service không load balance.** Nếu client cần tự chọn backend (gRPC, database), dùng `clusterIP: None`.
- **Ingress Controller cũng là workload.** Nó là Deployment/DaemonSet trong cluster, nên nó cũng cần Service, cũng có thể bị OOM.

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

Chốt lại: Service cho bạn địa chỉ ổn định ở L4, chọn backend qua label selector, backend được ghi vào EndpointSlice và kube-proxy biến ClusterIP thành rules DNAT trên từng node. Ingress thêm lớp L7 để định tuyến theo host/path, nhưng phải có Ingress Controller mới sống.

Vài chỗ trống để bạn tự kiểm tra: Service mặc định là {{c1::ClusterIP}}, chỉ truy cập được từ trong cluster. `NodePort` mở port trên {{c2::mọi node}} trong dải mặc định {{c3::30000-32767}}. Backend của Service được ghi vào object {{c4::EndpointSlice}}, và Ingress chỉ hoạt động khi có {{c5::Ingress Controller}}.

Bài tiếp theo là checkpoint của Module 1: bạn sẽ ghép kiến trúc control plane, vòng reconcile của Deployment và cách expose ở bài này thành một sơ đồ luồng hoàn chỉnh, kèm bài tập tổng hợp tự triển khai một app từ `kubectl apply` tới khi truy cập được qua domain.

```flashcard
[
  { "front": "Service type mặc định là gì?", "back": "ClusterIP, chỉ truy cập được từ bên trong cluster." },
  { "front": "ClusterIP có phải IP thật gán cho một Pod không?", "back": "Không. Đó là IP ảo do kube-proxy lập trình rule iptables/IPVS trên node." },
  { "front": "Service chọn Pod backend bằng cơ chế nào?", "back": "Bằng trường selector khớp với labels của Pod." },
  { "front": "Backend IP của Service được ghi vào object nào?", "back": "EndpointSlice, do EndpointSlice controller cập nhật." },
  { "front": "Pod ở trạng thái nào mới được thêm vào EndpointSlice?", "back": "Pod đã Ready (qua readiness probe)." },
  { "front": "Dải nodePort mặc định là bao nhiêu?", "back": "30000 đến 32767." },
  { "front": "Phân biệt port và targetPort của Service?", "back": "port là port Service lắng nghe, targetPort là port container trong Pod." },
  { "front": "Service type LoadBalancer thực chất làm gì?", "back": "Cấp NodePort rồi xin external load balancer từ cloud provider." },
  { "front": "Headless Service là gì?", "back": "Service có clusterIP: None, DNS trả về IP của từng Pod, không load balance." },
  { "front": "FQDN đầy đủ của một Service?", "back": "<service>.<namespace>.svc.cluster.local" },
  { "front": "Ingress hoạt động ở tầng nào và dựa vào gì để định tuyến?", "back": "Tầng L7 HTTP/HTTPS, dựa vào host và path." },
  { "front": "Vì sao kubectl get ingress mãi không có ADDRESS?", "back": "Cluster chưa có Ingress Controller hoặc thiếu ingressClassName." },
  { "front": "pathType Prefix khác Exact thế nào?", "back": "Prefix khớp theo tiền tố đường dẫn, Exact khớp chính xác toàn bộ chuỗi." }
]
```
