🔧 kubeadm init cluster và join node

Mục tiêu bài học

Sau bài này, bạn phải làm được 4 việc sau, theo đúng thứ tự mình vẫn làm trong phòng thi:

  1. Chuẩn bị node đủ điều kiện để kubeadm init chạy qua preflight check mà không phải vá víu.
  2. Khởi tạo control plane bằng kubeadm init, cài CNI và xác nhận node ở trạng thái Ready.
  3. Join worker node bằng join command, biết cách tạo lại join command khi token đã hết hạn.
  4. Đọc đúng thông báo lỗi khi node không join được và xử lý trong vòng 2 phút.

[!NOTE] Ôn nhanh bài trước: control plane gồm kube-apiserver, etcd, kube-scheduler, kube-controller-manager, tất cả chạy dưới dạng static pod trong /etc/kubernetes/manifests. Kubelet trên mỗi node là thứ duy nhất không phải pod, và nó là "tay sai" trực tiếp của control plane. Hiểu điều này thì bạn sẽ hiểu vì sao lỗi join gần như luôn nằm ở kubelet hoặc ở kết nối tới cổng 6443.

Ví dụ mẫu từng bước

Mình dựng lab 3 máy: cp1 (192.168.10.10), worker1 (192.168.10.11), worker2 (192.168.10.12). Toàn bộ lệnh dưới đây chạy bằng sudo.

Bước 1: Chuẩn bị trên cả 3 node

# Tắt swap ngay và tắt vĩnh viễn
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab

# Kernel module cho CNI
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay && sudo modprobe br_netfilter

# Sysctl bắt buộc
cat <<EOF | sudo 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
sudo sysctl --system

Sau đó cài containerd và đặt cgroup driver là systemd (mặc định của kubelet từ 1.22), rồi cài kubeadm, kubelet, kubectl cùng version và apt-mark hold chúng lại để tránh upgrade ngoài ý muốn.

[!IMPORTANT] Ba thứ hay bị bỏ sót nhất: swap chưa tắt, net.ipv4.ip_forward chưa bật, và SystemdCgroup = true chưa set trong /etc/containerd/config.toml. Thiếu một trong ba, kubeadm init hoặc kubelet sẽ lỗi ngay.

Bước 2: Khởi tạo control plane

Cách nhanh nhất, dùng cho lab và cho đề thi nếu đề không bắt dùng file config:

sudo kubeadm init \
  --apiserver-advertise-address=192.168.10.10 \
  --pod-network-cidr=10.244.0.0/16

Output cuối cùng sẽ có đúng 2 thứ bạn cần giữ lại:

Your Kubernetes control-plane has initialized successfully!

kubeadm join 192.168.10.10:6443 --token abcdef.0123456789abcdef \
        --discovery-token-ca-cert-hash sha256:3f8c...e21

Bước 3: Cấu hình kubectl cho user hiện tại

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

Lúc này cp1 sẽ ở trạng thái NotReady. Đó là bình thường, vì chưa có CNI.

Bước 4: Cài CNI

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
kubectl get pods -n kube-system -w

Đợi kube-flannel-*, coredns-*, kube-proxy-* đều Running, rồi kiểm tra kubectl get nodes. Giờ cp1 mới là Ready.

Bước 5: Join worker node

Chạy đúng lệnh join ở trên, trên worker1:

sudo kubeadm join 192.168.10.10:6443 --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:3f8c...e21

Rồi quay lại cp1 kiểm tra:

kubectl get nodes -o wide

Sơ đồ luồng đầy đủ:

flowchart TD
    Prep["🧹<br/>Chuẩn bị node<br/>swap off, sysctl, containerd"] --> Init["🚀<br/>kubeadm init<br/>control plane"]
    Init --> Kubeconfig["🔑<br/>Copy admin.conf<br/>về ~/.kube/config"]
    Init --> Token["🎫<br/>Join command<br/>token + CA hash"]
    Kubeconfig --> CNI["🌐<br/>Cài CNI plugin"]
    CNI --> Ready1["✅<br/>cp1 Ready"]
    Token --> Join["📥<br/>kubeadm join<br/>trên worker"]
    Join --> Ready2["✅<br/>worker Ready"]

Luyện tập có hướng dẫn

Phần này là xương sống của bài. Bạn làm lần lượt, mỗi bài đều có lệnh kiểm tra để biết mình đã đúng.

Bài 1: Chuẩn bị node và tự kiểm tra

Chạy phần chuẩn bị ở Bước 1 trên cả 3 node, sau đó xác nhận:

swapon --show                 # phải không in ra gì
sysctl net.ipv4.ip_forward    # phải là = 1
systemctl is-active containerd  # phải là active
kubeadm version -o short

[!TIP] Trong đề thi CKA, các node thường đã được chuẩn bị sẵn. Bạn chỉ cần chạy kubeadm init hoặc kubeadm join. Nhưng khi luyện ở nhà, hãy tự tay làm phần chuẩn bị ít nhất một lần, vì đề mock capstone ở Module 8 sẽ bắt bạn làm lại từ đầu.

Bài 2: Init bằng file config

Đề thi đôi khi đưa sẵn file config và yêu cầu dùng nó. Sinh file mẫu trước, đừng viết tay từ đầu:

kubeadm config print init-defaults > kubeadm-config.yaml

Sửa lại các trường quan trọng:

apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 192.168.10.10
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///run/containerd/containerd.sock
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.31.0
controlPlaneEndpoint: "192.168.10.10:6443"
networking:
  podSubnet: 10.244.0.0/16

Rồi chạy:

sudo kubeadm init --config kubeadm-config.yaml

[!WARNING] apiVersion phụ thuộc version kubeadm. Bản 1.31 trở lên dùng kubeadm.k8s.io/v1beta4, các bản cũ hơn dùng v1beta3. Đừng học thuộc, hãy luôn chạy kubeadm config print init-defaults để lấy đúng apiVersion của máy đang dùng.

Bài 3: Cài CNI và xác nhận Ready

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
kubectl get nodes
kubectl get pods -n kube-system

Điều kiện đạt: cp1 là Ready, và các pod coredns, kube-proxy, kube-flannel đều Running.

Bài 4: Join worker1 bằng join command

Copy nguyên lệnh kubeadm join ... từ output của Bước 2, chạy trên worker1. Nếu bạn đã mất output đó, tạo lại bất cứ lúc nào:

kubeadm token create --print-join-command

Lệnh này in ra join command đầy đủ, đúng token và đúng CA hash, dùng được ngay. Đây là lệnh mình dùng nhiều nhất trong phòng thi, hãy nhớ nó.

Kiểm tra:

kubectl get nodes -o wide
kubectl describe node worker1 | grep -A5 Conditions

Bài 5: Mô phỏng token hết hạn

Đây là tình huống gần như chắc chắn xuất hiện trong mock capstone. Giả lập:

kubeadm token list          # xem TTL, mặc định 24h
kubeadm token delete <token-id>

Giờ thử join worker2 bằng token cũ, bạn sẽ nhận lỗi:

error execution phase preflight: couldn't validate the identity of the API Server:
could not find a JWS signature in the cluster-info ConfigMap for token ID "..."

Cách xử lý: tạo join command mới rồi join lại, không cần đụng gì tới control plane.

Bài 6 (tùy chọn, nâng cao): Join control plane node thứ hai

Nếu lab cho phép, thêm một control plane node thứ hai. Yêu cầu bắt buộc là phải có --control-plane và một certificate key còn hạn:

sudo kubeadm init phase upload-certs --upload-certs
# in ra certificate key, mặc định có hiệu lực 2 giờ

sudo kubeadm join 192.168.10.10:6443 --control-plane \
  --certificate-key <key> \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

Sau khi join, chạy kubectl get nodes và thấy node mới với role control-plane,master.

Tự kiểm tra nhanh

Flag dùng để khai báo dải IP cho Pod khi chạy kubeadm init là {{c1::--pod-network-cidr}}.
Bootstrap token mặc định hết hạn sau {{c2::24 giờ}}.
Join một node với vai trò control plane cần thêm flag {{c3::--control-plane}} và {{c4::--certificate-key}}.
Trước khi join lại một node đã từng nằm trong cluster, cần chạy {{c5::kubeadm reset}}.
Cổng mặc định của kube-apiserver là {{c6::6443}}.

Lỗi thường gặp và cách xử lý

Bảng dưới là những lỗi mình gặp nhiều nhất, cả khi luyện và trong mock exam.

Triệu chứng Nguyên nhân thường gặp Cách xử lý
Join báo could not find a JWS signature hoặc token ... not found Token hết hạn hoặc gõ sai token kubeadm token create --print-join-command rồi join lại
Join báo x509: certificate signed by unknown authority CA cert hash sai hoặc thiếu Lấy hash từ join command mới, không gõ tay
Join treo rồi timeout Firewall chặn 6443 hoặc 10250 Kiểm tra curl -k https://192.168.10.10:6443 từ worker
Join thành công nhưng node NotReady Chưa có CNI plugin Cài CNI trên control plane, đợi vài phút
kubeadm init dừng ở preflight Swap Swap chưa tắt swapoff -a và sửa /etc/fstab
kubeadm init dừng ở FileContent--proc-sys-net-bridge-bridge-nf-call-iptables Thiếu br_netfilter hoặc sysctl Bật module, chạy lại sysctl --system
kubelet CrashLoopBackOff/restart liên tục Cgroup driver lệch (systemd vs cgroupfs) Set SystemdCgroup = true trong containerd config, restart containerd
Hostname trùng nhau giữa các node Image VM clone không đổi hostname hostnamectl set-hostname cho từng node
Node join được nhưng lệch giờ gây lỗi cert NTP không chạy Bật systemd-timesyncd hoặc chrony

Quy trình đọc lỗi mình luôn làm khi node không join được:

systemctl status kubelet
journalctl -u kubelet -n 50 --no-pager
sudo kubeadm join ... --v=5      # tăng verbosity để thấy bước nào fail

[!CAUTION] Đừng bao giờ dùng --ignore-preflight-errors để "cho qua" lỗi swap hay lỗi sysctl trong lúc luyện tập. Nó che mất nguyên nhân gốc, và trong đề thi bạn sẽ không có thời gian gỡ lại.

Nếu phải join lại một node từ đầu, thứ tự sạch sẽ là:

sudo kubeadm reset -f
sudo rm -rf /etc/cni/net.d $HOME/.kube
sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F
sudo systemctl restart containerd kubelet

Sau đó join lại bằng join command mới.

Góc chuyên gia

Vài quy tắc mình rút ra sau khi thi, viết ngắn để bạn nhớ nhanh:

  1. Join command luôn lấy từ kubeadm token create --print-join-command, không copy từ đề hay từ note cũ. Token 24h, certificate key 2h, đề thi có thể đưa token đã cũ để thử bạn.
  2. kubectl get nodes là câu trả lời cuối cùng, không phải output của kubeadm join. Join xong vẫn phải thấy node Ready mới tính là xong.
  3. Node NotReady sau join thì nghĩ tới CNI trước tiên, sau đó mới nghĩ tới kubelet. Thứ tự này tiết kiệm rất nhiều thời gian.
  4. Không sửa file trong /etc/kubernetes/manifests bằng cách restart lung tung. Với static pod, sửa file là kubelet tự nạp lại. Nhưng đó là bài sau, ở đây chỉ cần biết.
  5. ==Nếu đề cho sẵn join command, hãy chạy đúng nguyên văn, đừng "cải tiến".== Nhiều bạn tự thêm --cri-socket hoặc đổi --apiserver-advertise-address và làm hỏng bước join đang chạy tốt.
  6. Với lab một node, sau khi init bạn có thể bỏ taint để pod thường chạy trên control plane: kubectl taint nodes cp1 node-role.kubernetes.io/control-plane:NoSchedule-. Chỉ làm trong lab, không làm trong đề thi nếu đề không yêu cầu.

Bảng cổng cần nhớ khi debug kết nối:

Thành phần Cổng
kube-apiserver 6443/TCP
etcd 2379-2380/TCP
kubelet API 10250/TCP
kube-scheduler 10259/TCP
kube-controller-manager 10257/TCP
NodePort service 30000-32767/TCP

Tóm tắt và bước tiếp theo

Tóm gọn trong 5 dòng:

  • Chuẩn bị node gồm: tắt swap, bật overlay và br_netfilter, bật ip_forward, containerd với cgroup driver systemd.
  • kubeadm init tạo control plane, sinh ra join command chứa token và CA cert hash.
  • Copy /etc/kubernetes/admin.conf về $HOME/.kube/config để kubectl dùng được.
  • Cài CNI thì node mới Ready, thiếu CNI thì node mãi NotReady dù đã join.
  • Token sống 24h, certificate key sống 2h, hết hạn thì tạo lại bằng kubeadm token create --print-join-command.

Bộ thẻ ôn tập cho bài này, dùng để tự kiểm tra trước khi sang bài sau:

[
  { "front": "Thao tác bắt buộc với swap trước khi chạy kubeadm init?", "back": "Tắt swap bằng swapoff -a và comment dòng swap trong /etc/fstab." },
  { "front": "Flag nào của kubeadm init khai báo dải IP cho Pod?", "back": "--pod-network-cidr, ví dụ 10.244.0.0/16." },
  { "front": "File kubeconfig của admin nằm ở đâu sau khi init?", "back": "/etc/kubernetes/admin.conf, copy về $HOME/.kube/config." },
  { "front": "Vì sao node đã join nhưng vẫn NotReady?", "back": "Chưa cài CNI plugin nên network plugin chưa sẵn sàng." },
  { "front": "Bootstrap token mặc định có TTL bao lâu?", "back": "24 giờ." },
  { "front": "Lệnh tạo lại join command đầy đủ khi token hết hạn?", "back": "kubeadm token create --print-join-command." },
  { "front": "Flag bắt buộc khi join một node vai trò control plane?", "back": "--control-plane kèm --certificate-key." },
  { "front": "Certificate key dùng khi join control plane có hiệu lực bao lâu?", "back": "2 giờ." },
  { "front": "Lỗi 'could not find a JWS signature' khi join nghĩa là gì?", "back": "Token sai hoặc đã hết hạn, phải tạo token mới." },
  { "front": "Cổng mặc định của kube-apiserver?", "back": "6443/TCP." },
  { "front": "Lệnh dọn node trước khi join lại?", "back": "kubeadm reset -f, xóa /etc/cni/net.d và $HOME/.kube, flush iptables." },
  { "front": "Taint mặc định trên control plane node?", "back": "node-role.kubernetes.io/control-plane:NoSchedule." },
  { "front": "Hai lệnh xem log khi kubelet không chạy đúng?", "back": "systemctl status kubelet và journalctl -u kubelet -n 50 --no-pager." }
]

Bài tiếp theo: etcd backup và restore. Bạn vừa có một cluster chạy được, giờ là lúc học cách chụp snapshot etcd và phục hồi khi nó hỏng. Mình sẽ chỉ đúng 3 lệnh cần nhớ, vị trí file CA mà đề thi luôn bắt bạn khai báo, và mẹo đọc etcdctl mà không mất 10 phút mò biến môi trường.