HAProxy Gom Ba API Server và kubectl Từ Xa
Control plane đã đủ thành phần, nhưng đến giờ ta vẫn gọi api-server qua 127.0.0.1 trên từng controller. Từ bên ngoài — laptop của bạn, hay các worker sắp thêm vào — chưa ai biết phải gọi api-server nào trong ba. Bài này giải quyết: dựng một load balancer gom ba api-server thành một địa chỉ, cấu hình kubectl trên máy bạn, và thêm RBAC cần thiết trước khi worker tham gia.
Vì sao cần load balancer, và vì sao phải là TCP passthrough
Ba api-server chạy song song và bình đẳng (Bài 1: chúng không giữ trạng thái riêng). Client chỉ cần một địa chỉ ổn định, và nếu một api-server chết thì lưu lượng tự dồn sang hai cái còn lại. Đó đúng là việc của một load balancer.
Nhưng có một ràng buộc quan trọng: load balancer này không được giải mã TLS. Nhớ lại Bài 2 — cluster dùng mTLS, api-server đọc danh tính client từ chính client certificate. Nếu load balancer đứng ra kết thúc TLS (như cách một LB HTTP thường làm cho web), nó sẽ phá vỡ mTLS: api-server không còn thấy cert gốc của client, không đọc được danh tính, và mọi xác thực sụp đổ.
Nên ta cấu hình HAProxy ở chế độ TCP (mode tcp) — nó chỉ chuyển tiếp nguyên xi luồng bytes tới một api-server, không nhìn vào bên trong. TLS vẫn được thiết lập đầu-cuối giữa client và api-server, đi xuyên qua load balancer như một đường ống.
kubectl ──TLS──┐ ┌── controller-0:6443
(cert client) │ HAProxy :6443 │
├── chuyển bytes ────┼── controller-1:6443
worker ──TLS──┘ (mode tcp, └── controller-2:6443
KHÔNG mở TLS)
▲ ▲
└──────── mTLS đầu-cuối, LB không xen vào ┘
Bước 1 — Cài và cấu hình HAProxy trên lb-0
HAProxy có sẵn trong kho Ubuntu. Cài rồi viết cấu hình:
# trên lb-0
sudo apt-get update
sudo apt-get install -y haproxy
haproxy -v | head -1
HAProxy version 2.8.16-0ubuntu0.24.04.2 2026/04/15 - https://haproxy.org/
Cấu hình gồm một frontend lắng nghe ở :6443 và một backend liệt kê ba api-server. Toàn bộ ở mode tcp:
sudo tee /etc/haproxy/haproxy.cfg >/dev/null <<'EOF'
global
log /dev/log local0
maxconn 4096
daemon
defaults
log global
mode tcp
option tcplog
timeout connect 10s
timeout client 30s
timeout server 30s
frontend kubernetes
bind *:6443
default_backend kube-apiservers
backend kube-apiservers
option tcp-check
balance roundrobin
server controller-0 10.0.1.11:6443 check
server controller-1 10.0.1.12:6443 check
server controller-2 10.0.1.13:6443 check
EOF
sudo systemctl enable haproxy
sudo systemctl restart haproxy
systemctl is-active haproxy
active
option tcp-check cùng check ở mỗi server khiến HAProxy tự kiểm tra api-server nào còn sống và chỉ gửi lưu lượng tới những cái khỏe — nền của khả năng chịu lỗi.
Bước 2 — Kiểm tra load balancer từ workstation
Giờ từ máy bạn, gọi api-server qua Elastic IP của lb-0 (ở Bài 3 là 203.0.113.10). Vì SAN của cert api-server đã gồm IP này (Bài 4), TLS sẽ hợp lệ:
# trong ~/k8s-scratch/pki
curl -s --cacert ca.pem https://203.0.113.10:6443/healthz; echo
curl -s --cacert ca.pem --cert admin.pem --key admin-key.pem \
https://203.0.113.10:6443/version | jq -r '.gitVersion'
ok
v1.36.1
/healthz trả ok và lời gọi có xác thực thấy v1.36.1 — load balancer đang chuyển tiếp đúng, và mTLS vẫn nguyên vẹn xuyên qua nó. Nếu lúc này một api-server tắt, hai lệnh trên vẫn chạy nhờ hai cái còn lại.
Bước 3 — Cấu hình kubectl trên laptop
Đến giờ ta toàn dùng curl. Cấu hình kubectl cho tử tế: tạo một kubeconfig trỏ tới Elastic IP, dùng cert admin. Khác với admin.kubeconfig ở Bài 5 (trỏ 127.0.0.1 để chạy trên controller), file này trỏ ra load balancer:
KUBECONFIG_FILE=admin-remote.kubeconfig
kubectl config set-cluster k8s-scratch \
--certificate-authority=ca.pem --embed-certs=true \
--server=https://203.0.113.10:6443 --kubeconfig=$KUBECONFIG_FILE
kubectl config set-credentials admin \
--client-certificate=admin.pem --client-key=admin-key.pem \
--embed-certs=true --kubeconfig=$KUBECONFIG_FILE
kubectl config set-context k8s-scratch \
--cluster=k8s-scratch --user=admin --kubeconfig=$KUBECONFIG_FILE
kubectl config use-context k8s-scratch --kubeconfig=$KUBECONFIG_FILE
Thử vài lệnh thật — lần đầu kubectl nói chuyện với cluster qua đường chính thức:
kubectl --kubeconfig=$KUBECONFIG_FILE version
kubectl --kubeconfig=$KUBECONFIG_FILE get namespaces
kubectl --kubeconfig=$KUBECONFIG_FILE get nodes
Client Version: v1.36.1
Kustomize Version: v5.8.1
Server Version: v1.36.1
NAME STATUS AGE
default Active 14m
kube-node-lease Active 14m
kube-public Active 14m
kube-system Active 14m
No resources found
Bốn namespace mặc định đã có (do api-server tự tạo lúc khởi động), và get nodes trả No resources found — đúng, vì ta chưa thêm worker nào. Đó là việc của các bài sau. (Để khỏi gõ --kubeconfig=... mỗi lần, bạn có thể export KUBECONFIG=$PWD/admin-remote.kubeconfig.)
Bước 4 — RBAC để api-server gọi xuống kubelet
Còn một mảnh RBAC phải đặt trước khi worker tham gia. Khi bạn chạy kubectl logs hay kubectl exec, api-server phải gọi xuống kubelet của node chứa pod. Phía kubelet (Bài 11) sẽ ủy quyền việc cho api-server kiểm tra: "danh tính đang gọi tôi có được phép truy cập node API không?". Để câu trả lời là có, ta cần một ClusterRole cấp quyền lên các tài nguyên của kubelet, gắn cho danh tính mà api-server dùng khi gọi kubelet — chính là CN của cert apiserver-kubelet-client, tức kube-apiserver-kubelet-client.
cat <<'EOF' | kubectl --kubeconfig=admin-remote.kubeconfig apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: system:kube-apiserver-to-kubelet
rules:
- apiGroups: [""]
resources: ["nodes/proxy", "nodes/stats", "nodes/log", "nodes/spec", "nodes/metrics"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:kube-apiserver
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:kube-apiserver-to-kubelet
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: kube-apiserver-kubelet-client
EOF
clusterrole.rbac.authorization.k8s.io/system:kube-apiserver-to-kubelet created
clusterrolebinding.rbac.authorization.k8s.io/system:kube-apiserver created
Cert
apiserver-kubelet-clientcủa ta cóO=system:masters(Bài 4), nên thực ra nó đã có toàn quyền và binding này là thừa. Nhưng đây là cách làm chuẩn: ở môi trường siết chặt, người ta không cho cert đó vàosystem:mastersmà chỉ cấp đúng quyền tối thiểu lên kubelet qua ClusterRole này. Tạo sẵn để quen, và đểkubectl logs/execchạy đúng kể cả khi sau này bạn rút cert khỏisystem:masters.
🧹 Dọn dẹp
HAProxy là thành phần thường trú. Lưu file admin-remote.kubeconfig cẩn thận — nó nhúng cert admin với O=system:masters, tức là toàn quyền vào cluster. Đừng commit hay chia sẻ. Khi stop/start cụm EC2, Elastic IP giữ nguyên nên kubeconfig này vẫn dùng được; chỉ private IP nội bộ là không đổi sẵn rồi.
Tổng kết
Cluster giờ có một mặt tiền thật: một địa chỉ ổn định qua HAProxy, kubectl từ laptop điều khiển được, và RBAC cho api-server gọi xuống kubelet đã sẵn. Quan trọng về mặt hiểu biết là lý do load balancer phải ở chế độ TCP — đó là hệ quả trực tiếp của việc cluster dùng mTLS, và là lỗi cấu hình thường gặp nếu ai đó vô tình cho LB kết thúc TLS.
Phần control plane tới đây khép lại. Bài 10 mở sang phần worker với lớp nền của nó: container runtime. Ta sẽ tìm hiểu CRI — giao diện giữa kubelet và runtime — rồi cài containerd cùng runc trên hai worker, chuẩn bị chỗ cho kubelet đặt container lên ở Bài 11.