HAProxy Gom Ba API Server và kubectl Từ Xa

K
Kai··6 min read

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-client củ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ào system:masters mà 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/exec chạy đúng kể cả khi sau này bạn rút cert khỏi system: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.

Related Posts