Triển khai Cụm Kubernetes High Availability (HA) trên Bare Metal với kubeadm, HAProxy, Keepalived và etcd Raft Quorum

Tại sao cụm Kubernetes trên Bare Metal dễ sập toàn diện nếu thiết kế sai Control Plane và mạng L2/L4?

Trên các nền tảng đám mây công cộng (AWS EKS, Google GKE, Azure AKS), toàn bộ lớp điều khiển (Control Plane) bao gồm kube-apiserver, etcd, kube-scheduler và kube-controller-manager được nhà cung cấp vận hành ẩn phía sau một điểm cuối mạng (Endpoint) có độ sẵn sàng cao. Tuy nhiên, khi triển khai Kubernetes High Availability (HA) trên hạ tầng Bare Metal hoặc máy chủ vật lý On-Premises, đội ngũ Kỹ sư Hệ thống và SRE phải tự giải quyết ba bài toán sống còn ở tầng vật lý:

  1. Điểm lỗi đơn lẻ (Single Point of Failure - SPOF) tại kube-apiserver: Mọi thành phần trong cụm (kubelet trên các Worker Node, kube-proxy, bộ điều khiển CNI và công cụ CI/CD như ArgoCD GitOps) đều giao tiếp duy nhất qua kube-apiserver ở cổng TCP 6443. Nếu trỏ trực tiếp kubelet vào địa chỉ IP của một máy chủ Master duy nhất, toàn bộ cụm sẽ mất khả năng tự phục hồi (Self-healing) ngay khi máy chủ đó lỗi phần cứng hoặc bảo trì Kernel.
  2. Mất đồng thuận trạng thái etcd (Split-Brain & Quorum Loss): Cơ sở dữ liệu khóa-giá trị phân tán etcd sử dụng thuật toán đồng thuận Raft. Nếu đặt số lượng chẵn các nút etcd (ví dụ 2 hoặc 4 máy chủ) hoặc đặt etcd chung ổ đĩa HDD/SSD tiêu dùng với hệ điều hành và container logs, độ trễ ghi đĩa (wal_fsync_duration_seconds) vượt ngưỡng 10ms sẽ kích hoạt bầu cử lại Leader liên tục, làm tê liệt toàn bộ Control Plane.
  3. Thiếu vắng bộ cân bằng tải mạng (External Load Balancer) cho dịch vụ LoadBalancer: Khác với Cloud Provider tự động cấp phát AWS ALB/NLB khi tạo Service kiểu LoadBalancer, cụm Bare Metal sẽ giữ trạng thái <pending> vĩnh viễn tại trường EXTERNAL-IP nếu không triển khai giải pháp định tuyến tầng L2 ARP hoặc L3 BGP như MetalLB kết hợp kiến trúc Microservices trên Docker và Kubernetes thực chiến.

Bài viết kỹ thuật chuyên sâu này hướng dẫn từng bước thiết kế, tính toán dung lượng, cấu hình và nghiệm thu cụm Kubernetes High Availability trên Bare Metal chuẩn Production sử dụng kubeadm (v1.30+), HAProxy + Keepalived (VRRP VIP), Stacked etcd Raft và MetalLB.


1. Kiến trúc Control Plane HA: Stacked etcd so với External etcd và Toán học Đồng thuận Raft

Tài liệu chính thức của Kubernetes định nghĩa hai mô hình kiến trúc cho cụm High Availability: Stacked etcd topology (các thành phần Control Plane và thành viên etcd chạy chung trên cùng các nút Master) và External etcd topology (tách cụm etcd ra các máy chủ vật lý độc lập).

flowchart TB
    subgraph Clients["Tầng Quản trị & Worker Nodes"]
        Kubectl["kubectl / ArgoCD GitOps"]
        Kubelet1["Worker Node 01 (kubelet)"]
        Kubelet2["Worker Node 02 (kubelet)"]
        Kubelet3["Worker Node 03 (kubelet)"]
    end

    subgraph L4VIP["Tầng Cân Bằng Tải L4 & Virtual IP (VRRP)"]
        VIP["Virtual IP (VIP): 10.10.10.100:6443<br/>Keepalived VRRP (unicast)"]
        HAP1["LB-01: HAProxy + Keepalived MASTER<br/>10.10.10.11"]
        HAP2["LB-02: HAProxy + Keepalived BACKUP<br/>10.10.10.12"]
    end

    subgraph ControlPlane["Cụm Control Plane 3 Nút (Stacked etcd Raft Quorum = 2)"]
        CP1["CP-01 (10.10.10.21)<br/>kube-apiserver + etcd Leader<br/>NVMe Dedicated /var/lib/etcd"]
        CP2["CP-02 (10.10.10.22)<br/>kube-apiserver + etcd Follower<br/>NVMe Dedicated /var/lib/etcd"]
        CP3["CP-03 (10.10.10.23)<br/>kube-apiserver + etcd Follower<br/>NVMe Dedicated /var/lib/etcd"]
    end

    Kubectl --> VIP
    Kubelet1 --> VIP
    Kubelet2 --> VIP
    Kubelet3 --> VIP
    VIP --> HAP1
    VIP -.->|Failover VRRP < 3s| HAP2
    HAP1 -->|TCP 6443 /healthz| CP1
    HAP1 -->|TCP 6443 /healthz| CP2
    HAP1 -->|TCP 6443 /healthz| CP3
    CP1 <-->|Raft Peer TLS 2380| CP2
    CP2 <-->|Raft Peer TLS 2380| CP3
    CP1 <-->|Raft Peer TLS 2380| CP3

Công thức tính Quorum của thuật toán đồng thuận Raft

Trong thuật toán Raft mà etcd sử dụng, mọi thao tác ghi trạng thái (Mutation) vào cụm phải được xác nhận (Commit) bởi một đa số quá bán (Quorum) các nút thành viên:

$$text{Quorum} = leftlfloor frac{N}{2} rightrfloor + 1$$

Từ đó, số lượng nút tối đa được phép ngừng hoạt động đồng thời (Fault Tolerance) mà cụm vẫn duy trì khả năng đọc/ghi là:

$$text{Fault Tolerance} = N - left(leftlfloor frac{N}{2} rightrfloor + 1right) = leftlfloor frac{N - 1}{2} rightrfloor$$

Quy mô cụm etcd ($N$) Số nút cần thiết để đạt Quorum Số nút tối đa chịu lỗi đồng thời Đánh giá kiến trúc Bare Metal Production
1 nút 1 0 Không có HA. Mất ổ đĩa đồng nghĩa mất toàn bộ cụm.
2 nút 2 0 Phản mẫu (Anti-pattern): Xác suất lỗi tăng gấp đôi nhưng khả năng chịu lỗi bằng 0.
3 nút 2 1 Chuẩn vàng cho cụm vừa và nhỏ (< 100 Worker Nodes): Cân bằng tối ưu giữa chi phí phần cứng và độ trễ Raft replication.
4 nút 3 1 Phản mẫu: Tăng độ trễ mạng khi ghi nhưng chỉ chịu lỗi được 1 nút (ngang cụm 3 nút).
5 nút 3 2 Chuẩn Production quy mô lớn: Cho phép bảo trì chủ động 1 máy chủ mà vẫn chịu thêm được 1 sự cố phần cứng bất ngờ.

Quy tắc phần cứng bắt buộc cho etcd trên Bare Metal: Thư mục /var/lib/etcd (chứa Write-Ahead Log - WAL và dữ liệu BoltDB) phải được gắn (Mount) trên ổ cứng Enterprise NVMe/SSD có bảo vệ mất nguồn (PLP - Power Loss Protection) độc lập với phân vùng gốc / và /var/log/containers. Chỉ số phân vị thứ 99 (p99) của metric Prometheus etcd_disk_wal_fsync_duration_seconds_bucket bắt buộc phải nhỏ hơn 10ms và etcd_network_peer_round_trip_time_seconds_bucket phải nhỏ hơn 50ms.


2. Chuẩn bị Hệ điều hành Linux, Kernel Modules và Tham số sysctl trên Bare Metal

Trước khi khởi tạo cụm bằng kubeadm, toàn bộ các máy chủ Control Plane và Worker Node phải được đồng bộ hóa cấu hình nhân Linux (tham khảo chuyên sâu tại Tuning Linux Kernel cho máy chủ Web phục vụ hàng triệu kết nối đồng thời) để hỗ trợ mạng container Overlay/BGP, tắt phân vùng Swap nhằm đảm bảo tính xác định của cơ chế quản lý bộ nhớ kubelet (tương tự nguyên lý đã phân tích trong bài chẩn đoán OOM Killer và cgroups v2 trên Linux), và cài đặt containerd sử dụng SystemdCgroup = true.

#!/usr/bin/env bash
# File: /usr/local/sbin/bootstrap-k8s-baremetal-node.sh
# Mô tả: Chuẩn hóa nhân Linux và containerd cho nút Kubernetes Bare Metal
set -euo pipefail

# 1. Vô hiệu hóa hoàn toàn Swap ở cả phiên hiện tại và /etc/fstab
swapoff -a
sed -i '/sswaps/s/^/#/' /etc/fstab

# 2. Nạp các Kernel module bắt buộc cho containerd và kube-proxy IPVS
cat <<'EOF' | tee /etc/modules-load.d/k8s-baremetal.conf
overlay
br_netfilter
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
EOF

modprobe overlay
modprobe br_netfilter
modprobe ip_vs
modprobe ip_vs_rr
modprobe ip_vs_wrr
modprobe ip_vs_sh
modprobe nf_conntrack

# 3. Cấu hình tham số mạng và bộ nhớ nhân Linux (sysctl)
cat <<'EOF' | tee /etc/sysctl.d/99-kubernetes-baremetal.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
net.netfilter.nf_conntrack_max      = 1048576
fs.inotify.max_user_watches         = 524288
fs.inotify.max_user_instances       = 8192
vm.overcommit_memory                = 1
vm.panic_on_oom                     = 0
EOF

sysctl --system

# 4. Tạo cấu hình mặc định cho containerd và bật SystemdCgroup
mkdir -p /etc/containerd
containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/g' > /etc/containerd/config.toml
systemctl restart containerd
systemctl enable containerd


3. Thiết kế Tầng Cân Bằng Tải L4 cho kube-apiserver với Keepalived (VRRP) và HAProxy

Để loại bỏ hoàn toàn điểm lỗi đơn lẻ ở tầng tiếp nhận kết nối 6443, chúng ta cấu hình cặp bộ cân bằng tải Keepalived + HAProxy sử dụng giao thức VRRP ở chế độ Unicast (tránh bị chặn gói tin Multigroup 224.0.0.18 trên các thiết bị chuyển mạch Switch trong trung tâm dữ liệu) và kiểm tra sức khỏe chủ động (GET /healthz) qua TLS.

sequenceDiagram
    autonumber
    participant Kubelet as Worker Node (kubelet)
    participant VIP as Keepalived VIP (10.10.10.100)
    participant HAP1 as LB-01 HAProxy (MASTER)
    participant HAP2 as LB-02 HAProxy (BACKUP)
    participant API1 as CP-01 kube-apiserver
    participant API2 as CP-02 kube-apiserver

    Kubelet->>VIP: Kết nối TLS TCP :6443 (Watch Pods/Endpoints)
    VIP->>HAP1: Chuyển tiếp gói tin tới LB-01 (VRRP Priority 101)
    HAP1->>API1: L4 TCP Proxy (Round-Robin) tới CP-01 :6443
    Note over HAP1,API2: HAProxy thực thi GET /healthz mỗi 2000ms
    API1--xHAP1: CP-01 mất kết nối (fall 3 checks)
    HAP1->>API2: Tự động cô lập CP-01, điều hướng phiên mới sang CP-02
    Note over HAP1,HAP2: Giả lập sự cố sập tiến trình HAProxy trên LB-01
    HAP1--xVIP: vrrp_script chk_haproxy trả về mã lỗi != 0
    HAP2->>VIP: Chiếm quyền sở hữu VIP 10.10.10.100 + gửi Gratuitous ARP
    Kubelet->>VIP: Tự động khôi phục luồng Watch qua LB-02 tới CP-02

Cấu hình Keepalived Unicast (/etc/keepalived/keepalived.conf)

! File: /etc/keepalived/keepalived.conf (Trên nút LB-01: 10.10.10.11)
global_defs {
    router_id K8S_LB_01
    enable_script_security
    script_user root
}

vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    weight -30
    fall 2
    rise 2
}

vrrp_instance VI_K8S_APISERVER {
    state MASTER
    interface bond0
    virtual_router_id 51
    priority 101
    advert_int 1

    unicast_src_ip 10.10.10.11
    unicast_peer {
        10.10.10.12
    }

    authentication {
        auth_type PASS
        auth_pass K8sVrrpHa2026!
    }

    virtual_ipaddress {
        10.10.10.100/24 dev bond0 label bond0:vip
    }

    track_script {
        chk_haproxy
    }
}

Cấu hình HAProxy L4 TCP Health Check (/etc/haproxy/haproxy.cfg)

Khác với cấu hình cơ bản chỉ kiểm tra mở cổng TCP (option tcp-check), cấu hình chuẩn Production sử dụng option httpchk GET /healthz kết hợp check-ssl verify none ở tầng Backend để đảm bảo HAProxy chỉ gửi lưu lượng vào kube-apiserver khi kết nối tới etcd bên dưới thực sự khỏe mạnh:

# File: /etc/haproxy/haproxy.cfg
global
    log /dev/log local0
    log /dev/log local1 notice
    maxconn 20000
    user haproxy
    group haproxy
    daemon

defaults
    log     global
    mode    tcp
    option  tcplog
    option  dontlognull
    retries 3
    timeout queue           1m
    timeout connect         5s
    timeout client          4h
    timeout server          4h
    timeout check           5s

frontend k8s_apiserver_frontend
    bind 10.10.10.100:6443
    mode tcp
    option tcplog
    default_backend k8s_apiserver_backend

backend k8s_apiserver_backend
    mode tcp
    balance roundrobin
    option httpchk GET /healthz HTTP/1.0
    http-check expect status 200
    default-server inter 2s downinter 5s rise 2 fall 3 slowstart 60s maxconn 2000 maxqueue 256 weight 100
    server cp-01 10.10.10.21:6443 check check-ssl verify none
    server cp-02 10.10.10.22:6443 check check-ssl verify none
    server cp-03 10.10.10.23:6443 check check-ssl verify none

Lưu ý quan trọng về timeout client và timeout server: Các thành phần kubelet, kube-controller-manager và CNI duy trì các kết nối HTTP/2 Watch kéo dài (Long-lived Watch streams) tới kube-apiserver. Nếu đặt timeout client/server quá ngắn (ví dụ 50s mặc định của HTTP proxy), HAProxy sẽ ngắt kết nối định kỳ gây bão kết nối lại (Reconnection storm). Giá trị khuyến nghị cho cổng 6443 là 4h.


4. Khởi tạo Cụm Control Plane Đa Nút bằng kubeadm (Declarative API v1beta3)

Thay vì truyền hàng chục cờ dòng lệnh rời rạc, thực tiễn tốt nhất khi vận hành hạ tầng dưới dạng mã (IaC) là định nghĩa toàn bộ cấu hình cụm trong tệp kubeadm-config.yaml sử dụng API kubeadm.k8s.io/v1beta3, kích hoạt chế độ ipvs cho kube-proxy và tinh chỉnh ngưỡng ổ đĩa etcd.

# File: /etc/kubernetes/kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "10.10.10.21"
  bindPort: 6443
nodeRegistration:
  criSocket: "unix:///var/run/containerd/containerd.sock"
  name: "cp-01.infoteck.internal"
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: "v1.30.4"
controlPlaneEndpoint: "10.10.10.100:6443"
clusterName: "infoteck-baremetal-prod"
networking:
  serviceSubnet: "10.96.0.0/12"
  podSubnet: "10.244.0.0/16"
  dnsDomain: "cluster.local"
apiServer:
  certSANs:
    - "10.10.10.100"
    - "10.10.10.21"
    - "10.10.10.22"
    - "10.10.10.23"
    - "k8s-api.infoteck.internal"
  extraArgs:
    enable-admission-plugins: "NodeRestriction"
    audit-log-maxage: "30"
    audit-log-maxbackup: "10"
    audit-log-maxsize: "100"
etcd:
  local:
    dataDir: "/var/lib/etcd"
    extraArgs:
      quota-backend-bytes: "8589934592"
      auto-compaction-mode: "periodic"
      auto-compaction-retention: "1"
      heartbeat-interval: "100"
      election-timeout: "1000"
      snapshot-count: "10000"
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
  strictARP: true
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: "systemd"
systemReserved:
  cpu: "500m"
  memory: "1Gi"
  ephemeral-storage: "2Gi"
kubeReserved:
  cpu: "500m"
  memory: "1Gi"
  ephemeral-storage: "2Gi"
evictionHard:
  memory.available: "500Mi"
  nodefs.available: "10%"

Quy trình khởi tạo CP-01 và gia nhập CP-02, CP-03

# Bước 1: Trên CP-01 (10.10.10.21), khởi tạo nút Control Plane đầu tiên và tải chứng chỉ lên Secret tạm thời (tự xóa sau 2 giờ)
sudo kubeadm init --config=/etc/kubernetes/kubeadm-config.yaml --upload-certs

# Bước 2: Trên CP-02 (10.10.10.22) và CP-03 (10.10.10.23), gia nhập vào mặt phẳng điều khiển
sudo kubeadm join 10.10.10.100:6443 
  --token <token-id>.<token-secret> 
  --discovery-token-ca-cert-hash sha256:<ca-cert-hash> 
  --control-plane 
  --certificate-key <certificate-key> 
  --apiserver-advertise-address=<ip-rieng-cua-node-cp>

# Bước 3: Kiểm tra trạng thái đồng thuận của 3 thành viên etcd
sudo ETCDCTL_API=3 etcdctl 
  --endpoints=https://10.10.10.21:2379,https://10.10.10.22:2379,https://10.10.10.23:2379 
  --cacert=/etc/kubernetes/pki/etcd/ca.crt 
  --cert=/etc/kubernetes/pki/etcd/peer.crt 
  --key=/etc/kubernetes/pki/etcd/peer.key 
  endpoint status --write-out=table


5. Mạng CNI (Calico / Cilium) và Cấp phát IP Ngoài Cụm (External LoadBalancer) với MetalLB

Lựa chọn CNI Plugin (Calico BGP so với Cilium eBPF) và Bật strictARP

Sau khi mặt phẳng điều khiển hoàn tất khởi tạo, các nút vẫn ở trạng thái NotReady cho đến khi cài đặt Container Network Interface (CNI) (như Calico hỗ trợ định tuyến BGP nguyên bản trên thiết bị chuyển mạch ToR hoặc Cilium eBPF loại bỏ hoàn toàn độ trễ iptables khi quy mô Service vượt hàng chục nghìn bản ghi). Lưu ý trong tệp kubeadm-config.yaml ở phần trên, chúng ta đã bật ipvs.strictARP: true. Đây là điều kiện bắt buộc để MetalLB (hoặc kube-vip ở chế độ Static Pod ARP/BGP) hoạt động chính xác ở chế độ Layer 2 (ARP/NDP) hoặc BGP khi sử dụng kube-proxy chế độ IPVS.

Cấu hình IPAddressPool và L2Advertisement trên MetalLB

# File: metallb-l2-production-pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: baremetal-dmz-pool
  namespace: metallb-system
spec:
  addresses:
    - 10.10.20.200-10.10.20.250
  autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: baremetal-l2-adv
  namespace: metallb-system
spec:
  ipAddressPools:
    - baremetal-dmz-pool
  interfaces:
    - bond0

Khi triển khai kiến trúc bảo mật Zero Trust Security trong doanh nghiệp công nghệ, MetalLB sẽ gán một địa chỉ IP từ dải 10.10.20.200-10.10.20.250 và phản hồi bản tin ARP cho địa chỉ đó, giúp lưu lượng từ tường lửa biên đi thẳng vào cụm Kubernetes mà không cần Cloud Load Balancer.


6. Kịch bản Kiểm thử Chịu lỗi (Chaos Verification) và Sao lưu Khôi phục Thảm họa etcd

Một cụm Kubernetes HA trên Bare Metal chỉ được nghiệm thu đưa vào Production sau khi vượt qua bài kiểm tra ngắt đột ngột nút Leader và kiểm chứng khôi phục bản chụp (snapshot restore) của etcd:

#!/usr/bin/env bash
# File: /usr/local/sbin/backup-verify-etcd.sh
# Mô tả: Sao lưu snapshot etcd định kỳ và kiểm tra tính toàn vẹn hình học của dữ liệu
set -euo pipefail

BACKUP_DIR="/var/backups/etcd"
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
SNAPSHOT_FILE="${BACKUP_DIR}/etcd-snapshot-${TIMESTAMP}.db"

mkdir -p "${BACKUP_DIR}"
chmod 700 "${BACKUP_DIR}"

export ETCDCTL_API=3

# 1. Chụp snapshot trực tuyến từ nút cục bộ
etcdctl --endpoints=https://127.0.0.1:2379 
  --cacert=/etc/kubernetes/pki/etcd/ca.crt 
  --cert=/etc/kubernetes/pki/etcd/server.crt 
  --key=/etc/kubernetes/pki/etcd/server.key 
  snapshot save "${SNAPSHOT_FILE}"

# 2. Xác minh tính toàn vẹn bảng băm (Hash) và tổng số khóa trong snapshot
etcdctl snapshot status "${SNAPSHOT_FILE}" --write-out=table

# 3. Giữ lại các bản sao lưu trong 14 ngày gần nhất
find "${BACKUP_DIR}" -name "etcd-snapshot-*.db" -mtime +14 -delete

Danh mục Kiểm tra Nghiệm thu (Production Readiness Checklist)

  1. Kiểm tra chuyển đổi dự phòng VIP: Tắt dịch vụ haproxy trên LB-01 (systemctl stop haproxy), xác nhận địa chỉ VIP 10.10.10.100 dịch chuyển sang LB-02 trong vòng < 3 giây và lệnh kubectl get nodes --watch không bị đứt quãng.
  2. Kiểm tra mất nút etcd Leader: Tắt nguồn đột ngột nút đang giữ vai trò IS LEADER = true trong bảng etcdctl endpoint status. Xác nhận 2 nút còn lại bầu Leader mới trong khoảng 1000ms–2000ms và các ứng dụng đóng gói bằng Docker image tối ưu Multi-stage vẫn tiếp tục được lập lịch bình thường.
  3. Giám sát chứng chỉ PKI: Thiết lập cảnh báo thông qua Xây dựng hệ thống Monitoring toàn diện với Prometheus và Grafana cho thời hạn chứng chỉ trong /etc/kubernetes/pki và kiểm tra bằng lệnh kubeadm certs check-expiration trước mỗi đợt nâng cấp cụm.

Tài liệu tham khảo & Nguồn chuẩn (Primary References)

  1. Kubernetes Official Documentation: Options for Highly Available Topology (Stacked etcd vs External etcd) — https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/ha-topology/
  2. Kubernetes Official Documentation: Creating Highly Available Clusters with kubeadm — https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/
  3. etcd Official Documentation: etcd v3.5 Operations Guide, Raft Consensus & Hardware Recommendations — https://etcd.io/docs/v3.5/op-guide/hardware/
  4. MetalLB Official Documentation: Layer 2 & BGP Configuration with kube-proxy IPVS strictARP — https://metallb.universe.tf/installation/