# 🚪 Ingress và Ingress controller

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

Sau bài này, bạn phải làm được những việc sau, và làm được trong khoảng 8 đến 10 phút nếu gặp trong đề thi:

- Viết Ingress rule route theo **host** và theo **path** tới hai Service khác nhau.
- Kiểm tra Ingress đã được controller nhận, và test đúng bằng `curl` với header `Host`.
- Phân biệt và xử lý hai lỗi test hay gặp nhất: **404** (không match rule) và **502/503** (backend có vấn đề).

### Ôn nhanh 30 giây

Bài trước bạn đã chốt: Service có ClusterIP và DNS name dạng `<svc>.<ns>.svc.cluster.local`, và nếu Service không có endpoint thì kết nối sẽ fail. Ingress **đứng trên** Service: nó không thay thế Service, nó chỉ quyết định request HTTP nào được đẩy vào Service nào.

> [!NOTE]
> Ingress là object khai báo rule ở layer 7 (HTTP/HTTPS). Bản thân object Ingress **không làm gì cả**. Phải có Ingress controller (thường là `ingress-nginx`) đọc object đó rồi cấu hình reverse proxy. Không có controller thì `kubectl get ingress` vẫn ra resource, nhưng không có traffic nào được route.

Luồng traffic đầy đủ trông như sau:

```mermaid
flowchart LR
  Client["👤<br/>Client gửi Host header"] --> IC["🚪<br/>Ingress controller<br/>NodePort 30080"]
  IC --> Rule["📜<br/>Match rule<br/>host + path"]
  Rule --> Svc["🔗<br/>Service ClusterIP"]
  Svc --> EP["📦<br/>Pod endpoints"]
  Rule -.->|không khớp rule| E404["❌ 404 Not Found"]
  Svc -.->|không có endpoint| E503["⚠️ 502 / 503"]
```

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

Bối cảnh: namespace `shop` có hai app, `web` (nginx, port 80) và `api` (http-echo, port 5678). Yêu cầu: `shop.example.com/` vào `web`, `shop.example.com/api` vào `api`.

**Bước 1: Kiểm tra cluster đã có Ingress controller chưa.** Đây là bước nhiều người bỏ qua rồi ngồi sửa YAML 15 phút.

```bash
kubectl get ingressclass
kubectl get pods -A | grep -i ingress
```

Có `nginx` trong danh sách ingressclass là dùng được. Không có thì phải cài controller trước, nhưng trong đề CKA controller gần như luôn được cài sẵn.

**Bước 2: Tạo workload và Service.**

```bash
kubectl create ns shop
kubectl -n shop create deploy web --image=nginx:1.25 --port=80
kubectl -n shop expose deploy web --port=80 --name=web

kubectl -n shop create deploy api --image=hashicorp/http-echo:1.0
kubectl -n shop expose deploy api --port=80 --target-port=5678 --name=api
```

Chú ý: `--port=80` là port của Service, `--target-port=5678` là port container đang listen. Nhầm chỗ này chính là nguồn gốc lỗi 502/503 ở phần sau.

**Bước 3: Sinh YAML bằng `kubectl create ingress` thay vì gõ tay.** Cú pháp `--rule="host/path=service:port"`, path kết thúc bằng `*` thì thành `pathType: Prefix`.

```bash
kubectl -n shop create ingress shop-ing \
  --class=nginx \
  --rule="shop.example.com/*=web:80" \
  --rule="shop.example.com/api*=api:80" \
  --dry-run=client -o yaml > shop-ing.yaml
```

**Bước 4: Đọc lại và chỉnh YAML** (thêm TLS, đổi port number sang port name, sửa path nếu cần):

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

```bash
kubectl apply -f shop-ing.yaml
kubectl -n shop get ingress
kubectl -n shop describe ingress shop-ing
```

Trong `describe`, phần `Rules:` phải hiện đúng Host, Path và Backends. Đó là bằng chứng rule đã vào đúng chỗ.

**Bước 5: Test bằng `curl` với header `Host`.**

```bash
kubectl -n ingress-nginx get svc ingress-nginx-controller
# ví dụ trả về NodePort 30080

curl -s -o /dev/null -w "%{http_code}\n" -H "Host: shop.example.com" http://10.0.0.10:30080/
curl -s -H "Host: shop.example.com" http://10.0.0.10:30080/api
```

Không tiện dùng NodePort thì dùng port-forward, miễn giữ nguyên header `Host`:

```bash
kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 8080:80
curl -s -H "Host: shop.example.com" http://localhost:8080/api
```

**Bước 6: Xử lý lỗi khi test.**

- Ra **404**: controller nhận request nhưng không rule nào khớp. Kiểm tra lại `Host` bạn gửi có đúng bằng `host` trong YAML, và path có khớp `pathType` không.
- Ra **503**: rule đã khớp, nhưng Service backend không có endpoint. Chạy `kubectl -n shop get endpoints web api`. Nếu ENDPOINTS là `<none>` thì sai `targetPort`, hoặc pod chưa Ready.

> [!TIP]
> Trong đề thi, thứ tự path trong YAML không quan trọng. Nginx Ingress luôn chọn **prefix dài nhất** trước, nên `/api` thắng `/` mà bạn không cần sắp xếp gì.

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

Phần này là phần chính. Bạn hãy tự làm trước, chỉ mở phần giải sau khi đã thử ít nhất một lần.

### Bài 1: Hai host, một cluster (4 phút)

Tạo Ingress tên `multi-host` trong namespace `shop` với:

- `web.example.com` route mọi path vào Service `web` port 80.
- `api.example.com` route mọi path vào Service `api` port 80.

```bash
kubectl -n shop create ingress multi-host \
  --class=nginx \
  --rule="web.example.com/*=web:80" \
  --rule="api.example.com/*=api:80" \
  --dry-run=client -o yaml | kubectl apply -f -
```

<details><summary>Xem cách kiểm tra kết quả</summary>

```bash
kubectl -n shop describe ingress multi-host
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: web.example.com" http://<NODE_IP>:30080/
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: api.example.com" http://<NODE_IP>:30080/
```

Cả hai phải trả `200`. Nếu một cái trả 404, gần như chắc chắn bạn gõ sai host trong lệnh `curl` hoặc trong `--rule`.

</details>

### Bài 2: Sửa lỗi 404 do `pathType` quá chặt (3 phút)

Apply manifest dưới đây, sau đó test `curl -H "Host: shop.example.com" .../api/v1/users` và giải thích tại sao ra 404.

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: exact-demo
  namespace: shop
spec:
  ingressClassName: nginx
  rules:
  - host: shop.example.com
    http:
      paths:
      - path: /api
        pathType: Exact
        backend:
          service:
            name: api
            port:
              number: 80
```

<details><summary>Xem lời giải</summary>

`pathType: Exact` chỉ khớp chính xác chuỗi `/api`, không khớp `/api/v1/users`. Sửa nhanh nhất:

```bash
kubectl -n shop patch ingress exact-demo --type=json \
  -p '[{"op":"replace","path":"/spec/rules/0/http/paths/0/pathType","value":"Prefix"}]'
```

Test lại, phải ra `200`. Đây là bẫy số một của dạng bài Ingress: đề cho path kiểu `/api` rồi bảo "mọi request dưới `/api`", bạn phải dùng `Prefix`.

</details>

### Bài 3: Sửa lỗi 502 do sai `targetPort` (3 phút)

```bash
kubectl -n shop expose deploy api --port=80 --target-port=8080 --name=api-broken
```

Tạo Ingress trỏ vào `api-broken` rồi test. Bạn sẽ thấy `502` hoặc `503`. Chẩn đoán theo đúng thứ tự sau:

```bash
kubectl -n shop get endpoints api-broken
```

<details><summary>Xem lời giải</summary>

`ENDPOINTS` sẽ là `<none>` vì `targetPort: 8080` không khớp port container đang listen (5678). Không có endpoint thì controller không có gì để proxy tới.

```bash
kubectl -n shop patch svc api-broken -p '{"spec":{"ports":[{"port":80,"targetPort":5678}]}}'
kubectl -n shop get endpoints api-broken
```

Đợi vài giây, endpoint xuất hiện, test lại ra `200`.

Bài học: **404 là lỗi ở tầng rule, 502/503 là lỗi ở tầng backend.** Nhìn mã lỗi là biết phải đi kiểm tra phía nào.

</details>

### Bài 4: Đề mini bấm giờ 6 phút

> Trong namespace `shop`, tạo Ingress tên `timebox` route `shop.example.com/api` (và mọi path con của nó) tới Service `api` port 80, đồng thời route tất cả host khác vào `web`. Kiểm tra bằng `curl` và chứng minh cả hai rule đều hoạt động.

<details><summary>Xem gợi ý và lời giải</summary>

Gợi ý: rule không có `host` sẽ đóng vai trò catch-all.

```bash
kubectl -n shop create ingress timebox --class=nginx \
  --rule="shop.example.com/api*=api:80" \
  --rule="*=web:80" \
  --dry-run=client -o yaml | kubectl apply -f -
```

Kiểm tra:

```bash
kubectl -n shop describe ingress timebox
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: shop.example.com" http://<NODE_IP>:30080/api
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: batky.example.com" http://<NODE_IP>:30080/
```

</details>

## Lỗi thường gặp và troubleshoot

| Triệu chứng | Nguyên nhân hay gặp | Lệnh kiểm tra |
| --- | --- | --- |
| 404 từ controller | Host gửi lên không khớp `host` trong rule, hoặc `pathType: Exact` quá chặt, hoặc thiếu `ingressClassName` | `kubectl -n shop describe ingress <name>` |
| 404 dù YAML đúng | Rule nằm ở namespace khác với Service, hoặc controller không phải class bạn khai | `kubectl get ingressclass`, `kubectl -n shop get ingress` |
| 503 | Service backend không có endpoint (sai `targetPort`, pod chưa Ready, sai tên Service) | `kubectl -n shop get endpoints <svc>` |
| 502 | Controller nối được tới IP nhưng backend đóng kết nối, sai `targetPort`, hoặc app không nói HTTP | `kubectl -n shop exec deploy/web -- curl -s <cluster-ip>:80` |
| Connection refused khi curl | Sai NodePort, controller Service không tồn tại, hoặc firewall node | `kubectl -n ingress-nginx get svc ingress-nginx-controller` |
| Ingress có mà `describe` không có `Rules` | Rule sai cấu trúc, backend thiếu `port` | `kubectl -n shop get ingress -o yaml` |

Thêm một mẹo chẩn đoán: log của controller thường ghi rõ request nào bị 404.

```bash
kubectl -n ingress-nginx logs deploy/ingress-nginx-controller --tail=50
```

==Không có Ingress controller thì Ingress chỉ là file YAML vô hồn.== Luôn kiểm tra controller và ingressclass trước khi sửa manifest.

## Góc chuyên gia

- Trong đề CKA, controller thường đã cài sẵn. Việc của bạn là **viết rule đúng**, không phải cài lại controller. Đừng mất thời gian debug controller.
- Dùng `kubectl create ingress ... --dry-run=client -o yaml` để có skeleton, sau đó `edit` hoặc `apply`. Gõ tay toàn bộ manifest Ingress rất dễ sai indent ở tầng `paths`.
- Nhớ quy tắc dấu `*`: có `*` thì `Prefix` (và dấu `*` bị cắt), không có `*` thì `Exact`. ==Đây là bẫy số một của dạng bài này.==
- Nếu đề yêu cầu backend dùng **port name** thay vì số, sửa thành `port: { name: http }` và đảm bảo Service có `name: http` trong `spec.ports`.
- Đặt namespace mặc định cho context để khỏi gõ `-n` mỗi lệnh: `kubectl config set-context --current --namespace=shop`.
- Test nhanh bằng một dòng, đỡ đọc output dài: `curl -s -o /dev/null -w "%{http_code}\n" -H "Host: ..." http://...`
- Chỉ thêm annotation `nginx.ingress.kubernetes.io/rewrite-target` khi đề nói rõ yêu cầu rewrite. Thêm khi không được yêu cầu là tự tạo bug.
- Quy tắc thời gian: task Ingress đơn giản nên xong trong 6 đến 8 phút. Quá 8 phút thì ghi chú lại, chuyển task khác, quay lại sau. Một task 4% không đáng để mất 15 phút.

## Tóm tắt và bài tiếp theo

Chốt lại 4 dòng, đúng kiểu mình hay ghi ra giấy nháp:

- Ingress = rule L7 theo host + path, cần Ingress controller mới có tác dụng.
- `apiVersion: networking.k8s.io/v1`, chọn controller bằng `ingressClassName`, backend trỏ tới Service cùng namespace.
- `pathType` chỉ có 3 giá trị: `Prefix`, `Exact`, `ImplementationSpecific`. Mặc định khi tạo bằng `kubectl` là `Exact` nếu path không có `*`.
- 404 = rule không khớp. 502/503 = backend không có endpoint hoặc sai port.

Điền thử trước khi xem đáp án:

```cloze
1. Ingress object chỉ có hiệu lực khi cluster đang chạy một {{c1::Ingress controller}}.
2. `kubectl create ingress` sinh ra `pathType` {{c2::Prefix}} khi path kết thúc bằng `*`, và {{c3::Exact}} khi path không có `*`.
3. Mã 404 từ Ingress nghĩa là request không khớp {{c4::host hoặc path trong rules}}.
4. Mã 502 hoặc 503 nghĩa là rule đã khớp nhưng {{c5::Service backend không có endpoint hoặc sai targetPort}}.
5. Lệnh kiểm tra nhanh backend có endpoint hay không là `kubectl get {{c6::endpoints}} <service>`.
```

Ôn lại bằng bộ flashcard dưới đây, mỗi ngày một lượt là đủ nhớ:

```flashcard
[
  { "front": "Ingress hoạt động ở layer nào và cho giao thức nào?", "back": "Layer 7, chỉ HTTP/HTTPS (khác Service là layer 4)." },
  { "front": "Điều kiện bắt buộc để một Ingress object có tác dụng?", "back": "Cluster phải có Ingress controller đang chạy và nhận đúng ingressClassName." },
  { "front": "apiVersion hiện tại của Ingress?", "back": "networking.k8s.io/v1 (từ Kubernetes 1.19)." },
  { "front": "Field nào chọn Ingress controller cho một Ingress?", "back": "spec.ingressClassName." },
  { "front": "Ba giá trị hợp lệ của pathType?", "back": "Prefix, Exact, ImplementationSpecific." },
  { "front": "`--rule=\"foo.com/path*=svc:80\"` tạo ra pathType gì?", "back": "Prefix, với path là /path (dấu * bị cắt)." },
  { "front": "`--rule=\"foo.com/path=svc:80\"` (không có dấu *) tạo ra pathType gì?", "back": "Exact, chỉ khớp đúng chuỗi /path." },
  { "front": "Backend của Ingress trỏ tới đối tượng nào?", "back": "Một Service cùng namespace, chỉ định port bằng number hoặc name." },
  { "front": "Ingress trả 404 thường do nguyên nhân gì?", "back": "Host hoặc path không khớp rule, pathType quá chặt, hoặc thiếu ingressClassName." },
  { "front": "Ingress trả 503 thường do nguyên nhân gì?", "back": "Service backend không có endpoint (sai targetPort, pod chưa Ready, sai tên Service)." },
  { "front": "Lệnh kiểm tra Service có endpoint hay không?", "back": "kubectl get endpoints <svc>, hoặc kubectl get endpointslices." },
  { "front": "Test Ingress bằng curl như thế nào?", "back": "Gửi header Host đúng bằng host trong rule tới địa chỉ controller: curl -H \"Host: shop.example.com\" http://<node-ip>:<nodeport>/" },
  { "front": "spec.tls của Ingress gồm những field nào?", "back": "Danh sách hosts và secretName trỏ tới Secret type kubernetes.io/tls." },
  { "front": "Khi nhiều path cùng khớp, Ingress controller chọn path nào?", "back": "Path có prefix dài nhất (longest match), không phụ thuộc thứ tự trong YAML." }
]
```

**Bài tiếp theo: NetworkPolicy và CNI.** Bạn đã biết traffic từ ngoài vào pod đi qua những đâu. Bài sau chúng ta sẽ chặn traffic đó ở tầng mạng: viết NetworkPolicy theo label, hiểu vì sao mặc định mọi pod nói chuyện được với nhau, và vì sao policy đúng mà vẫn "không ăn" khi CNI không hỗ trợ.
