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.

