# 🔧 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

```bash
# 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:

```bash
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:

```text
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

```bash
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

```bash
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`:

```bash
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:

```bash
kubectl get nodes -o wide
```

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

```mermaid
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:

```bash
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:

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

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

```yaml
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:

```bash
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

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```text
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:

```bash
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

```cloze
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:

```bash
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à:

```bash
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:

```flashcard
[
  { "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.
