Bài 04

09 tháng 7, 2026 · 30 phút đọc

Bài 04 — Service & Namespace: địa chỉ cố định & vách ngăn

Hai bài trước để lại một mâu thuẫn nhức nhối. Bài 02 dặn: "Pod mau hỏng, đừng bám IP." Bài 03 cho bạn 3 bản replica — tức 3 IP khác nhau, và mỗi lần redeploy là đổi hết. Vậy nginx muốn gọi tới php, php muốn gọi tới database — gọi vào đâu? Gọi IP nào trong 3 cái? IP đó mai còn không?

Câu trả lời là Service: một địa chỉ cố định + tên DNS + cân bằng tải, đứng chắn trước đám Pod hay thay đổi. Đây chính là thứ tương đương "DNS auto giữa service" mà bạn đã yêu thích ở chặng Compose — giờ ở quy mô cluster.

Nửa sau bài là Namespace — vách ngăn logic để chia cluster thành nhiều "khu", tránh giẫm chân nhau.


Vấn đề: làm sao gọi một mục tiêu di động?

   Deployment "api"3 Pod:

   Pod-1  IP 10.244.1.5   ← redeploy → đổi thành 10.244.2.9
   Pod-2  IP 10.244.1.6   ← crash → RS tạo Pod mới IP khác
   Pod-3  IP 10.244.3.2   ← scale xuống → biến mất

   frontend muốn gọi "api"... gọi IP nào? 😵
   • Hardcode 1 IP → Pod đó chết là toi
   • Hardcode cả 3 → scale/redeploy là sai hếtTcode logic theo dõi IP → đúng thứ K8s phải lo, không phải bạn

Ta cần một lớp trung gian ổn định: một cái tên, một IP không bao giờ đổi, tự biết hiện có những Pod nào sống và rải request cho chúng. Đó là Service.


Service là gì

Service là một điểm truy cập ổn định (IP + DNS cố định) đứng trước một nhóm Pod, tự cân bằng tải tới các Pod còn sống. Nó tìm Pod của mình bằng selector (label — nhớ Bài 03):

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector:
    app: api               # rót traffic tới mọi Pod có label app=api
  ports:
    - protocol: TCP
      port: 80             # cổng Service lắng nghe
      targetPort: 8080     # cổng container trong Pod
  # type không ghi → mặc định ClusterIP
            ┌──────────────────────────────┐
   gọi  ──► │  api-service                  │   IP cố định + tên DNS
            │  10.96.0.10  (ClusterIP)       │
            └───────────────┬────────────────┘
                            │ load balance (selector app=api)
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
          ┌───────┐    ┌───────┐    ┌───────┐
          │ Pod-1 │    │ Pod-2 │    │ Pod-3 │   IP đổi tùy ý —
          └───────┘    └───────┘    └───────┘   Service tự cập nhật

Service IP gắn với Service suốt đời nó, không đổi dù Pod bên dưới sinh tử thế nào. Pod-2 chết, RS tạo Pod-2′ với IP mới → Service tự nhận ra và cập nhật danh sách. Người gọi không hề hay biết.

Bên trong: Endpoints + kube-proxy

Service không phải phép màu — nó là hai mảnh ghép:

   1. Service có selector (app=api)
            │
            ▼
   2. K8s tự lập danh sách ENDPOINTS = IP:port của các Pod khớp selector
      (và CHỈ những Pod đã readiness PASS — nhớ Bài 02!)
            │
            ▼
   3. kube-proxy (Bài 01) trên mỗi node viết luật iptables/IPVS:
      "ai gọi ClusterIP 10.96.0.10 → chuyển ngẫu nhiên tới 1 trong các endpoint"
kubectl get endpoints api-service     # xem Service đang trỏ tới IP Pod nào

💡 Mẹo debug vàng: Service không truy cập được? Chạy kubectl get endpoints <svc>. ENDPOINTS rỗng = selector không khớp label Pod nào (hoặc Pod chưa readiness PASS). Đây là nguyên nhân số 1 của lỗi "Service không kết nối được".


DNS — gọi nhau bằng tên (CoreDNS từ Bài 01)

Đây là phần "magic" giống Compose nhất. Mỗi Service được CoreDNS cấp một tên gọi được:

   Cùng namespace:        api-service
   Khác namespace:        api-service.<namespace>
   Tên đầy đủ (FQDN):     api-service.<namespace>.svc.cluster.local

   Trong code (giống hệt chặng Compose):
       $host = "api-service";   // gọi tên Service, không gọi IP Pod
       $port = 80;

→ Bạn không bao giờ cần biết IP. Gọi tên Service, CoreDNS phân giải ra ClusterIP, kube-proxy rải tới Pod. Đúng tinh thần "dùng service name làm hostname" của Compose, nhưng giờ qua nhiều máy.


4 loại Service — chọn đúng cửa

Khác biệt cốt lõi: ai được phép gọi, và gọi từ đâu.

   ClusterIP    → chỉ TRONG cluster gi được          (mặc định)
   NodePort     → mở 1 cổng trên MỌI node, ngoài gi vào được
   LoadBalancer → xin IP public từ cloud / MetalLB
   ExternalName → bí danh DNS trỏ ra dịch vụ NGOÀI cluster

ClusterIP — nội bộ (mặc định)

IP ảo chỉ truy cập được từ bên trong cluster. Dùng cho giao tiếp service-to-service: frontend → api → database. Pod gọi nhau qua tên Service. 90% Service của bạn là loại này.

spec:
  type: ClusterIP        # hoặc bỏ trống — đây là mặc định

⚠️ Khi không có ClusterIP, Pod vẫn có thể gọi nhau bằng IP Pod hoặc DNS tên Pod — nhưng cả hai phức tạp và không ổn định (IP đổi, không cân bằng tải). ClusterIP là cách đúng.

NodePort — mở cổng ra ngoài

Mở cùng một cổng (mặc định dải 30000-32767) trên mọi node. Gõ <IP-bất-kỳ-node>:<NodePort> là vào được, K8s tự định tuyến tới đúng Pod dù Pod nằm ở node khác.

spec:
  type: NodePort
  selector: { app: api }
  ports:
    - port: 80           # ClusterIP nội bộ vẫn có
      targetPort: 8080   # cổng container
      nodePort: 30080    # cổng mở trên node (không ghi → tự chọn trong dải)
   Client ──► <NodeIP>:30080 ──► (ClusterIP) ──► load balance ──► Pod

→ Hợp để test/demo, hoặc khi có sẵn LB ngoài. Không đẹp cho production trực tiếp: cổng cao kỳ cục, lộ IP node, không có domain/HTTPS chuẩn. Production thường để Ingress (xem dưới) đứng trước.

LoadBalancer — IP public thật

spec:
  type: LoadBalancer

Tạo Service loại này, K8s xin nhà cung cấp cloud một IP public + bộ cân bằng tải, rồi forward về Pod (thường Round Robin).

⚠️ Chỉ hoạt động sẵn trên cloud (nhờ Cloud Controller Manager — Bài 01). On-prem không có ai cấp IP → Service kẹt <pending> mãi. Lời giải on-prem: MetalLB (ta cài ở Bài 05), hoặc HAProxy/Ingress Controller phía trước.

ExternalName — bí danh ra ngoài

Không trỏ tới Pod nào cả; nó là một bản ghi DNS trỏ ra dịch vụ ngoài cluster:

spec:
  type: ExternalName
  externalName: api.example.com

→ Pod gọi my-external-api, CoreDNS chuyển tiếp thành api.example.com. Hữu ích khi DB hoặc API thật nằm ngoài cluster mà bạn vẫn muốn code gọi qua một cái tên nội bộ thống nhất (đổi backend chỉ cần sửa Service, không sửa code).

Headless Service — khi cần từng Pod

spec:
  clusterIP: None        # "headless" — không cấp IP ảo
  selector: { app: db }

clusterIP: None khiến DNS trả về IP của từng Pod thay vì 1 IP ảo cân bằng tải. Dùng cho StatefulSet (Bài 03) — vd cụm DB, nơi client cần gọi đích danh từng replica (primary/replica), không phải "một Pod bất kỳ".


Ingress — cánh cổng L7 (giới thiệu, làm tay ở Bài 05)

NodePort/LoadBalancer mở cổng ở tầng 4 (TCP). Nhưng web thật cần định tuyến theo domain và đường dẫn — đó là Ingress (tầng 7, HTTP):

                     Internet
                        │
                        ▼
              ┌──────────────────┐
              │  Ingress (L7)     │   1 IP, định tuyến theo host/path:
              └────────┬─────────┘
        ┌──────────────┼───────────────┐
        ▼              ▼               ▼
   app.example.com  api.example.com  app.example.com/admin
        │              │               │
   web-service     api-service     admin-service

Một Ingress có thể gom nhiều domain/path về một IP duy nhất, kèm TLS/HTTPS — thay vì mỗi service một LoadBalancer tốn kém. ⚠️ Ingress chỉ là luật; phải có Ingress Controller (nginx, traefik...) thực thi nó. Ta cài và chạy thật ở capstone Bài 05.

Tương quan để nhớ: Service lo tầng 4 trong cluster; Ingress lo tầng 7 từ ngoài vào. Ingress gần như luôn trỏ tới các Service ClusterIP phía sau.


Khóa cửa giữa các Pod — NetworkPolicy (biết để dùng sau)

Mặc định trong K8s mọi Pod nói chuyện được với mọi Pod — phẳng lì, không firewall. NetworkPolicy là tường lửa tầng Pod: "chỉ Pod app=frontend được gọi app=db, còn lại cấm". ⚠️ NetworkPolicy chỉ có tác dụng nếu CNI hỗ trợ (Calico, Cilium — Flannel cơ bản thì không). Đây là một chủ đề bảo mật quan trọng khi bạn lên prod; bài này chỉ cắm mốc để bạn biết nó tồn tại.


Namespace — vách ngăn trong cluster

Namespace chia một cluster thành nhiều "không gian" logic. Mỗi namespace chứa Pod, Service, Deployment... riêng, gần như không thấy nhau.

   ┌──────────────── 1 CLUSTER ────────────────┐
   │  ns: dev          ns: prod      ns: team-a │
   │  ┌──────────┐    ┌──────────┐  ┌─────────┐ │
   │  │ api, db,  │    │ api, db,  │  │ ...     │ │
   │  │ web...    │    │ web...    │  │         │ │
   │  └──────────┘    └──────────┘  └─────────┘ │
   │   (cùng tên "api" được phép — khác ns)     │
   └────────────────────────────────────────────┘

Quy tắc:

  • Trong cùng namespace, tên tài nguyên không được trùng. Khác namespace thì trùng tên thoải mái (apidevapiprod là hai thứ khác nhau).
  • Không chỉ định → tài nguyên vào namespace default.
  • Pod trong cùng namespace gọi nhau bằng tên Service ngắn (api-service); gọi sang namespace khác phải thêm tên ns: api-service.prod.

Các namespace có sẵn

   default          → nơi đồ của bạn vào nếu không chỉ định
   kube-system      → ruột của K8s (API Server, CoreDNS, kube-proxy...) — ĐỪNG đụng
   kube-public      → tài nguyên đọc công khai (hiếm dùng)
   kube-node-lease  → "nhịp tim" báo node còn sống

Làm việc với namespace

kubectl create namespace dev
kubectl get namespaces

# Triển khai vào namespace (qua YAML)
#   metadata: { namespace: dev }   ← thêm dòng này
# hoặc qua cờ -n:
kubectl apply -f deploy.yaml -n dev

kubectl get pods -n dev               # xem Pod trong 1 ns
kubectl get all -n dev                # mọi tài nguyên trong ns
kubectl get pods --all-namespaces     # xem khắp ns

# Đặt namespace mặc định cho context (đỡ gõ -n mãi)
kubectl config set-context --current --namespace=dev

ResourceQuota — chia khẩu phần

Namespace còn dùng để giới hạn tài nguyên từng nhóm, tránh team dev ăn hết RAM của prod:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    pods: "20"          # ns dev tối đa 20 Pod

→ Đây là cách K8s biến "1 cluster vật lý" thành "nhiều môi trường có ranh giới" — gần với multi-environment của Compose (Bài Compose), nhưng cứng và an toàn hơn nhiều.


RBAC — ai được phép làm gì

Namespace chia tài nguyên. Nhưng còn câu hỏi an ninh: ai được phép đụng vào tài nguyên nào? Dev junior có nên xóa được Pod prod không? App có nên đọc được mọi Secret không? Đây là việc của RBAC (Role-Based Access Control) — thứ Bài 01 liệt kê và Dashboard ở Bài 05 cần tới, giờ ta mở ra.

RBAC ghép 4 mảnh:

   AI (chủ thể)            ĐƯỢC LÀM GÌ (quyền)         GẮN VỚI NHAU
   ────────────            ──────────────────          ────────────
   User / Group            Role        (trong 1 ns)    RoleBinding
   ServiceAccount (app)    ClusterRole (toàn cluster)  ClusterRoleBinding
  • Role = bộ quyền trong một namespace (vd: "được get/list Pod trong ns dev").
  • ClusterRole = bộ quyền toàn cluster (vd: "được xem node", "được mọi thứ").
  • RoleBinding / ClusterRoleBinding = dán một Role cho một chủ thể (gắn quyền vào người/app).
  • ServiceAccount = "danh tính" của app (Pod) — khác với User là danh tính người.

Ví dụ: dev chỉ được xem Pod trong namespace dev

# Role: định nghĩa QUYỀN (chưa gắn cho ai)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: dev
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]      # chỉ ĐỌC, không xóa/sửa
---
# RoleBinding: GẮN Role đó cho user "an"
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: an-pod-reader
  namespace: dev
subjects:
  - kind: User
    name: an                              # user "an"
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

→ Sau khi apply, an chỉ get/list/watch Pod trong ns dev. Gõ kubectl delete pod → bị từ chối "forbidden". Gõ trong ns prod → cũng forbidden (Role chỉ phạm vi dev).

ServiceAccount — danh tính cho app

Mỗi Pod chạy dưới một ServiceAccount (mặc định là default của namespace). Nếu app cần gọi API Server (vd controller tự viết, CI/CD agent), cấp cho nó một SA với đúng quyền tối thiểu:

apiVersion: v1
kind: ServiceAccount
metadata: { name: ci-bot, namespace: dev }
---
# rồi gắn Role cho ci-bot bằng RoleBinding (subjects: kind: ServiceAccount)
# và khai trong Pod:  spec.serviceAccountName: ci-bot

⚠️ Nguyên tắc tối thiểu quyền (least privilege). Đây chính là cái bẫy ở Bài 05: tạo admin Dashboard, người ta hay gán thẳng ClusterRole: cluster-admin (toàn quyền) cho nhanh — tiện nhưng nguy hiểm, lộ token đó là lộ cả cluster. Production: cấp đúng quyền cần, không hơn.

# Kiểm tra mình (hoặc SA) có quyền gì không — cực hữu ích khi debug "forbidden"
kubectl auth can-i delete pods -n prod
kubectl auth can-i list secrets --as=system:serviceaccount:dev:ci-bot -n dev

💡 RBAC bật mặc định từ lâu trên cluster thật. Lỗi Error from server (Forbidden) gần như luôn là RBAC — dùng kubectl auth can-i để soi, đừng đoán.


🚀 Lab — Service, DNS, Namespace

Dùng cluster kind.

1. Deploy + ClusterIP + thử DNS

kubectl create deployment api --image=nginx:1.25 --replicas=3
kubectl expose deployment api --port=80 --target-port=80   # tạo ClusterIP

kubectl get svc                       # api: ClusterIP 10.96.x.x
kubectl get endpoints api             # thấy 3 IP Pod — đúng 3 replica

# Thử gọi BẰNG TÊN từ 1 Pod debug:
kubectl run debug --rm -it --image=busybox -- sh
#  Trong Pod debug, gõ:
#    nslookup api                 → phân giải ra ClusterIP
#    wget -qO- http://api         → trang nginx, qua tên Service!
#    exit

2. Quan sát endpoints tự cập nhật

kubectl get endpoints api             # 3 IP
kubectl scale deployment/api --replicas=5
kubectl get endpoints api             # NGAY LẬP TỨC thành 5 IP — Service tự bám

3. NodePort — gọi từ ngoài

kubectl expose deployment api --name=api-np --type=NodePort --port=80
kubectl get svc api-np                # PORT(S): 80:3xxxx/TCP
# Với kind, lấy IP node: kubectl get nodes -o wide → <NodeIP>:3xxxx

4. Namespace + cross-namespace

kubectl create namespace shop
kubectl create deployment cart --image=nginx -n shop
kubectl expose deployment cart --port=80 -n shop

# Từ namespace default gọi sang shop phải dùng tên đầy đủ:
kubectl run debug --rm -it --image=busybox -- sh
#    wget -qO- http://cart.shop          → OK (ngắn gọn cross-ns)
#    wget -qO- http://cart.shop.svc.cluster.local   → OK (FQDN)
#    wget -qO- http://cart               → FAIL (không cùng ns!)

5. Dọn

kubectl delete deployment api && kubectl delete svc api api-np
kubectl delete namespace shop

→ Khoảnh khắc "à há": kubectl get endpoints cho bạn thấy Service bám theo Pod theo thời gian thực — scale lên 5, endpoints thành 5 ngay. Và nslookup api chứng minh DNS nội bộ hoạt động y như "magic" của Compose, chỉ là ở quy mô cluster.


Câu chuyện thực tế

Một chiều, dev báo: "api gọi database toàn timeout, mà Pod database Running bình thường!". Mọi người soi log database — sạch. Soi log api — chỉ thấy "connection timeout". Loay hoay gần một tiếng.

Tôi gõ đúng một lệnh:

kubectl get endpoints db-service
# ENDPOINTS:  <none>

<none>. Service db-service không trỏ tới Pod nào. Lý do: Pod database mang label app: database, nhưng Service viết selector: { app: db }. Lệch một chữdatabase vs db. Service tồn tại, có ClusterIP, DNS phân giải ngon — nhưng nó rải traffic vào... hư không.

Sửa selector cho khớp, endpoints hiện ra 1 IP Pod ngay, api kết nối được tức thì. Một tiếng debug, lời giải là get endpoints.

Lần khác đắt hơn về tiền: team deploy lên cluster on-prem, đặt Service type: LoadBalancer như quen tay hồi xài GKE. Service kẹt EXTERNAL-IP: <pending> mãi mãi. Họ tưởng cluster hỏng. Thực ra on-prem không có cloud để cấp IP (Bài 01 — không có Cloud Controller Manager). Phải cài MetalLB thì LoadBalancer mới có nghĩa. Mất nửa ngày mới ngộ ra.

Bài học:

  1. Service không kết nối được → kubectl get endpoints TRƯỚC TIÊN. Rỗng = selector lệch label. Đây là lỗi số một, và lời giải mất 5 giây nếu biết nhìn đúng chỗ.

  2. Label phải khớp tuyệt đối. K8s buộc mọi thứ bằng label, sai một ký tự là đứt mạch. Service↔Pod, Deployment↔Pod — đều qua label.

  3. type: LoadBalancer không tự có IP ở on-prem. Kẹt <pending> là thiếu MetalLB, không phải cluster hỏng. Biết kiến trúc (Bài 01) thì chẩn đoán trong 1 phút thay vì nửa ngày.

  4. Gọi cross-namespace phải thêm tên ns. api ở ns khác không gọi bằng tên ngắn được — phải api.<ns>. Bug "tự nhiên không gọi được" hay là do quên ns.

  5. Đừng phơi NodePort thẳng ra production. Cổng cao, lộ IP node, không HTTPS chuẩn. Để Ingress đứng trước (Bài 05).


Pitfalls

  1. Selector Service ≠ label Pod → ENDPOINTS rỗng → Service rải vào hư không. Lỗi phổ biến nhất. Kiểm bằng kubectl get endpoints.

  2. Bám IP Pod thay vì tên Service. IP Pod đổi liên tục; tên Service ổn định. Luôn gọi qua tên.

  3. Gọi cross-namespace bằng tên ngắn. Phải service.<namespace> hoặc FQDN. Tên ngắn chỉ work trong cùng ns.

  4. type: LoadBalancer trên on-prem → kẹt <pending> vì không có cloud cấp IP. Cài MetalLB (Bài 05).

  5. Phơi NodePort ra production. Hợp test, không hợp prod — dùng Ingress + LoadBalancer.

  6. Tên Service có dấu gạch dưới _. DNS có thể không phân giải. Dùng gạch ngang - (đúng như cảnh báo ở chặng Compose).

  7. Quên readiness probe nên endpoints có Pod chưa sẵn sàng. Service rải traffic vào Pod chưa boot xong → lỗi. Endpoints chỉ gồm Pod readiness PASS — nên hãy đặt probe.

  8. Đặt app vào nhầm namespace rồi kubectl get pods không thấy (vì đang ở default). Nhớ -n <ns> hoặc set namespace mặc định cho context.

  9. Xóa namespace = xóa SẠCH mọi thứ trong đó. kubectl delete namespace prod cuốn theo toàn bộ Pod/Service/PVC bên trong. Cực kỳ cẩn thận — như compose down -v nhưng quy mô lớn hơn.

  10. Tưởng Pod khác namespace bị cô lập mạng hoàn toàn. Không — mặc định chúng vẫn gọi nhau được (qua FQDN). Cô lập thật cần NetworkPolicy, không phải namespace.

  11. Gán cluster-admin cho nhanh thay vì cấp quyền tối thiểu. Lộ token đó = lộ cả cluster. Lỗi Forbidden → dùng kubectl auth can-i soi, đừng vá bằng cách cho toàn quyền.


Tóm tắt

  • Service = địa chỉ ổn định (IP + DNS) + cân bằng tải trước nhóm Pod hay đổi. Tìm Pod bằng selector (label).
  • Cơ chế: selector → Endpoints (IP các Pod readiness PASS) → kube-proxy viết iptables/IPVS rải traffic.
  • Debug số 1: kubectl get endpoints <svc> — rỗng nghĩa là selector lệch label.
  • DNS: gọi nhau bằng tên Service (api-service); cross-ns api-service.<ns>; FQDN ....svc.cluster.local. CoreDNS lo việc này.
  • 4 loại: ClusterIP (nội bộ, mặc định), NodePort (mở cổng 30000–32767 trên mọi node), LoadBalancer (IP public — cloud, hoặc MetalLB on-prem), ExternalName (bí danh ra ngoài). Thêm Headless (clusterIP: None) cho StatefulSet.
  • Ingress = định tuyến L7 theo domain/path, gom nhiều service về 1 IP + TLS (cần Ingress Controller — Bài 05).
  • NetworkPolicy = tường lửa tầng Pod (cần CNI hỗ trợ).
  • Namespace = vách ngăn logic; tên không trùng trong cùng ns; default/kube-system/... có sẵn; ResourceQuota chia khẩu phần.
  • RBAC = ai được làm gì: Role/ClusterRole (quyền) + RoleBinding/ClusterRoleBinding (gắn quyền) + ServiceAccount (danh tính app). Theo least-privilege; debug Forbidden bằng kubectl auth can-i.
  • on-prem không có LoadBalancer "miễn phí" — đây là cây cầu dẫn thẳng sang capstone.

Bài sau (Bài 05 — ConfigMap & Secret) lấp một lỗ hổng bạn có thể chưa để ý: tới giờ mọi cấu hình (biến môi trường, mật khẩu DB) vẫn nằm cứng trong YAML hoặc image. Đổi một setting phải build lại image; mật khẩu lỡ commit lên git là lộ. Ta sẽ tách cấu hình ra khỏi code bằng ConfigMap (config thường) và Secret (dữ liệu nhạy cảm) — đúng tinh thần 12-factor, và là tiền đề để cùng một image chạy được ở dev/staging/prod.

Nếu hôm nay bạn nhìn ENDPOINTS rỗng và biết ngay "selector lệch label" — bạn đã debug Service như người có nghề.

Minh Hưng

← Bài trước
Bài 03
Bài tiếp theo →
Bài 05
Zalo tư vấn