Ingress & TLS: một cổng vào, HTTPS tự động
Bài 04 giới thiệu Ingress bằng một sơ đồ và một lời hứa: "chi tiết ở bài sau". Đây là bài sau đó.
Bài 04 giới thiệu Ingress bằng một sơ đồ và một lời hứa: "chi tiết ở bài sau". Đây là bài sau đó.
Tám bài qua bạn đã chạy được app, phơi được ra ngoài bằng NodePort/LoadBalancer. Nhưng chưa có web production nào ra đời với địa chỉ kiểu http://192.168.200.85:31847. Production thật cần: một domain đẹp, HTTPS, nhiều app dùng chung một cổng vào — và cái chứng chỉ HTTPS đó phải tự gia hạn, chứ không phải đánh thức bạn lúc 2 giờ sáng vì nó hết hạn.
Bài này làm ba việc: định tuyến L7 cho ra hồn (host/path/annotation), TLS termination, và cert-manager + Let's Encrypt để chứng chỉ tự xin, tự gia hạn, mãi mãi.
Vấn đề: mỗi app một cổng vào là không xuể
❌ Không Ingress — mỗi service tự phơi ra:
webapp → LoadBalancer → IP 192.168.200.85
api → LoadBalancer → IP 192.168.200.86 ← mỗi IP tốn tiền (cloud)
admin → LoadBalancer → IP 192.168.200.87 / tốn IP LAN (on-prem)
grafana → NodePort → :31847 ← xấu, khó nhớ
→ Cần HTTPS? Tự cài cert vào TỪNG app. 4 app = 4 lần cấu hình TLS.
→ Cert hết hạn (Let's Encrypt: 90 ngày)? Gia hạn tay 4 chỗ.
→ Muốn cty.vn/api và cty.vn/admin về 2 service khác nhau? LoadBalancer
làm ở tầng 4 (TCP) — nó KHÔNG BIẾT đường dẫn HTTP là gì. Bó tay.Vấn đề gốc: LoadBalancer/NodePort làm việc ở tầng 4 — chúng chỉ thấy IP và cổng, không đọc được HTTP. Mà web thật lại cần định tuyến theo domain và đường dẫn — thứ chỉ tồn tại ở tầng 7.
Ingress = luật, Ingress Controller = người thi hành
Đây là điểm nhiều người lẫn, nên khắc sâu ngay:
Ingress một OBJECT K8s chứa LUẬT định tuyến
("cty.vn/api → api-service:80")
→ Chỉ là tờ giấy. Tự nó KHÔNG làm gì cả.
Ingress Controller một Pod thật (nginx/traefik/HAProxy) ĐỌC các luật đó
và thực thi: nhận request, đọc Host/Path, chuyển tiếp.
→ Đây mới là thứ chạy. Không cài nó = Ingress vô dụng.⚠️ Tạo Ingress mà chưa cài Controller → không có gì xảy ra, và K8s cũng không báo lỗi. Pitfall kinh điển của người mới. (Ta đã cài ingress-nginx ở phần capstone — bài sau.)
Internet
│
▼ 1 IP duy nhất (từ MetalLB / cloud LB)
┌──────────────────────┐
│ Ingress Controller │ ← Pod nginx thật, đọc luật
│ (đọc Host + Path) │ + kết thúc TLS tại đây
└──────────┬───────────┘
┌────────────────┼────────────────┐
▼ ▼ ▼
cty.vn/ cty.vn/api admin.cty.vn
│ │ │
web-service api-service admin-service ← toàn ClusterIP (Bài 04)
│ │ │
Pods Pods Pods→ Một IP, một cổng 443, phục vụ vô số app. Và TLS chỉ cần cấu hình một chỗ — tại Ingress Controller.
Định tuyến: host và path
Theo domain (host-based)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-host
namespace: myapp
spec:
ingressClassName: nginx # ← controller nào xử lý (BẮT BUỘC ghi rõ)
rules:
- host: cty.vn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port: { number: 80 }
- host: api.cty.vn # domain khác → service khác
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port: { number: 80 }Theo đường dẫn (path-based)
rules:
- host: cty.vn
http:
paths:
- path: /api # cty.vn/api/... → api-service
pathType: Prefix
backend:
service: { name: api-service, port: { number: 80 } }
- path: /admin # cty.vn/admin/... → admin-service
pathType: Prefix
backend:
service: { name: admin-service, port: { number: 80 } }
- path: / # còn lại → web-service (đặt CUỐI)
pathType: Prefix
backend:
service: { name: web-service, port: { number: 80 } }pathType — đừng chọn bừa
Prefix khớp theo ĐOẠN đường dẫn: /api khớp /api, /api/v1, /api/users
(KHÔNG khớp /apixyz — khớp theo đoạn!)
Exact khớp CHÍNH XÁC: /api chỉ khớp đúng /api
ImplementationSpecific tùy controller diễn giải (tránh — khó đoán)IngressClass — ai xử lý luật này
spec:
ingressClassName: nginx # nginx-ingress xử lý
# ingressClassName: traefik # nếu bạn dùng Traefik⚠️ Cluster có thể chạy nhiều controller (vd nginx cho public, một cái khác cho internal). Không ghi ingressClassName → có thể không controller nào nhận, Ingress nằm im. Luôn ghi rõ.
Annotations — nơi sức mạnh thật nằm
Bản thân object Ingress khá nghèo nàn. Mọi tính năng nâng cao đến từ annotation (mỗi controller có bộ riêng — dưới đây là của ingress-nginx):
metadata:
annotations:
# Ép HTTPS: ai vào http:// sẽ bị chuyển sang https://
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Viết lại đường dẫn: /api/users → gửi tới backend thành /users
nginx.ingress.kubernetes.io/rewrite-target: /$2
# Cho upload file lớn (mặc định nginx chỉ 1MB → lỗi 413!)
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# Timeout cho request chạy lâu
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
# Giới hạn tốc độ — chống lạm dụng (request/giây mỗi IP)
nginx.ingress.kubernetes.io/limit-rps: "10"
# Backend chạy HTTPS (không phải HTTP)
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
# CORS
nginx.ingress.kubernetes.io/enable-cors: "true"rewrite-target — cái bẫy hay gặp nhất
Người mới hay viết /api rồi ngạc nhiên vì backend nhận cả /api/users thay vì /users:
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2 # $2 = nhóm bắt thứ 2
spec:
rules:
- host: cty.vn
http:
paths:
- path: /api(/|$)(.*) # regex: bắt phần sau /api
pathType: ImplementationSpecific
backend:
service: { name: api-service, port: { number: 80 } }→ cty.vn/api/users → backend nhận /users. Không có rewrite, backend nhận nguyên /api/users và thường trả 404.
TLS — HTTPS trong Ingress
TLS được kết thúc (terminate) tại Ingress Controller: client ↔ Ingress là HTTPS mã hóa; Ingress ↔ Pod thường là HTTP trong mạng nội bộ cluster.
Client ══HTTPS (mã hóa)══► Ingress Controller ──HTTP──► Pod
(giải mã ở đây) (mạng nội bộ)Chứng chỉ nằm trong một Secret loại kubernetes.io/tls (nhớ Bài 05 — loại Secret chuyên dụng):
kubectl create secret tls cty-vn-tls \
--cert=fullchain.pem --key=privkey.pem \
-n myappspec:
ingressClassName: nginx
tls:
- hosts:
- cty.vn
- api.cty.vn
secretName: cty-vn-tls # ← Secret chứa cert + key
rules:
- host: cty.vn
...⚠️ Secret TLS phải nằm CÙNG namespace với Ingress. Ingress ở ns myapp không dùng được Secret ở ns default (đúng luật namespaced của Bài 04/05). Lỗi này khiến Ingress im lặng phục vụ cert mặc định (self-signed) → trình duyệt báo đỏ.
cert-manager — chứng chỉ tự xin, tự gia hạn
Tạo Secret TLS bằng tay nghĩa là: xin cert → copy vào cluster → 90 ngày sau làm lại (Let's Encrypt chỉ sống 90 ngày). Quên một lần là web đỏ chót cảnh báo bảo mật.
cert-manager tự động hóa toàn bộ: xin chứng chỉ từ Let's Encrypt, tạo Secret, và tự gia hạn trước khi hết hạn — vĩnh viễn, không cần bạn nhớ.
Cài
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
-n cert-manager --create-namespace \
--set crds.enabled=true💡 cert-manager cài các CRD (Custom Resource Definition) — nó mở rộng K8s bằng loại tài nguyên mới (
Certificate,ClusterIssuer). Đây là cách hệ sinh thái K8s lớn lên: không sửa K8s, mà thêm loại object mới + một controller canh chúng (đúng reconciliation loop của Bài 01).
ClusterIssuer — "ai cấp chứng chỉ cho tôi"
apiVersion: cert-manager.io/v1
kind: ClusterIssuer # ClusterIssuer = dùng được ở MỌI namespace
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@cty.vn # LE gửi cảnh báo hết hạn vào đây
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01: # chứng minh sở hữu domain qua HTTP
ingress:
class: nginx⚠️ Luôn thử với letsencrypt-staging trước (https://acme-staging-v02.api.letsencrypt.org/directory). Let's Encrypt giới hạn 5 lần cấp/domain/tuần — sai cấu hình vài lần trên prod issuer là bạn bị khóa cả tuần. Staging không giới hạn (cert không được trình duyệt tin, nhưng đủ để kiểm chứng luồng).
HTTP-01 vs DNS-01
HTTP-01 LE gọi http://cty.vn/.well-known/acme-challenge/xxx để xác thực
→ Cần cổng 80 MỞ RA INTERNET, domain trỏ đúng về Ingress.
→ ❌ KHÔNG cấp được wildcard (*.cty.vn)
→ Đơn giản nhất — dùng khi web public.
DNS-01 cert-manager tự tạo bản ghi TXT trên DNS để chứng minh sở hữu
→ Cần credential API của nhà cung cấp DNS (Cloudflare, Route53...)
→ ✅ Cấp được WILDCARD (*.cty.vn)
→ ✅ Chạy được cả khi cluster KHÔNG public (internal)Xin cert: chỉ cần một annotation
Phần đẹp nhất — bạn không phải tạo Certificate bằng tay. Chỉ thêm annotation vào Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: myapp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod # ⭐ dòng ma thuật
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts: [cty.vn]
secretName: cty-vn-tls # cert-manager sẽ TỰ TẠO Secret này
rules:
- host: cty.vn
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: web-service, port: { number: 80 } } Bạn apply Ingress
│
▼
cert-manager thấy annotation → tự tạo Certificate → gọi Let's Encrypt
│
▼
Giải bài toán HTTP-01 (tạo Pod + Ingress tạm cho đường /.well-known/...)
│
▼
LE cấp cert → cert-manager LƯU vào Secret "cty-vn-tls"
│
▼
Ingress Controller nạp cert → https://cty.vn xanh lá 🔒
│
▼
~30 ngày TRƯỚC hạn: cert-manager TỰ GIA HẠN. Mãi mãi. Bạn không phải nhớ gì.kubectl get certificate -n myapp # READY: True là xong
kubectl describe certificate cty-vn-tls -n myapp # debug nếu chưa Ready
kubectl get certificaterequest,order,challenge -A # soi từng bước ACME🚀 Lab — Ingress + TLS trên kind
1. Cluster kind có cổng vào
cat <<EOF | kind create cluster --name ing --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- { containerPort: 80, hostPort: 80, protocol: TCP }
- { containerPort: 443, hostPort: 443, protocol: TCP }
EOF
# Cài ingress-nginx (bản dành cho kind)
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl wait --namespace ingress-nginx --for=condition=ready pod \
--selector=app.kubernetes.io/component=controller --timeout=120s2. Hai app, định tuyến theo path
kubectl create deployment web --image=nginx --port=80
kubectl expose deployment web --port=80
kubectl create deployment api --image=hashicorp/http-echo \
-- /http-echo -text="xin chao tu API" -listen=:80
kubectl expose deployment api --port=80
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo
spec:
ingressClassName: nginx
rules:
- host: demo.local
http:
paths:
- path: /api
pathType: Prefix
backend: { service: { name: api, port: { number: 80 } } }
- path: /
pathType: Prefix
backend: { service: { name: web, port: { number: 80 } } }
EOF
curl -H "Host: demo.local" http://localhost/ # → trang nginx (web)
curl -H "Host: demo.local" http://localhost/api # → "xin chao tu API" ✨
# → MỘT cổng vào, hai app, phân biệt bằng đường dẫn.3. TLS bằng cert tự ký (không cần Internet)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt -subj "/CN=demo.local"
kubectl create secret tls demo-tls --cert=tls.crt --key=tls.key
kubectl patch ingress demo --type=merge -p \
'{"spec":{"tls":[{"hosts":["demo.local"],"secretName":"demo-tls"}]}}'
curl -k -H "Host: demo.local" https://localhost/ # HTTPS chạy 🔒4. cert-manager (luồng thật — cần domain public)
helm repo add jetstack https://charts.jetstack.io && helm repo update
helm install cert-manager jetstack/cert-manager -n cert-manager \
--create-namespace --set crds.enabled=true
kubectl get pods -n cert-manager
# ClusterIssuer STAGING (luôn thử staging trước!)
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-staging }
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: admin@cty.vn
privateKeySecretRef: { name: letsencrypt-staging-key }
solvers:
- http01: { ingress: { class: nginx } }
EOF
kubectl get clusterissuer # READY True
# (Bước xin cert thật cần domain public trỏ về cluster — làm ở cluster on-prem
# của bài sau, thêm annotation cert-manager.io/cluster-issuer vào Ingress.)5. Dọn
kind delete cluster --name ingCâu chuyện thực tế
Ba sự cố Ingress, ba bài học.
Chứng chỉ hết hạn lúc 2 giờ sáng. Năm đầu, chúng tôi xin cert Let's Encrypt bằng tay, copy vào Secret. Ai đó đặt nhắc lịch 90 ngày sau. Rồi người đó đổi việc. Ngày thứ 91, một sáng thứ Bảy, toàn bộ web hiện cảnh báo đỏ "kết nối không an toàn". Khách hoảng, gọi tổng đài, tưởng bị hack. Chúng tôi mất hơn một tiếng để xin cert mới và deploy (còn phải tỉnh ngủ đã). Sau vụ đó tôi cài cert-manager trong nửa buổi — và từ đó không bao giờ phải nghĩ về chứng chỉ nữa. Nó tự gia hạn, im lặng, mỗi 60 ngày, suốt nhiều năm.
Bị Let's Encrypt khóa một tuần. Lần đầu dùng cert-manager, tôi cấu hình sai solver và cứ apply đi apply lại trên issuer production. Sau 5 lần thất bại, LE trả về too many certificates already issued — khóa domain đó một tuần. Không xin thêm cert được, dù đã sửa đúng. Bài học đắt: luôn dùng letsencrypt-staging cho tới khi thấy Certificate READY: True, rồi mới đổi sang prod issuer.
Lỗi 413 bí ẩn. Người dùng báo không upload được ảnh > 1MB, mà log app sạch trơn — vì request chưa bao giờ tới app. Thủ phạm: nginx-ingress mặc định giới hạn body 1MB, và nó từ chối ngay tại cổng. Thêm một dòng proxy-body-size: "50m" là xong. Bài học: Ingress là một tầng riêng — bug có thể nằm ở đó, không phải ở app. Khi log app trống trơn mà client vẫn lỗi, hãy soi Ingress Controller.
Bài học:
Chứng chỉ thủ công là bom hẹn giờ 90 ngày. Con người sẽ quên — cert-manager thì không. Nửa buổi cài, đổi lấy sự yên tâm nhiều năm.
Staging issuer trước, prod issuer sau. LE giới hạn 5 cert/domain/tuần. Sai vài lần trên prod = khóa một tuần, không cứu được.
Ingress là một tầng, và bug có thể ở tầng đó. Log app trống mà client vẫn lỗi (413, 502, 504) → soi Ingress Controller và annotation trước.
Một Ingress Controller thay cho N LoadBalancer. Tiết kiệm IP/tiền, và TLS chỉ cấu hình một chỗ thay vì mỗi app một lần.
kubectl describe certificatelà bạn. Khi cert không Ready, chuỗiCertificate → CertificateRequest → Order → Challengecho biết chính xác kẹt ở đâu (thường là DNS chưa trỏ, hoặc cổng 80 bị chặn).
Pitfalls
Tạo Ingress mà chưa cài Ingress Controller → không có gì xảy ra, không lỗi. Phải cài controller (nginx/traefik).
Quên
ingressClassName→ không controller nào nhận luật, Ingress nằm im. Luôn ghi rõ.Secret TLS khác namespace với Ingress → Ingress phục vụ cert mặc định (self-signed), trình duyệt báo đỏ. Phải cùng namespace.
Dùng thẳng
letsencrypt-prodkhi đang thử → sai vài lần là bị rate limit khóa cả tuần. Staging trước.HTTP-01 mà cổng 80 không mở ra Internet / DNS chưa trỏ đúng → challenge fail mãi. Cluster nội bộ hoặc cần wildcard → phải dùng DNS-01.
Xin wildcard (
*.cty.vn) bằng HTTP-01 → không bao giờ được. Wildcard bắt buộc DNS-01.Quên
rewrite-target→ backend nhận cả tiền tố (/api/usersthay vì/users) → 404 khó hiểu.Lỗi 413 khi upload →
proxy-body-sizemặc định 1MB. Tăng lên.Thứ tự path sai — đặt
path: /(Prefix) trước/api→ mọi request rơi vào/. Đặt path cụ thể trước,/cuối cùng.Backend chạy HTTPS mà không khai
backend-protocol: HTTPS→ Ingress gửi HTTP tới cổng HTTPS → 502.Tưởng TLS được mã hóa suốt tới Pod. Không — mặc định TLS kết thúc ở Ingress; đoạn Ingress→Pod là HTTP nội bộ. Cần mã hóa cả chặng trong → service mesh (mTLS).
Quên
ssl-redirect→ người dùng vẫn vào đượchttp://không mã hóa. Bật để ép sang HTTPS.
Tóm tắt
- LoadBalancer/NodePort = tầng 4 (chỉ IP+cổng). Web cần định tuyến theo domain và path → tầng 7 → Ingress.
- Ingress = luật; Ingress Controller = Pod thực thi. Không cài controller → Ingress vô dụng (và không báo lỗi).
- Một IP + một cổng 443 phục vụ vô số app; TLS cấu hình một chỗ.
- Định tuyến: host-based, path-based;
pathType(Prefix / Exact);ingressClassName(bắt buộc ghi). - Annotations là nơi sức mạnh nằm:
ssl-redirect,rewrite-target,proxy-body-size(413!),limit-rps,backend-protocol, CORS. - TLS: Secret loại
kubernetes.io/tls, khai trongspec.tls, phải cùng namespace với Ingress. TLS kết thúc tại Controller. - cert-manager:
ClusterIssuer(Let's Encrypt) + một annotationcert-manager.io/cluster-issuer→ cert tự xin, tự tạo Secret, tự gia hạn mãi mãi. - ⚠️ Staging issuer trước (LE giới hạn 5 cert/domain/tuần). HTTP-01 (đơn giản, cần cổng 80 public, không wildcard) vs DNS-01 (cần API DNS, có wildcard, chạy được cả cluster nội bộ).
- Debug:
kubectl describe certificate→ chuỗiCertificateRequest → Order → Challenge.
Bài sau là đỉnh của chặng: Bài 10 — Capstone. Ta gom toàn bộ 9 bài — Pod, Deployment, Service, Namespace, ConfigMap/Secret, Storage/StatefulSet, Job/CronJob, hàng rào bảo mật, và Ingress/TLS vừa học — để tự tay dựng cluster on-prem 3 node bằng kubeadm (containerd → CNI → MetalLB → Ingress → Storage), rồi migrate cả stack từ Docker Compose lên K8s hoàn chỉnh.
Nếu hôm nay bạn không bao giờ phải đặt lịch nhắc gia hạn chứng chỉ nữa — cert-manager đã trả công cho nửa buổi bạn bỏ ra, và sẽ còn trả tiếp trong nhiều năm.
— Minh Hưng