Hướng dẫn triển khai Kubernetes với kubeadm trên CentOS Stream 9: Xây dựng Cluster production-ready với Containerd và Calico CNI

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

Từ chuyện migrate server đến bài học Kubernetes

Hồi CentOS 8 EOL năm 2021, mình phải migrate gấp 5 server sang Rocky Linux trong vòng 1 tuần. Công việc đó không chỉ dạy mình về migration — nó còn buộc mình nhìn lại toàn bộ cách quản lý infrastructure. Chạy container trực tiếp trên từng server, mỗi server cấu hình khác nhau một chút, deploy là cả một đống manual steps… Đó là lúc mình thực sự nghiêm túc với Kubernetes.

CentOS Stream 9 là nền tảng mình chọn cho production — stable hơn Fedora, upstream RHEL, có hỗ trợ chính thức từ Red Hat. EKS hay GKE tiện hơn, nhưng tốn thêm ~$70-150/tháng chỉ riêng cluster fee. Với kubeadm + Containerd + Calico, cluster tự quản trên VPS rẻ hơn đáng kể mà vẫn đủ production-ready.

Containerd và Calico: Tại sao chọn combo này

Từ Kubernetes 1.24 trở đi, Docker không còn được hỗ trợ trực tiếp làm container runtime. Nhiều người vẫn nhầm điểm này. Containerd là runtime tách ra từ Docker — nhẹ hơn, không kéo theo Docker daemon, và là runtime mặc định của EKS, GKE, AKS.

Calico mình chọn vì network policy. Flannel đơn giản hơn khi setup, nhưng không có policy — mặc định mọi pod đều nói chuyện được với nhau. Calico cho phép viết rule kiểu “service A chỉ được gọi service B, cấm hẳn database trực tiếp” — cần thiết khi nhiều team deploy trên cùng cluster. Hiệu năng trên bare-metal cũng tốt hơn overlay VXLAN của Flannel.

Chuẩn bị môi trường

Yêu cầu tối thiểu

  • Control plane: 2 CPU, 2GB RAM (4GB khuyến nghị)
  • Worker node: 1 CPU, 1GB RAM (2GB khuyến nghị)
  • CentOS Stream 9, mỗi node có IP tĩnh
  • Kết nối internet để pull image

Bài này dùng 3 node: 1 control plane + 2 worker. Các bước dưới đây chạy trên tất cả các node trừ khi có ghi chú riêng.

Tắt swap và cấu hình kernel

Kubernetes yêu cầu swap tắt hoàn toàn. Bước này hay bị quên nhất — và khi bỏ qua thì kubelet restart liên tục với lỗi mơ hồ, rất khó debug:

# Tắt swap ngay lập tức
swapoff -a

# Tắt vĩnh viễn — comment dòng swap trong /etc/fstab
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

# Bật module kernel cần thiết
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

# Cấu hình sysctl
cat <<EOF | tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sysctl --system

Cấu hình firewall đúng cách

Nhiều hướng dẫn bảo tắt firewalld cho nhanh — đừng làm vậy trên production. Mở đúng port cần thiết vừa an toàn hơn vừa tránh rắc rối về sau:

# Trên control plane
firewall-cmd --permanent --add-port=6443/tcp       # Kubernetes API
firewall-cmd --permanent --add-port=2379-2380/tcp  # etcd
firewall-cmd --permanent --add-port=10250/tcp      # Kubelet API
firewall-cmd --permanent --add-port=10259/tcp      # kube-scheduler
firewall-cmd --permanent --add-port=10257/tcp      # kube-controller-manager

# Trên worker nodes
firewall-cmd --permanent --add-port=10250/tcp
firewall-cmd --permanent --add-port=30000-32767/tcp  # NodePort Services

# Calico cần BGP và IP-in-IP — áp dụng tất cả node
firewall-cmd --permanent --add-port=179/tcp
firewall-cmd --permanent --add-protocol=ipip

firewall-cmd --reload

Cài đặt Containerd

# Thêm Docker repo (Containerd nằm trong repo này)
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# Cài Containerd
dnf install -y containerd.io

# Tạo config mặc định
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml

# Bật SystemdCgroup — bắt buộc với Kubernetes
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# Khởi động
systemctl enable --now containerd

Bước SystemdCgroup = true này hay bị bỏ qua. Nếu để false, kubelet và containerd dùng 2 cgroup driver khác nhau. Cluster vẫn start, pod vẫn chạy — nhưng resource limits không hoạt động đúng. Lỗi chỉ lộ ra khi load cao, OOM killer nổ theo kiểu khó đoán.

Cài đặt kubeadm, kubelet, kubectl

# Thêm Kubernetes repo
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF

# Set SELinux permissive
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

# Cài đặt
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

# Bật kubelet
systemctl enable --now kubelet

Về SELinux: permissive thay vì disabled để vẫn log được violation — biết sau này cần viết policy gì. Tắt hẳn thì nhanh hơn, nhưng trên production dài hạn thì đầu tư viết policy đúng vẫn hơn.

Khởi tạo Control Plane

Chỉ chạy bước này trên node control plane:

kubeadm init \
  --pod-network-cidr=192.168.0.0/16 \
  --control-plane-endpoint="10.0.0.10:6443" \
  --cri-socket=unix:///run/containerd/containerd.sock

Giải thích các flag:

  • --pod-network-cidr=192.168.0.0/16: Calico mặc định dùng range này, phải khớp chính xác
  • --control-plane-endpoint: IP của control plane. Nếu sau này muốn HA với nhiều control plane, đặt là IP load balancer
  • --cri-socket: Chỉ định rõ Containerd socket, tránh nhầm lẫn nếu có nhiều runtime trên máy

Khi chạy xong, cuối output sẽ có lệnh kubeadm join — copy lại ngay, cần để join worker nodes. Sau đó cấu hình kubectl:

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

Cài đặt Calico CNI

# Cài Calico operator
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml

# Cài Calico custom resources
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml

# Theo dõi đến khi tất cả pod Calico Running
watch kubectl get pods -n calico-system

Chờ khoảng 2-3 phút. Khi tất cả pod Running, control plane node sẽ chuyển từ NotReady sang Ready:

kubectl get nodes
# NAME      STATUS   ROLES           AGE   VERSION
# cp-node   Ready    control-plane   5m    v1.30.x

Join Worker Nodes vào Cluster

Chạy lệnh join trên từng worker node — lệnh này lấy từ output của kubeadm init:

kubeadm join 10.0.0.10:6443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

Token mặc định hết hạn sau 24 giờ. Nếu join muộn, tạo mới trên control plane:

kubeadm token create --print-join-command

Tips thực chiến sau khi cluster chạy

Kiểm tra cluster cơ bản

# Kiểm tra tất cả node
kubectl get nodes -o wide

# Kiểm tra tất cả pod hệ thống
kubectl get pods -A

# Deploy test nhanh
kubectl create deployment nginx-test --image=nginx --replicas=3
kubectl get pods -o wide  # Xác nhận pod phân bổ đều trên các node

Backup etcd — đừng bỏ qua

etcd lưu toàn bộ state của cluster: deployments, secrets, configmaps, certificates. Mất snapshot này là không phục hồi — không phải “mất một phần”, mà là mất hết. Mình setup cronjob backup hàng ngày:

ETCDCTL_API=3 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 /backup/etcd-$(date +%Y%m%d).db

Drain node trước khi bảo trì

Bao giờ cũng drain node trước khi reboot hay update OS. Kubernetes sẽ reschedule pod sang node khác trước khi tắt — pod không bị drop, user không thấy gì:

# Trước khi bảo trì
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data

# Sau khi bảo trì xong
kubectl uncordon worker-1

Luôn set resource limits

Không set requests/limits thì scheduler không biết đặt pod vào node nào cho hợp lý. Hay gặp nhất: một pod ngốn hết memory của node, OOM killer nổ, kéo theo pod khác chết theo. Template tối thiểu cho mọi Deployment:

resources:
  requests:
    memory: "128Mi"
    cpu: "100m"
  limits:
    memory: "256Mi"
    cpu: "500m"

Kết luận

Setup xong cluster lần đầu tốn khoảng 1-2 tiếng — phần lớn là đợi image pull và Calico khởi động. Lần sau nhanh hơn nhiều, nhất là khi đã có script hóa các bước lặp lại.

Hiểu từng bước làm gì quan trọng hơn tốc độ setup. Swap tắt vì sao, SystemdCgroup ảnh hưởng gì, Calico cần pod CIDR riêng để làm gì — những thứ này không rõ thì khi cluster có vấn đề (và sẽ có), chỉ còn nước destroy rồi làm lại thay vì debug được.

Ba thứ nên cài thêm ngay để cluster dùng được thực sự: metrics-server (để có kubectl top), Nginx Ingress Controller (expose service qua domain), và StorageClass nếu workload cần persistent storage.

Share: