Công nghệ IT
Hướng dẫn cài đặt Kubernetes Cluster cho môi trường production
imageimage
Thoại Kỳ
08-07-2025
658 lượt xem

Hướng dẫn cài đặt Kubernetes Cluster cho môi trường production bằng Script

Xây dựng Kubernetes HA cho production trên Ubuntu 24.04 LTS

Hướng dẫn triển khai mới bằng kubeadm: 3 control plane với stacked etcd, 3 worker, 2 load balancer, Cilium và persistent storage. Ví dụ dùng IPv4 trên VM/bare metal có mạng private. Cập nhật tài liệu: 22/10/2026.

Đây là cấu hình tham chiếu để triển khai và nghiệm thu. Bộ lệnh trong tài liệu chưa được chạy trên hạ tầng của bạn; chỉ đưa workload thật vào sau khi các bài kiểm tra ở cuối tài liệu đạt yêu cầu.

1. Kiến trúc và phạm vi

MáyIP private ví dụVai tròCấu hình khởi điểm đề xuất
k8s-lb-1192.168.76.5HAProxy + Keepalived2 vCPU, 2 GB RAM
k8s-lb-2192.168.76.6HAProxy + Keepalived2 vCPU, 2 GB RAM
VIP192.168.76.9Endpoint HAIP dành riêng, không cấp qua DHCP
k8s-cp-1192.168.76.10Control plane + etcd4 vCPU, 8 GB RAM, SSD 100 GB
k8s-cp-2192.168.76.11Control plane + etcd4 vCPU, 8 GB RAM, SSD 100 GB
k8s-cp-3192.168.76.12Control plane + etcd4 vCPU, 8 GB RAM, SSD 100 GB
k8s-worker-1192.168.76.21Ứng dụng + storage8 vCPU, 16 GB RAM; SSD dữ liệu riêng
k8s-worker-2192.168.76.22Ứng dụng + storageNhư worker 1
k8s-worker-3192.168.76.23Ứng dụng + storageNhư worker 1

Các con số tài nguyên là điểm bắt đầu cho ví dụ, không phải cam kết capacity. Đo tải CPU/RAM, IOPS, độ trễ etcd và lượng storage trước khi chốt sizing.

  • API endpoint: k8s-api.example.internal:6443 → VIP.
  • Pod CIDR: 10.244.0.0/16; Service CIDR: 10.96.0.0/12.
  • Domain ứng dụng ví dụ: app.example.com → VIP nếu truy cập nội bộ; nếu public, DNS trỏ public load balancer/NAT chỉ chuyển tiếp cổng ứng dụng.
  • Control plane giữ taint mặc định; workload chạy trên worker.
  • Các control plane phải ở các failure domain khác nhau khi hạ tầng cho phép. Ba VM trên cùng một hypervisor không bảo vệ trước việc hypervisor đó hỏng.
  • etcd có 3 member, chịu được mất 1 member. Không bảo trì đồng thời 2 control plane.

Điều kiện VIP: hai load balancer cùng mạng L2, hạ tầng cho phép di chuyển VIP, gratuitous ARP và VRRP unicast. Ví dụ này không cung cấp HA xuyên subnet/AZ bằng Keepalived. Với cloud không hỗ trợ cơ chế trên, dùng TCP load balancer HA của nhà cung cấp và giữ nguyên DNS API; bỏ phần Keepalived. Không áp dụng VIP kiểu này trực tiếp trên AWS VPC.

VIP là endpoint chung trong ví dụ gọn này. Hệ thống lớn nên tách API LB private và application LB theo nhu cầu tải, quyền truy cập và failure domain.

2. Phiên bản và quy ước thao tác

Thành phầnNhánh/bản tham chiếuCách kiểm soát
Ubuntu24.04 LTS, đã cập nhật bản váKhông yêu cầu cố định ở point release 24.04.2
Kubernetesv1.36.5Pin cùng bản package trên 6 node
containerd2.3.x từ Docker APTChọn và lưu đúng Debian package version một lần
CiliumHelm chart 1.20.3Pin chart, VXLAN, Kubernetes IPAM
Gateway APIv1.6.3Pin CRD theo tài liệu Cilium 1.20
Helm CLIv4.3.1Kiểm tra checksum trước khi cài
LonghornHelm chart 1.13.0, V1 data engineÁp dụng khi dùng storage ở phần 12

Đây là bộ phiên bản tham chiếu, không phải chỉ dẫn luôn dùng bản mới nhất. Trước mỗi đợt triển khai, đối chiếu release notes, cảnh báo bảo mật và compatibility matrix; chọn bản vá còn được hỗ trợ rồi cập nhật đồng bộ tài liệu và file khóa phiên bản. Không tự động đổi minor version khi chạy lại script.

Quy ước: SSH bằng tài khoản vận hành có sudo. Chạy sudo -i để vào root shell cho các khối cài đặt host, backup và kubeadm. Các khối kubectl/helm chạy từ k8s-cp-1 trong root shell với kubeconfig chỉ rõ ở phần 8. Không ghi token, private key, file encryption hay kubeconfig quản trị lên Git hoặc bài blog.

Thứ tự: chuẩn bị mạng → LB → cài 6 node → bảo mật control plane → init CP1 → join CP2/CP3 và worker → Cilium → storage/HTTPS → backup/monitoring → nghiệm thu.

3. Chuẩn bị host và kết nối

3.1. Kiểm tra trên 8 máy

bash
sudo -i hostnamectl ip -br address ip route nproc free -h lsblk -f timedatectl status
  • IP tĩnh/DHCP reservation và hostname duy nhất; VM clone không trùng MAC, product UUID hoặc machine identity.
  • Đồng bộ giờ bằng chrony/NTP; kiểm tra chronyc tracking sau khi cài chrony.
  • SSD local có độ trễ thấp cho /var/lib/etcd; tránh volume mạng có latency không ổn định.
  • Node tới được DNS/NTP, Ubuntu/Docker/Kubernetes APT, Helm repo và image registry cần dùng; môi trường hạn chế internet phải mirror các dependency trước.
  • Host network, Pod CIDR, Service CIDR và mạng VPN không chồng lấn.
  • Dùng kernel Ubuntu được hỗ trợ, cgroup v2; Cilium yêu cầu kernel và eBPF/netfilter phù hợp. Ví dụ chỉ nhắm Linux amd64.

Đặt hostname đúng từng máy, ví dụ trên CP1:

bash
hostnamectl set-hostname k8s-cp-1

Dùng DNS nội bộ ổn định. Nếu chưa có DNS cho bootstrap, thêm cùng mapping vào /etc/hosts trên các máy liên quan, không xóa nội dung hiện hữu:

text
192.168.76.5 k8s-lb-1 192.168.76.6 k8s-lb-2 192.168.76.9 k8s-api.example.internal 192.168.76.10 k8s-cp-1 192.168.76.11 k8s-cp-2 192.168.76.12 k8s-cp-3 192.168.76.21 k8s-worker-1 192.168.76.22 k8s-worker-2 192.168.76.23 k8s-worker-3

Kiểm tra trên cả 6 node và máy quản trị:

bash
getent hosts k8s-api.example.internal getent hosts k8s-cp-1

3.2. Firewall

Giữ AppArmor và firewall. Mô hình này dùng firewall/ACL ở lớp mạng để kiểm soát north-south; 6 Kubernetes node nằm trong một vùng mạng tin cậy dành riêng cho cluster. Cho phép lưu lượng host và Pod cần thiết giữa chính các node đó. Không mở cả subnet nếu trong subnet có máy không thuộc cluster.

Nguồn → đíchGiao thức/cổngMục đích
Bastion/VPN quản trị → hostTCP 22SSH
6 node + quản trị → VIP/LBTCP 6443API server
LB + 6 node + quản trị cần thiết → control planeTCP 6443Backend API và kết nối nội bộ
Control plane ↔ control planeTCP 2379–2380etcd, chỉ nội bộ
Control plane → 6 nodeTCP 10250Kubelet API
6 node ↔ 6 nodeUDP 8472Cilium VXLAN
6 node ↔ 6 nodeTCP 4240 + ICMPCilium health checking
LB1 ↔ LB2IP protocol 112VRRP; đây không phải TCP/UDP port 112
Client được phép → VIP/LBTCP 80/443Ứng dụng, bật sau phần 13
LB → 3 workerTCP 8080/8443Gateway host network, phần 13
Host → DNS/NTP/repo/registry/backupTheo dependency thực tếBootstrap và vận hành

Không mở scheduler/controller-manager, etcd, kubelet hoặc dashboard ra internet. Mô hình Cilium thay kube-proxy này không cần mở cả dải NodePort.

Nếu host có UFW, giữ lại chính sách quản trị nhưng phải thiết kế thêm rule cho forwarding, Pod CIDR và datapath Cilium. Các rule ufw allow <port> đơn lẻ không đủ; không bật UFW mặc định rồi giả định cluster sẽ chạy. Với mô hình này, thống nhất chính sách host firewall với đội mạng và kiểm chứng Cilium connectivity trước khi nghiệm thu. Không dùng ufw disable hay flush iptables như một bước cài đặt mặc định.

4. Load balancer HA

Chỉ thực hiện trên k8s-lb-1 và k8s-lb-2. Không cài kubeadm/containerd lên hai máy này.

4.1. HAProxy

bash
apt-get update apt-get install -y haproxy keepalived curl netcat-openbsd

Tạo /etc/haproxy/haproxy.cfg giống nhau trên cả hai LB:

haproxy
global log /dev/log local0 log /dev/log local1 notice user haproxy group haproxy daemon maxconn 10000 defaults log global mode tcp option tcplog timeout connect 5s timeout client 1h timeout server 1h frontend kubernetes_api bind :6443 default_backend kubernetes_control_planes backend kubernetes_control_planes balance roundrobin option tcp-check default-server inter 2s fall 3 rise 2 server cp1 192.168.76.10:6443 check server cp2 192.168.76.11:6443 check server cp3 192.168.76.12:6443 check
bash
haproxy -c -f /etc/haproxy/haproxy.cfg systemctl enable --now haproxy systemctl restart haproxy

Các backend chưa có API server sẽ DOWN trước khi init; đây là trạng thái dự kiến. TCP check chỉ xác nhận listener; phải giám sát thêm API /readyz có xác thực sau khi cluster lên.

4.2. Keepalived

Chạy ip -br address để xác định interface private; ví dụ dưới dùng ens18. Thay đúng tên interface trên từng LB.

Trên cả hai LB, tạo /usr/local/sbin/check-haproxy:

bash
#!/usr/bin/env bash set -euo pipefail /usr/bin/pgrep -x haproxy >/dev/null /usr/bin/nc -z -w 1 127.0.0.1 6443
bash
chown root:root /usr/local/sbin/check-haproxy chmod 0755 /usr/local/sbin/check-haproxy

Tạo /etc/keepalived/keepalived.conf trên LB1:

text
global_defs { router_id K8S_LB_1 enable_script_security script_user root } vrrp_script chk_haproxy { script "/usr/local/sbin/check-haproxy" interval 2 timeout 2 fall 2 rise 2 } vrrp_instance VI_K8S { state BACKUP interface ens18 virtual_router_id 76 priority 150 advert_int 1 unicast_src_ip 192.168.76.5 unicast_peer { 192.168.76.6 } virtual_ipaddress { 192.168.76.9/24 dev ens18 } track_script { chk_haproxy } }

Trên LB2, dùng cùng cấu hình nhưng đổi:

Thuộc tínhLB2
router_idK8S_LB_2
priority100
unicast_src_ip192.168.76.6
IP trong unicast_peer192.168.76.5

VIP, virtual_router_id và subnet mask giữ giống nhau. Chọn VRID không trùng instance khác trong mạng. Script không đặt weight nên lỗi kiểm tra sẽ đưa instance vào FAULT, thay vì chỉ giảm một lượng priority không đủ để failover.

Trên mỗi LB:

bash
keepalived --config-test -f /etc/keepalived/keepalived.conf systemctl enable --now keepalived systemctl restart keepalived ip -br address journalctl -u keepalived -n 30 --no-pager

VIP chỉ xuất hiện trên một LB. Nếu cả hai cùng giữ VIP, dừng bootstrap và sửa VRRP/ACL/ARP; không tiếp tục khi mạng đang split-brain.

5. Chuẩn bị chung trên 6 Kubernetes node

5.1. Hệ điều hành, swap và kernel

Trên cả 6 node, root shell:

bash
apt-get update apt-get install -y ca-certificates curl gpg chrony jq python3 \ netcat-openbsd iptables conntrack openssl systemctl enable --now chrony swapoff -a cp -a /etc/fstab /etc/fstab.before-k8s sed -ri '/^[[:space:]]*#/!{ /[[:space:]]swap[[:space:]]/s/^/# /; }' /etc/fstab swapon --show systemctl list-units --type=swap --all

Nếu swap đến từ systemd swap unit/zram, vô hiệu hóa đúng unit/generator tương ứng. Sau reboot, swapon --show phải rỗng. Tài liệu chủ động dùng mô hình không swap; Kubernetes có chế độ hỗ trợ swap nhưng không bật trong cấu hình này.

bash
cat >/etc/modules-load.d/k8s.conf <<'EOF' overlay br_netfilter vxlan xt_socket xt_TPROXY EOF modprobe overlay modprobe br_netfilter modprobe vxlan modprobe xt_socket modprobe xt_TPROXY cat >/etc/sysctl.d/99-kubernetes.conf <<'EOF' net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 EOF sysctl --system stat -fc %T /sys/fs/cgroup chronyc tracking

Kỳ vọng cgroup2fs. Giữ AppArmor bật. Cập nhật bản vá OS và reboot trong cửa sổ triển khai trước khi init; về sau dùng lịch bảo trì có drain, không tự reboot đồng loạt các node.

5.2. Thêm APT repository

Thực hiện trên cả 6 node:

bash
install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg chmod 0644 /etc/apt/keyrings/docker.gpg cat >/etc/apt/sources.list.d/docker.list <<EOF deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable EOF curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key \ | gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg chmod 0644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg cat >/etc/apt/sources.list.d/kubernetes.list <<'EOF' deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ / EOF apt-get update

5.3. Khóa phiên bản một lần, rồi dùng cùng file trên 6 node

Chỉ trên CP1, chọn phiên bản từ repository đã xác thực:

bash
bash <<'SCRIPT' set -euo pipefail K8S_DEB_VERSION="$(apt-cache madison kubeadm | awk '$3 ~ /^1\.36\.5-/ {print $3; exit}')" CONTAINERD_DEB_VERSION="$(apt-cache madison containerd.io | awk '$3 ~ /^2\.3\./ {print $3; exit}')" test -n "$K8S_DEB_VERSION" test -n "$CONTAINERD_DEB_VERSION" cat >/root/k8s-versions.env <<EOF K8S_VERSION='v1.36.5' K8S_DEB_VERSION='$K8S_DEB_VERSION' CONTAINERD_DEB_VERSION='$CONTAINERD_DEB_VERSION' EOF chmod 0600 /root/k8s-versions.env cat /root/k8s-versions.env SCRIPT

Nếu không tìm thấy package, dừng để chọn bản vá hợp lệ và cập nhật bộ phiên bản; không bỏ pin rồi cài tùy ý. Chuyển chính file /root/k8s-versions.env bằng kênh quản trị tới 5 node còn lại. Lưu package đã chọn trong artifact mirror để tái dựng được về sau.

5.4. Cài runtime và công cụ Kubernetes

Trên cả 6 node, chạy nguyên khối sau trong root shell. Chỉ dùng cho node mới; không ghi đè runtime của node đang có workload.

bash
bash <<'SCRIPT' set -euo pipefail source /root/k8s-versions.env : "${K8S_DEB_VERSION:?}" : "${CONTAINERD_DEB_VERSION:?}" apt-get install -y "containerd.io=${CONTAINERD_DEB_VERSION}" \ "kubelet=${K8S_DEB_VERSION}" "kubeadm=${K8S_DEB_VERSION}" \ "kubectl=${K8S_DEB_VERSION}" cri-tools kubernetes-cni apt-mark hold containerd.io kubelet kubeadm kubectl install -d -m 0755 /etc/containerd containerd config default >/etc/containerd/config.toml python3 - <<'PY' import pathlib, re, tomllib p = pathlib.Path('/etc/containerd/config.toml') s = p.read_text() s, count = re.subn(r'SystemdCgroup\s*=\s*false', 'SystemdCgroup = true', s) if not count and 'SystemdCgroup = true' not in s: raise SystemExit('Không tìm thấy SystemdCgroup; kiểm tra schema containerd.') cfg = tomllib.loads(s) section = cfg['plugins']['io.containerd.cri.v1.runtime'] assert section['containerd']['runtimes']['runc']['options']['SystemdCgroup'] is True assert 'cri' not in cfg.get('disabled_plugins', []) assert 'io.containerd.cri.v1.runtime' not in cfg.get('disabled_plugins', []) p.write_text(s) PY cat >/etc/crictl.yaml <<'EOF' runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 debug: false EOF systemctl enable --now containerd systemctl restart containerd systemctl enable kubelet crictl info >/dev/null for plugin in bridge host-local loopback portmap; do test -x "/opt/cni/bin/${plugin}" done dpkg-query -W containerd.io kubeadm kubelet kubectl cri-tools kubernetes-cni SCRIPT

Kubelet có thể restart khi chưa có cấu hình từ kubeadm; kiểm tra lại sau init/join. Reboot 6 node trước bước tiếp theo và xác nhận swap vẫn tắt, thời gian đồng bộ, containerd hoạt động.

6. Chuẩn bị encryption và audit trước khi init

Chỉ trên CP1, root shell:

bash
bash <<'SCRIPT' set -euo pipefail umask 077 install -d -m 0700 /etc/kubernetes/encryption /etc/kubernetes/audit install -d -m 0700 /var/log/kubernetes/audit test ! -e /etc/kubernetes/encryption/config.yaml ENCRYPTION_KEY="$(openssl rand -base64 32)" cat >/etc/kubernetes/encryption/config.yaml <<EOF apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key-202610 secret: ${ENCRYPTION_KEY} - identity: {} EOF unset ENCRYPTION_KEY cat >/etc/kubernetes/audit/policy.yaml <<'EOF' apiVersion: audit.k8s.io/v1 kind: Policy omitStages: - RequestReceived rules: - level: None nonResourceURLs: - /healthz* - /livez* - /readyz* - level: Metadata EOF chmod 0600 /etc/kubernetes/encryption/config.yaml /etc/kubernetes/audit/policy.yaml SCRIPT

Chuyển hai file trên bằng kênh quản trị bảo mật tới cùng đường dẫn trên CP2 và CP3 trước khi join. Trên hai máy đó cũng tạo các thư mục với mode tương ứng. Ba bản encryption config phải có cùng checksum; không tự sinh key riêng cho mỗi control plane.

bash
sha256sum /etc/kubernetes/encryption/config.yaml chmod 0600 /etc/kubernetes/encryption/config.yaml /etc/kubernetes/audit/policy.yaml install -d -m 0700 /var/log/kubernetes/audit

Encryption ở đây bảo vệ Secrets trong etcd. Key nằm trên host nên cần backup mã hóa, phân quyền host và quy trình xoay key; đây không thay thế KMS hoặc mã hóa ổ đĩa. Audit chỉ ghi Metadata để không ghi payload Secrets vào log. Gửi audit log từ cả ba control plane tới hệ thống log ngoài cluster và giám sát dung lượng.

7. Khởi tạo control plane đầu tiên

Trên CP1 tạo /root/kubeadm-init.yaml:

yaml
apiVersion: kubeadm.k8s.io/v1beta4 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.76.10 bindPort: 6443 nodeRegistration: name: k8s-cp-1 criSocket: unix:///run/containerd/containerd.sock kubeletExtraArgs: - name: node-ip value: "192.168.76.10" skipPhases: - addon/kube-proxy --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: v1.36.5 clusterName: k8s-prod controlPlaneEndpoint: k8s-api.example.internal:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 dnsDomain: cluster.local apiServer: certSANs: - k8s-api.example.internal - 192.168.76.9 extraArgs: - name: encryption-provider-config value: /etc/kubernetes/encryption/config.yaml - name: audit-policy-file value: /etc/kubernetes/audit/policy.yaml - name: audit-log-path value: /var/log/kubernetes/audit/audit.log - name: audit-log-maxage value: "14" - name: audit-log-maxbackup value: "10" - name: audit-log-maxsize value: "100" extraVolumes: - name: encryption-config hostPath: /etc/kubernetes/encryption mountPath: /etc/kubernetes/encryption readOnly: true pathType: Directory - name: audit-policy hostPath: /etc/kubernetes/audit mountPath: /etc/kubernetes/audit readOnly: true pathType: Directory - name: audit-logs hostPath: /var/log/kubernetes/audit mountPath: /var/log/kubernetes/audit readOnly: false pathType: DirectoryOrCreate etcd: local: dataDir: /var/lib/etcd --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd failSwapOn: true rotateCertificates: true containerLogMaxSize: 20Mi containerLogMaxFiles: 5 systemReserved: cpu: 500m memory: 512Mi kubeReserved: cpu: 500m memory: 1Gi evictionHard: memory.available: 500Mi nodefs.available: "10%" nodefs.inodesFree: "5%" imagefs.available: "15%"

Reservation áp dụng cho cả 6 node; điều chỉnh theo sizing thực tế trước bootstrap. Cilium thay kube-proxy trong mô hình này; không cài thêm Flannel/Calico hoặc chạy lại phase addon kube-proxy.

Kiểm tra cấu hình, tải image và init:

bash
chmod 0600 /root/kubeadm-init.yaml kubeadm config validate --config /root/kubeadm-init.yaml kubeadm config images pull --config /root/kubeadm-init.yaml kubeadm init --config /root/kubeadm-init.yaml --upload-certs

Lưu join command và certificate key trong nơi quản trị bảo mật, không tee ra file log có thể đọc rộng. Chỉ CP1 chạy init; mọi máy khác dùng join.

8. Join hai control plane còn lại và worker

8.1. Kubeconfig quản trị

Trên CP1, root shell:

bash
export KUBECONFIG=/etc/kubernetes/admin.conf kubectl get nodes -o wide

Khi mở root shell mới trên CP1, đặt lại biến trên. Nếu dùng user vận hành thông thường, chép kubeconfig vào ~/.kube/config của đúng user và đặt mode 0600. Đây là quyền cluster-admin dành cho bootstrap/break-glass; người dùng hằng ngày cần identity và RBAC riêng.

8.2. Join CP2 và CP3, từng máy một

Trước join, xác nhận hai file encryption/audit đã có và đúng quyền. Với host nhiều interface, cấu hình kubelet chọn đúng InternalIP; ví dụ chỉ trên CP2:

bash
printf '%s\n' 'KUBELET_EXTRA_ARGS="--node-ip=192.168.76.11"' >/etc/default/kubelet

CP3/worker thay bằng IP của chính máy đó. Dùng lệnh join control plane kubeadm vừa in. Ví dụ trên CP2, điền token/hash/key thật:

bash
kubeadm join k8s-api.example.internal:6443 \ --token REPLACE_WITH_TOKEN \ --discovery-token-ca-cert-hash sha256:REPLACE_WITH_CA_HASH \ --control-plane \ --certificate-key REPLACE_WITH_CERTIFICATE_KEY \ --apiserver-advertise-address=192.168.76.11 \ --node-name=k8s-cp-2 \ --cri-socket=unix:///run/containerd/containerd.sock

CP3 đổi IP thành 192.168.76.12 và node name thành k8s-cp-3.

Trên CP1 kiểm tra API/etcd static Pods của node vừa join trước khi join control plane tiếp theo. NotReady do chưa có CNI là dự kiến ở giai đoạn này.

8.3. Join 3 worker

Lấy command mới trên CP1 khi cần:

bash
kubeadm token create --ttl 1h --print-join-command

Trên mỗi worker, chạy command nhận được và thêm node name/runtime; ví dụ worker 1:

bash
kubeadm join k8s-api.example.internal:6443 \ --token REPLACE_WITH_TOKEN \ --discovery-token-ca-cert-hash sha256:REPLACE_WITH_CA_HASH \ --node-name=k8s-worker-1 \ --cri-socket=unix:///run/containerd/containerd.sock

Worker nhiều interface cũng đặt KUBELET_EXTRA_ARGS với IP riêng trước join. Worker không nhận certificate key, encryption key hay admin kubeconfig.

Certificate upload cho control-plane join mặc định chỉ tồn tại khoảng 2 giờ. Nếu hết hạn, trên CP1 chạy kubeadm init phase upload-certs --upload-certs để tạo lại; không chạy lại toàn bộ init. Token join thông thường cũng có hạn; dùng --ttl ngắn và xóa token không còn dùng sau bootstrap.

9. Cài Helm, Gateway CRD và Cilium

Trên CP1 trong root shell đã đặt KUBECONFIG.

9.1. Helm CLI

bash
bash <<'SCRIPT' set -euo pipefail HELM_VERSION=v4.3.1 WORKDIR="$(mktemp -d)" trap 'rm -rf "$WORKDIR"' EXIT cd "$WORKDIR" ARCHIVE="helm-${HELM_VERSION}-linux-amd64.tar.gz" curl -fSLO "https://get.helm.sh/${ARCHIVE}" curl -fSLO "https://get.helm.sh/${ARCHIVE}.sha256sum" sha256sum -c "${ARCHIVE}.sha256sum" tar -xzf "$ARCHIVE" install -m 0755 linux-amd64/helm /usr/local/bin/helm helm version --short SCRIPT

9.2. Gateway CRD

Chỉ cài CRD từ tag cố định phù hợp với Cilium. Các dependency bên dưới gồm TLSRoute theo yêu cầu bản Cilium tham chiếu:

bash
bash <<'SCRIPT' set -euo pipefail GATEWAY_API_VERSION=v1.6.3 for resource in gatewayclasses gateways httproutes referencegrants grpcroutes backendtlspolicies tlsroutes; do kubectl apply --server-side -f \ "https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/${GATEWAY_API_VERSION}/config/crd/standard/gateway.networking.k8s.io_${resource}.yaml" done SCRIPT

9.3. Cilium

Tạo /root/cilium-values.yaml:

yaml
kubeProxyReplacement: true k8sServiceHost: k8s-api.example.internal k8sServicePort: "6443" ipam: mode: kubernetes routingMode: tunnel tunnelProtocol: vxlan ipv4: enabled: true ipv6: enabled: false operator: replicas: 2 l7Proxy: true gatewayAPI: enabled: true hostNetwork: enabled: true nodes: matchLabels: gateway: enabled

Kubernetes cấp PodCIDR theo từng node; không cấu hình thêm cluster-pool IPAM với CIDR khác. k8sServiceHost là endpoint HA để Cilium bootstrap khi chưa có kube-proxy. Host network cho phép Gateway dùng worker IP, không cần cài MetalLB chỉ để có external IP.

bash
helm repo add cilium https://helm.cilium.io/ helm repo update helm upgrade --install cilium cilium/cilium \ --version 1.20.3 --namespace kube-system \ -f /root/cilium-values.yaml --wait --timeout 10m kubectl label nodes k8s-worker-1 k8s-worker-2 k8s-worker-3 \ node-role.kubernetes.io/worker=true gateway=enabled --overwrite kubectl -n kube-system rollout status daemonset/cilium --timeout=10m kubectl -n kube-system rollout status deployment/cilium-operator --timeout=10m kubectl wait --for=condition=Ready nodes --all --timeout=10m kubectl get nodes -o wide kubectl get pods -A -o wide kubectl -n kube-system get daemonset kube-proxy

Lệnh cuối phải báo không tìm thấy kube-proxy trong mô hình này. Kiểm tra thêm cilium status bằng Cilium CLI khi có; cài CLI theo release cố định từ nguồn chính thức, không dùng CLI chưa rõ phiên bản.

10. Xác nhận HA và DNS hệ thống

bash
kubectl -n kube-system get pods -l component=etcd -o wide kubectl -n kube-system get pods -l component=kube-apiserver -o wide kubectl get --raw='/readyz?verbose' kubectl -n kube-system rollout status deployment/coredns --timeout=5m kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide

Kỳ vọng: 3 etcd, 3 API server, 6 node Ready; ít nhất 2 CoreDNS replica nằm trên các worker khác nhau. Nếu CoreDNS dồn một node, chỉnh affinity/topology spread trong manifest quản lý của bạn và lưu thay đổi để áp dụng lại sau upgrade. Không coi Running là đã đạt mọi tiêu chí mạng.

Trên mỗi control plane kiểm tra local etcd bằng binary trong container cùng phiên bản:

bash
ETCD_CONTAINER_ID="$(crictl ps --name etcd -q | head -n 1)" test -n "$ETCD_CONTAINER_ID" crictl exec "$ETCD_CONTAINER_ID" etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \ --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \ endpoint health --cluster

Giữ RBAC và NodeRestriction do kubeadm thiết lập mặc định. Kiểm tra taint control plane vẫn còn:

bash
kubectl describe node k8s-cp-1 kubeadm certs check-expiration

11. Chính sách workload và kiểm thử mạng

11.1. Namespace production và quyền truy cập

bash
kubectl create namespace production kubectl label namespace production \ pod-security.kubernetes.io/enforce=restricted \ pod-security.kubernetes.io/enforce-version=v1.36 \ pod-security.kubernetes.io/audit=restricted \ pod-security.kubernetes.io/warn=restricted

Chỉ áp dụng restricted cho namespace ứng dụng. Cilium/CSI có nhu cầu đặc quyền nên không áp chính sách này hàng loạt vào namespace hệ thống. Dùng OIDC với identity provider của tổ chức cho người dùng; bind group vào Role/ClusterRole theo nhu cầu. Ví dụ quyền đọc workload trong một namespace, không cấp đọc Secrets:

yaml
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: workload-reader namespace: production rules: - apiGroups: [""] resources: [pods, pods/log, services, endpoints, events, persistentvolumeclaims] verbs: [get, list, watch] - apiGroups: [apps] resources: [deployments, replicasets, statefulsets, daemonsets] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: developers-read namespace: production subjects: - kind: Group name: developers apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: workload-reader apiGroup: rbac.authorization.k8s.io

Lưu thành /root/workload-rbac.yaml, rồi kubectl apply -f /root/workload-rbac.yaml. Group chỉ có tác dụng khi hệ thống xác thực thực sự phát hành group developers; manifest không tự tạo tài khoản. Không phân phối admin.conf cho developer/CI.

Kiểm tra Secrets thực sự được mã hóa, dùng một marker thử không nhạy cảm. Từ CP1 tạo Secret rồi đọc trực tiếp record etcd bằng TLS client quản trị:

bash
kubectl -n production create secret generic encryption-probe --from-literal=marker=probe-only ETCD_CONTAINER_ID="$(crictl ps --name etcd -q | head -n 1)" crictl exec "$ETCD_CONTAINER_ID" etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \ --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \ get /registry/secrets/production/encryption-probe --print-value-only \ | od -An -tc -N 32 kubectl -n production delete secret encryption-probe

Kỳ vọng prefix dạng k8s:enc:aescbc:v1:; kiểm tra đọc Secret qua API của cả ba control plane cũng thành công. Không dùng Secret nghiệp vụ làm marker test. Khi thay key, cần kiểm tra lại với snapshot cũ còn trong retention.

11.2. Ứng dụng nghiệm thu

Tạo /root/smoke.yaml. Image là ví dụ có tag cố định; trước workload thật phải duyệt image, quét lỗ hổng và khóa digest trong registry của tổ chức.

yaml
apiVersion: apps/v1 kind: Deployment metadata: name: web namespace: production spec: replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: automountServiceAccountToken: false nodeSelector: node-role.kubernetes.io/worker: "true" securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: web containers: - name: web image: busybox:1.37.0 command: [sh, -c] args: - 'printf "ok %s\n" "$HOSTNAME" >/www/index.html; exec httpd -f -p 8080 -h /www' ports: - containerPort: 8080 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ALL] resources: requests: cpu: 50m memory: 32Mi limits: cpu: 250m memory: 64Mi readinessProbe: httpGet: path: / port: 8080 livenessProbe: httpGet: path: / port: 8080 initialDelaySeconds: 5 volumeMounts: - name: www mountPath: /www volumes: - name: www emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: web namespace: production spec: selector: app: web ports: - name: http port: 80 targetPort: 8080 --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: web namespace: production spec: minAvailable: 2 selector: matchLabels: app: web
bash
kubectl apply -f /root/smoke.yaml kubectl -n production rollout status deployment/web --timeout=5m kubectl -n production get pods -o wide

Tạo /root/network-policy.yaml:

yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny namespace: production spec: podSelector: {} policyTypes: [Ingress, Egress] --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-dns namespace: production spec: podSelector: {} policyTypes: [Egress] egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53 --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: client-to-web namespace: production spec: podSelector: matchLabels: access: web policyTypes: [Egress] egress: - to: - podSelector: matchLabels: app: web ports: - protocol: TCP port: 8080 --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: web-from-client namespace: production spec: podSelector: matchLabels: app: web policyTypes: [Ingress] ingress: - from: - podSelector: matchLabels: access: web ports: - protocol: TCP port: 8080
bash
kubectl apply -f /root/network-policy.yaml

Tạo /root/network-clients.yaml với hai Pod đặt trên worker 2 để kiểm thử từ xa:

yaml
apiVersion: v1 kind: Pod metadata: name: allowed-client namespace: production labels: access: web spec: nodeSelector: kubernetes.io/hostname: k8s-worker-2 automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: client image: busybox:1.37.0 command: [sh, -c, "sleep 86400"] resources: requests: {cpu: 10m, memory: 16Mi} limits: {cpu: 100m, memory: 32Mi} securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ALL] --- apiVersion: v1 kind: Pod metadata: name: denied-client namespace: production spec: nodeSelector: kubernetes.io/hostname: k8s-worker-2 automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: client image: busybox:1.37.0 command: [sh, -c, "sleep 86400"] resources: requests: {cpu: 10m, memory: 16Mi} limits: {cpu: 100m, memory: 32Mi} securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ALL]
bash
kubectl apply -f /root/network-clients.yaml kubectl -n production wait --for=condition=Ready pod/allowed-client pod/denied-client --timeout=5m kubectl -n production exec allowed-client -- nslookup kubernetes.default.svc.cluster.local kubectl -n production exec allowed-client -- wget -T 5 -qO- http://web.production.svc.cluster.local kubectl -n production exec denied-client -- wget -T 5 -qO- http://web.production.svc.cluster.local

Hai lệnh đầu thành công; lệnh cuối phải bị chặn/timeout. Lấy Pod IP của một web Pod trên worker 1 hoặc 3 rồi gọi http://POD_IP:8080 từ allowed-client để kiểm chứng cross-node, không chỉ Service trên cùng node. Nếu có Cilium CLI, chạy thêm cilium connectivity test trong namespace riêng và dọn tài nguyên test sau đó.

12. Persistent storage với Longhorn

Phần này cung cấp lựa chọn storage cụ thể cho mô hình VM/bare metal. Nếu tổ chức đã có CSI cho SAN/Ceph hoặc cloud block storage, dùng CSI đó và StorageClass tương ứng; không cài chồng Longhorn chỉ để theo ví dụ.

12.1. Chuẩn bị 3 worker

Mỗi worker cần SSD dữ liệu riêng đã format ext4/XFS, mount bền vững bằng UUID tại /var/lib/longhorn. Provision disk theo quy trình hạ tầng của bạn; xác nhận đúng thiết bị trước thao tác format. Không dùng disk OS làm nơi lưu replica trong mô hình này. Dành thêm capacity cho replica, snapshot và rebuild; dung lượng khả dụng nhỏ hơn tổng dung lượng raw.

Trên từng worker, root shell:

bash
findmnt --mountpoint /var/lib/longhorn df -h /var/lib/longhorn apt-get update apt-get install -y open-iscsi nfs-common cryptsetup dmsetup modprobe iscsi_tcp printf '%s\n' iscsi_tcp >/etc/modules-load.d/longhorn.conf systemctl enable --now iscsid iscsiadm --version

Không tiếp tục nếu findmnt không xác nhận mount riêng hoặc mount không còn sau reboot. Longhorn cảnh báo không dùng open-iscsi 2.1.12; kiểm tra bản host và dùng bản không bị ảnh hưởng. Nếu host dùng multipath, áp dụng cấu hình loại trừ thiết bị Longhorn theo tài liệu hãng trước khi attach volume.

Trên CP1:

bash
kubectl label nodes k8s-worker-1 k8s-worker-2 k8s-worker-3 \ storage=enabled node.longhorn.io/create-default-disk=true --overwrite kubectl create namespace longhorn-system kubectl label namespace longhorn-system pod-security.kubernetes.io/enforce=privileged

Namespace đặc quyền chỉ dành cho CSI/Longhorn; giới hạn ai được tạo Pod trong namespace này.

12.2. Cài đặt

Tạo /root/longhorn-values.yaml:

yaml
persistence: defaultClass: false defaultSettings: defaultDataPath: /var/lib/longhorn createDefaultDiskLabeledNodes: true defaultReplicaCount: 3 replicaSoftAntiAffinity: false allowVolumeCreationWithDegradedAvailability: false storageOverProvisioningPercentage: 100 storageMinimalAvailablePercentage: 25 systemManagedComponentsNodeSelector: "storage:enabled" v1DataEngine: true v2DataEngine: false longhornManager: nodeSelector: storage: enabled longhornDriver: nodeSelector: storage: enabled
bash
helm repo add longhorn https://charts.longhorn.io helm repo update helm upgrade --install longhorn longhorn/longhorn \ --namespace longhorn-system --version 1.13.0 \ -f /root/longhorn-values.yaml --wait --timeout 15m kubectl -n longhorn-system get pods -o wide kubectl -n longhorn-system get nodes.longhorn.io

Tạo /root/storage-class.yaml:

yaml
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: longhorn-retain provisioner: driver.longhorn.io allowVolumeExpansion: true reclaimPolicy: Retain volumeBindingMode: Immediate parameters: numberOfReplicas: "3" staleReplicaTimeout: "2880" fsType: ext4 dataEngine: v1
bash
kubectl apply -f /root/storage-class.yaml kubectl get storageclass

Workload phải khai báo storageClassName: longhorn-retain. Retain giữ PV sau khi xóa PVC, không thay thế backup; đội vận hành phải có quy trình reclaim/xóa dữ liệu chủ động.

12.3. Nghiệm thu dữ liệu qua việc tạo lại Pod

Tạo /root/storage-smoke.yaml:

yaml
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: storage-smoke namespace: production spec: accessModes: [ReadWriteOnce] storageClassName: longhorn-retain resources: requests: storage: 1Gi --- apiVersion: v1 kind: Pod metadata: name: storage-smoke namespace: production spec: nodeSelector: storage: enabled automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: client image: busybox:1.37.0 command: [sh, -c, "sleep 86400"] resources: requests: {cpu: 10m, memory: 16Mi} limits: {cpu: 100m, memory: 32Mi} securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ALL] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: storage-smoke
bash
kubectl apply -f /root/storage-smoke.yaml kubectl -n production wait --for=condition=Ready pod/storage-smoke --timeout=10m kubectl -n production exec storage-smoke -- sh -c 'echo persistent-ok >/data/check.txt' kubectl -n production delete pod storage-smoke --wait=true kubectl apply -f /root/storage-smoke.yaml kubectl -n production wait --for=condition=Ready pod/storage-smoke --timeout=10m kubectl -n production exec storage-smoke -- cat /data/check.txt

Kỳ vọng persistent-ok và volume Healthy với 3 replica trên 3 worker khác nhau. Đây mới kiểm tra persistence qua Pod replacement; phải làm thêm bài attach lại trên worker khác và phục hồi từ offsite backup trước workload có dữ liệu thật. Không force-detach khi chưa fence được node cũ đang có khả năng ghi volume.

13. HTTPS qua Gateway API

13.1. Certificate

Thay app.example.com bằng domain thật trong toàn bộ phần này. Chuẩn bị certificate chain và private key do CA được client tin cậy cấp. Với public service dùng public CA; với nội bộ có thể dùng CA doanh nghiệp. Ví dụ tạo Secret từ hai file đã có:

bash
kubectl -n production create secret tls app-tls \ --cert=/root/tls/fullchain.pem --key=/root/tls/privkey.pem

Certificate phải có SAN đúng domain và còn hạn. Cần tự động hóa renewal bằng cert-manager/tích hợp PKI của tổ chức hoặc có lịch thay cert đã kiểm chứng; không dùng certificate tự ký tạm thời để nghiệm thu production public.

13.2. Gateway và route

Tạo /root/gateway.yaml:

yaml
apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: application-gateway namespace: production spec: gatewayClassName: cilium listeners: - name: http hostname: app.example.com protocol: HTTP port: 8080 allowedRoutes: namespaces: from: Same - name: https hostname: app.example.com protocol: HTTPS port: 8443 tls: mode: Terminate certificateRefs: - kind: Secret name: app-tls allowedRoutes: namespaces: from: Same --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: redirect-to-https namespace: production spec: parentRefs: - name: application-gateway sectionName: http hostnames: [app.example.com] rules: - filters: - type: RequestRedirect requestRedirect: scheme: https port: 443 statusCode: 301 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: web namespace: production spec: parentRefs: - name: application-gateway sectionName: https hostnames: [app.example.com] rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: web port: 80 --- apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: name: web-from-gateway namespace: production spec: endpointSelector: matchLabels: app: web ingress: - fromEntities: - ingress toPorts: - ports: - port: "8080" protocol: TCP
bash
kubectl apply -f /root/gateway.yaml kubectl -n production get gateway,httproute kubectl -n production describe gateway application-gateway kubectl -n production describe httproute web

Gateway phải Accepted=True, Programmed=True; HTTPRoute Accepted=True, ResolvedRefs=True. Cilium policy cho phép identity ingress tới web, không mở toàn bộ internet tới backend Pod. Các listener host network dùng 8080/8443 để không cần cấp thêm quyền bind cổng thấp cho Envoy. Dùng Gateway chung và chia route khi thêm domain; không tạo các Gateway host-network cạnh tranh cùng port trên cùng node.

13.3. Thêm frontend ứng dụng vào hai LB

Thêm cùng đoạn sau vào /etc/haproxy/haproxy.cfg trên cả hai LB:

haproxy
frontend application_http bind :80 default_backend application_http_workers backend application_http_workers balance roundrobin option tcp-check server worker1 192.168.76.21:8080 check server worker2 192.168.76.22:8080 check server worker3 192.168.76.23:8080 check frontend application_https bind :443 default_backend application_https_workers backend application_https_workers balance roundrobin option tcp-check server worker1 192.168.76.21:8443 check server worker2 192.168.76.22:8443 check server worker3 192.168.76.23:8443 check
bash
haproxy -c -f /etc/haproxy/haproxy.cfg systemctl reload haproxy

Ở đây TLS kết thúc tại Cilium Envoy; HAProxy chuyển TCP nguyên trạng, không dùng PROXY protocol. Ứng dụng sẽ không tự nhận được IP client thật chỉ nhờ cấu hình này; nếu cần, phải thiết kế thêm cơ chế truyền IP, giới hạn trusted proxy và kiểm tra chống giả mạo header.

Từ máy client có đường tới VIP:

bash
curl -I --resolve app.example.com:80:192.168.76.9 http://app.example.com/ curl --fail --resolve app.example.com:443:192.168.76.9 https://app.example.com/

Kỳ vọng HTTP chuyển sang HTTPS, HTTPS trả ok ... và certificate được xác minh. Với CA nội bộ, cài CA tin cậy hoặc truyền --cacert; không dùng -k để coi bài kiểm tra TLS đã đạt.

14. Backup etcd, PKI và cấu hình ra ngoài cluster

Backup có ba lớp độc lập:

LớpNội dung cần giữCách phục hồi
Trạng thái clusterSnapshot etcdKhôi phục control plane/metadata Kubernetes
Danh tính và cấu hìnhPKI, encryption key, kubeadm config, cấu hình từng node/LBPhục hồi đúng trust và khả năng giải mã
Dữ liệu ứng dụngVolume backup + database-native backup/PITRPhục hồi dữ liệu thực tế

Snapshot etcd không chứa dữ liệu PVC. Replica của Longhorn cũng không bảo vệ trước xóa nhầm hoặc mất cả site.

14.1. Kho backup mã hóa ngoài cluster

Ví dụ dùng restic với S3-compatible object storage độc lập. Trên cả 3 control plane, root shell:

bash
apt-get update apt-get install -y restic install -d -m 0700 /etc/restic /var/backups/kubernetes

Tạo /etc/restic/k8s.env bằng thông tin thật, mode 0600:

bash
RESTIC_REPOSITORY='s3:https://s3.example.com/k8s-backups/cluster-prod' RESTIC_PASSWORD='REPLACE_WITH_STRONG_PASSWORD_FROM_SECRET_MANAGER' AWS_ACCESS_KEY_ID='REPLACE_WITH_BACKUP_ACCESS_KEY' AWS_SECRET_ACCESS_KEY='REPLACE_WITH_BACKUP_SECRET_KEY'

Không dùng nguyên placeholder. Chỉ cấp quyền bucket cần thiết; lưu mật khẩu restic trong secret manager ngoài cluster. Dùng retention/Object Lock theo quy định của tổ chức; không giữ credential có quyền phá toàn bộ backup trên host nếu có thể tách quyền ghi và retention.

Khởi tạo repository một lần trên một control plane:

bash
chmod 0600 /etc/restic/k8s.env set -a source /etc/restic/k8s.env set +a restic init

Nếu repository đã tồn tại, bỏ init, dùng restic snapshots để xác nhận kết nối.

14.2. Script backup

Trên mỗi control plane, tạo /usr/local/sbin/backup-kubernetes:

bash
#!/usr/bin/env bash set -euo pipefail umask 077 exec 9>/run/backup-kubernetes.lock flock -n 9 || exit 0 set -a source /etc/restic/k8s.env set +a NODE="$(hostname -s)" WORKDIR=/var/backups/kubernetes/current install -d -m 0700 "$WORKDIR" EXPORT_PATH=/var/lib/etcd/backup-export.db trap 'rm -f "$EXPORT_PATH" "$EXPORT_PATH.part"' EXIT test ! -e "$EXPORT_PATH" ETCD_CONTAINER_ID="$(crictl ps --name etcd -q | head -n 1)" test -n "$ETCD_CONTAINER_ID" # Đường dẫn này được mount vào etcd container; chuyển snapshot ra ngoài # data directory ngay sau khi tạo, không giữ lịch sử backup tại đây. crictl exec "$ETCD_CONTAINER_ID" etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \ --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \ snapshot save "$EXPORT_PATH" crictl exec "$ETCD_CONTAINER_ID" etcdutl \ snapshot status "$EXPORT_PATH" -w json >"$WORKDIR/snapshot-status.json" mv "$EXPORT_PATH" "$WORKDIR/snapshot.db" tar -C / -czf "$WORKDIR/control-plane-files.tar.gz" \ etc/kubernetes etc/containerd/config.toml etc/crictl.yaml if test -f /root/kubeadm-init.yaml; then cp /root/kubeadm-init.yaml "$WORKDIR/" fi cp /root/k8s-versions.env "$WORKDIR/" crictl exec "$ETCD_CONTAINER_ID" etcd --version >"$WORKDIR/etcd-version.txt" dpkg-query -W containerd.io kubelet kubeadm kubectl >"$WORKDIR/packages.txt" date -u +%FT%TZ >"$WORKDIR/created-at.txt" restic backup "$WORKDIR" --host "$NODE" --tag k8s-prod rm -rf -- "$WORKDIR"

Script dùng binary etcdctl/etcdutl trong etcd image thực tế, tránh trộn client khác phiên bản. Kiểm tra image cung cấp hai binary này trước khi bật timer. Mỗi control plane backup cả PKI/config của chính node đó. Đường dẫn backup cố định giúp retention theo host/path hoạt động nhất quán; flock ngăn hai job trên cùng máy ghi chồng nhau. Nếu upload lỗi, thư mục làm việc được giữ để điều tra và lần chạy sau sẽ cập nhật lại; giám sát dung lượng disk.

bash
chmod 0700 /usr/local/sbin/backup-kubernetes /usr/local/sbin/backup-kubernetes set -a source /etc/restic/k8s.env set +a restic snapshots --tag k8s-prod

14.3. Lịch chạy

Trên mỗi control plane, tạo /etc/systemd/system/backup-kubernetes.service:

ini
[Unit] Description=Encrypted offsite Kubernetes control-plane backup Wants=network-online.target After=network-online.target containerd.service [Service] Type=oneshot User=root UMask=0077 TimeoutStartSec=30min ExecStart=/usr/local/sbin/backup-kubernetes

Tạo /etc/systemd/system/backup-kubernetes.timer:

ini
[Unit] Description=Hourly Kubernetes backup [Timer] OnCalendar=hourly RandomizedDelaySec=10min Persistent=true [Install] WantedBy=timers.target
bash
systemctl daemon-reload systemctl enable --now backup-kubernetes.timer systemctl list-timers backup-kubernetes.timer journalctl -u backup-kubernetes.service -n 50 --no-pager

Lịch này cho RPO tham chiếu xấp xỉ một giờ cộng thời gian thực thi khi backup thành công; đặt lịch ngắn hơn nếu nghiệp vụ yêu cầu. Bắt buộc alert khi job lỗi hoặc không có backup thành công trong hơn 2 giờ. Chạy restic check, retention và prune bằng job quản trị riêng đã kiểm thử; không để repository tăng vô hạn. Lưu thêm cấu hình LB, Helm values và manifest ứng dụng trong Git riêng không chứa secret.

14.4. Restore drill

Trước production, phục hồi thử trên mạng cô lập với RPO/RTO đã thống nhất:

  1. Dùng credential ngoài cluster để restic restore <SNAPSHOT_ID> --target <RESTORE_DIR>. Chọn một snapshot etcd tốt làm nguồn cho cả ba member; kiểm tra metadata/hash bằng etcdutl cùng phiên bản etcd ghi trong backup.
  2. Fencing/cô lập các member cũ để chúng không tham gia mạng của cụm phục hồi. Dừng API server và etcd cũ; di chuyển static Pod manifest ra ngoài /etc/kubernetes/manifests khi kubelet còn chạy, xác nhận container đã dừng rồi mới dừng kubelet. Không chỉ dừng kubelet và giả định container đã dừng.
  3. Khôi phục PKI/config đúng node, encryption key tương ứng snapshot, DNS/VIP và quyền file. Không sao chép certificate riêng của CP1 lên mọi control plane. Nếu thay IP/name, phải lập lại membership và cấp certificate có SAN phù hợp.
  4. Trên từng control plane, dùng etcdutl snapshot restore với cùng snapshot và initial-cluster, nhưng --name, --initial-advertise-peer-urls, data directory riêng của node. Với etcd 3.6 cho Kubernetes, áp dụng revision bump và --mark-compacted theo tài liệu etcd để tránh informer dùng cache cũ. Giá trị bump phải bao phủ số write kể từ snapshot.
  5. Gắn data directory mới vào static etcd manifest theo runbook; khởi động lại etcd rồi API server trên cả ba control plane. Xác nhận quorum/endpoint health, Secrets đọc được, node và controller phục hồi.
  6. Khôi phục PVC/database từ backup riêng; chạy lại DNS, policy, HTTPS và bài kiểm tra dữ liệu. Ghi thời gian phục hồi đo được và các bước cần sửa.

Không dùng kubeadm reset, xóa etcd directory hoặc --force-new-cluster như một cách sửa lỗi thông thường. Mất một member khi quorum còn khỏe là bài thay member, không phải restore toàn bộ cluster.

15. Backup volume và database

Trong Longhorn, cấu hình Backup Target tới object storage ngoài cluster, dùng credential riêng với etcd backup. Chỉ truy cập UI qua VPN/port-forward hoặc ingress có xác thực:

bash
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8081:80

Port-forward mặc định bind loopback của máy chạy lệnh; khi chạy trên server, dùng SSH tunnel từ máy quản trị. Không đổi sang --address 0.0.0.0 để mở UI công khai.

Sau khi target kết nối thành công:

  1. Tạo recurring backup job, ví dụ mỗi ngày, retain tối thiểu 14 bản hoặc theo RPO/retention nghiệp vụ.
  2. Gán job/group vào tất cả volume cần bảo vệ, không chỉ tạo job mà quên gán volume.
  3. Xác nhận có bản backup trong object storage; restore sang PVC mới và kiểm tra dữ liệu.
  4. Với PostgreSQL/MySQL, triển khai thêm backup/PITR và quy trình quiesce phù hợp; snapshot volume không tự bảo đảm tính nhất quán cấp nghiệp vụ.

Nếu chọn CSI khác, làm bài tương đương theo hệ thống storage đó. Định kỳ diễn tập mất một worker, storage rebuild và mất toàn bộ site; kiểm chứng giới hạn thời gian detach/attach và fencing.

16. Giám sát, cảnh báo và log

Cài monitoring sau storage để metrics có persistent volume. Ví dụ dùng kube-prometheus-stack chart cố định 92.10.1; kiểm tra release notes/schema và helm template trước khi áp dụng nếu chọn bản khác.

Trên CP1 tạo /root/monitoring-values.yaml:

yaml
grafana: enabled: false prometheus: prometheusSpec: replicas: 2 retention: 7d podAntiAffinity: hard podAntiAffinityTopologyKey: kubernetes.io/hostname nodeSelector: node-role.kubernetes.io/worker: "true" resources: requests: cpu: 500m memory: 2Gi limits: memory: 4Gi storageSpec: volumeClaimTemplate: spec: storageClassName: longhorn-retain accessModes: [ReadWriteOnce] resources: requests: storage: 20Gi alertmanager: alertmanagerSpec: replicas: 3 podAntiAffinity: hard podAntiAffinityTopologyKey: kubernetes.io/hostname nodeSelector: node-role.kubernetes.io/worker: "true" storage: volumeClaimTemplate: spec: storageClassName: longhorn-retain accessModes: [ReadWriteOnce] resources: requests: storage: 2Gi
bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm template monitoring prometheus-community/kube-prometheus-stack \ --version 92.10.1 --namespace monitoring \ -f /root/monitoring-values.yaml >/root/monitoring-rendered.yaml helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \ --version 92.10.1 --namespace monitoring --create-namespace \ -f /root/monitoring-values.yaml --wait --timeout 15m kubectl -n monitoring get pods,pvc

Hai Prometheus có dữ liệu riêng; đây không tự tạo một TSDB gộp/deduplicate. Khi cần truy vấn HA tập trung, cấu hình thêm remote-write/Thanos hoặc nền tảng metrics của tổ chức. Có thể dùng Grafana hiện hữu; ví dụ không mở dashboard mặc định.

Chưa hoàn tất monitoring chỉ bằng cài chart. Cấu hình receiver thật trong Alertmanager, lưu credential bằng Secret và gửi test alert tới người trực. Receiver mặc định có thể không gửi thông báo. Xác nhận scrape target và bổ sung endpoint/TLS/RBAC cho etcd, scheduler/controller-manager nếu các metrics mặc định chỉ bind loopback; không mở cổng metrics công khai chỉ để hết cảnh báo.

Tối thiểu phải có các cảnh báo sau:

Phạm viCần theo dõi
Ngoài clusterDNS/VIP, HTTPS ứng dụng, API readyz, chứng chỉ; alert vẫn chạy khi cả cluster hỏng
HAMember etcd/quorum, API server, Cilium agent/operator/Envoy, CoreDNS và LB/VRRP
Tài nguyênDisk/inode, etcd WAL/fsync latency, memory, CPU, Pod Pending/CrashLoopBackOff
StorageReplica/volume degraded, disk pressure, capacity phục vụ rebuild, attach/detach thất bại
BackupThời điểm backup thành công cuối, lỗi job, repository/volume backup, restore drill
Bảo mậtCertificate sắp hết hạn, API audit, RBAC thay đổi bất thường

Cài agent log theo nền tảng của tổ chức trên mọi host/node; gửi container log, kubelet/containerd, HAProxy/Keepalived và audit log ra nơi lưu trữ có retention ngoài cluster. Đặt giới hạn dung lượng và alert cho log/audit. Metrics-server là thành phần riêng nếu ứng dụng cần HPA dựa trên resource metrics; kube-prometheus-stack không thay thế metrics-server.

17. Nghiệm thu trước khi nhận traffic thật

Thực hiện trên staging tương đương trước, sau đó trên cụm mới khi chưa có dữ liệu thật. Lưu kết quả từng bài, thời gian gián đoạn và giới hạn quan sát được.

Bài kiểm traKết quả cần đạt
Bootstrap6 node Ready, 3 etcd/3 API server healthy, không có Pod hệ thống lỗi kéo dài
NetworkDNS, ClusterIP, cross-node Pod traffic hoạt động; policy allow thành công và deny bị chặn
TLSRedirect đúng; chứng chỉ/SAN/chain hợp lệ; có renewal và cảnh báo hạn
LB failoverTắt Keepalived trên LB đang giữ VIP; VIP chuyển sang LB còn lại, API/HTTPS hoạt động lại trong ngưỡng đã chốt
HAProxy failureDừng HAProxy trên LB giữ VIP; health script làm VIP chuyển sang LB còn lại
Một control plane mấtTắt VM hoặc dừng dịch vụ theo runbook; 2 etcd còn quorum, API qua VIP vẫn dùng được
Worker mất/drainApp còn phục vụ từ replica khác; kiểm tra reschedule và storage attach theo RTO
StorageGhi/đọc qua Pod replacement và đổi worker; volume không dồn replica cùng một máy
BackupCó backup ngoài cluster, kiểm tra integrity; restore etcd và PVC/database thử thành công
Vận hànhAlert test đến đúng người, log được lưu, có runbook và lịch patch/cert/backup

Ví dụ kiểm thử VIP sau khi cluster hoàn chỉnh: từ CP1 giữ lệnh kubectl get nodes và từ máy client gọi HTTPS, trên LB đang active chạy systemctl stop keepalived, đo gián đoạn, sau đó systemctl start keepalived. Các TCP session hiện hữu có thể phải reconnect; HA không đồng nghĩa không rớt một request nào.

Không kiểm thử mất hai control plane cùng lúc. Nếu việc bảo trì gặp PDB chặn hoặc volume chưa an toàn để detach, điều tra capacity/replica trước; không mặc định force-delete để vượt qua bài kiểm tra.

18. Vận hành sau triển khai

18.1. Bảo trì từng node

Từ máy quản trị, ví dụ worker 1:

bash
kubectl cordon k8s-worker-1 kubectl drain k8s-worker-1 --ignore-daemonsets --timeout=15m

Nếu drain bị chặn bởi PDB, bare Pod nghiệm thu hoặc emptyDir, xem nguyên nhân và kế hoạch dữ liệu trước khi tiếp tục. Không thêm --force, --disable-eviction hoặc xóa emptyDir một cách mặc định. Dọn bare Pod test trước lịch bảo trì; theo quy trình Longhorn cho replica/engine đang ở node đó.

Sau cập nhật/reboot, chờ node, Cilium và storage khỏe rồi:

bash
kubectl uncordon k8s-worker-1 kubectl get nodes

Với control plane, cập nhật từng máy và xác nhận etcd quorum khỏe sau mỗi máy. Không bật tự động reboot đồng thời.

18.2. Upgrade và certificate

  • Backup và restore drill trước đợt upgrade lớn; xem compatibility Cilium/Gateway API/CSI/monitoring trước khi đổi Kubernetes minor.
  • Theo quy trình kubeadm chính thức; không nhảy qua minor. Control plane trước, worker sau; unhold/install/hold có kiểm soát từng node.
  • Giữ việc skip kube-proxy trong cấu hình/quy trình upgrade vì Cilium đang thay thế nó; kiểm tra không sinh lại addon kube-proxy.
  • Kiểm tra kubeadm certs check-expiration, lập lịch renew theo chính sách PKI; certificate kubeadm và certificate HTTPS ứng dụng là hai vòng đời khác nhau.
  • Xoay encryption key đồng bộ trên mọi API server: thêm key mới, rolling restart, rewrite Secrets, xác minh giải mã, giữ key cũ cho snapshot còn retention. Không xóa key cũ chỉ vì đã đổi key ghi mới.
  • Theo dõi capacity N+1: sau mất/bảo trì một worker vẫn đủ tài nguyên cho workload quan trọng và storage rebuild. Ba worker với ba volume replica sẽ bị degraded khi mất một worker và cần node thay thế để trở lại đủ ba replica.
  • Workload thật cần probes, requests/limits, PDB, topology spread, quota, policy egress và image digest phù hợp; không dùng giá trị của app nghiệm thu làm sizing production.

18.3. Thay node và gỡ cụm

Thay node theo runbook drain → xử lý volume/etcd membership → xóa Node object → reprovision host → join lại. Gỡ cluster là quy trình riêng sau khi đã xác nhận backup/retention và di chuyển workload. Hướng dẫn này không chứa phần flush toàn bộ iptables, xóa hàng loạt PKI hoặc xóa etcd để tránh trộn thao tác phá hủy vào luồng dựng production.

Sau nghiệm thu, xóa riêng các tài nguyên smoke test nếu không dùng nữa; giữ lại và chỉnh sửa policy/namespace/storage theo ứng dụng thật. Do StorageClass dùng Retain, xóa PVC không tự xóa dữ liệu PV; ghi nhận và xử lý volume test theo quy trình reclaim.

19. Xử lý lỗi theo lớp

Hiện tượngKiểm tra trước
API qua VIP không truy cập đượcDNS, VIP trên đúng một LB, VRRP/ARP, HAProxy backend, firewall TCP 6443
API container không lêncrictl ps -a, log container; đường dẫn/mode encryption/audit; certificate; etcd
Node NotReadyjournalctl -u kubelet -n 100, crictl info, Cilium DaemonSet, swap/cgroup
Cross-node timeoutUDP 8472, MTU sau encapsulation, IP/interface node, ACL/forwarding và policy
DNS lỗiCoreDNS Pods/EndpointSlice, policy UDP và TCP 53, Service datapath Cilium
Gateway lỗiCRD/chart compatibility, Gateway/HTTPRoute conditions, Secret TLS, listener 8080/8443 trên worker
PVC Pending/attach lỗiDisk mount, CSI Pods, iscsid, Longhorn node/replica/capacity, version open-iscsi
Audit/backup đầy diskRotation/retention, job upload lỗi, repo ngoài cluster, alert last-success

Lệnh chẩn đoán chung:

bash
kubectl get nodes -o wide kubectl get pods -A -o wide kubectl get events -A --sort-by=.metadata.creationTimestamp kubectl -n kube-system logs daemonset/cilium --tail=100

Trên node có lỗi:

bash
systemctl status containerd kubelet --no-pager journalctl -u containerd -u kubelet -n 100 --no-pager crictl info crictl ps -a swapon --show ip route

20. Tài liệu chính thức