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

Đào sâu: port, targetPort, nodePort
  • 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.

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.

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

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

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

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.

Gợi ý và đáp án
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).

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

Gợi ý và đáp án
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.

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.

Gợi ý và đáp án
  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>.

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.

[
  { "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." }
]