kubelet: Đưa Worker Vào Cluster

K
Kai··8 min read

Ở Bài 10, hai worker đã có container runtime: containerd nghe trên /run/containerd/containerd.sock, sẵn sàng nhận lệnh CRI. Nhưng crictl chỉ là công cụ ta gõ tay; chưa có ai tự động quan sát cluster và bảo containerd chạy pod nào. Người làm việc đó là kubelet, và bài này dựng nó lên cả hai worker.

kubelet là tiến trình duy nhất trên một worker nói chuyện trực tiếp với api-server. Nó đăng ký node vào cluster, theo dõi những pod được giao cho node mình, gọi containerd qua CRI để tạo container, và liên tục báo cáo sức khỏe node lẫn pod ngược lên api-server. Cài xong kubelet, lần đầu tiên kubectl get nodes sẽ trả về một thứ gì đó.

kubelet đứng ở đâu trong bức tranh

Nhớ lại chuỗi ủy thác ở Bài 10. kubelet là mắt nối control plane với runtime:

   api-server  ◄──────── kubelet kubeconfig (cert system:node:worker-0)
       │  ▲                   │
       │  │ báo cáo trạng     │ CRI (gRPC)
       │  │ thái node/pod     ▼
       │  └───────────────  kubelet ──► containerd ──► runc ──► [container]
       │                       ▲
       └── gọi xuống ──────────┘
          (logs/exec/proxy, RBAC ở Bài 9)

Có hai chiều giao tiếp tách biệt giữa api-server và kubelet, và ta đã chuẩn bị cho cả hai từ trước:

  • kubelet → api-server (chiều lên): kubelet là client, dùng kubeconfig với cert worker-0 để xác thực. Api-server đọc danh tính system:node:worker-0 và Node authorizer (bật từ Bài 7) quyết định kubelet này được đọc/ghi đúng những gì liên quan tới node của nó.
  • api-server → kubelet (chiều xuống): khi bạn chạy kubectl logs/exec, api-server là client gọi vào kubelet. Lúc này kubelet phải xác thực và phân quyền cho api-server — đó là phần authentication/authorization trong cấu hình kubelet bên dưới, dựa trên ClusterRole ta đã tạo ở Bài 9.

Hiểu rõ hai chiều này thì các trường cấu hình tiếp theo không còn là phép thuật.

Bước 1 — Phân phối certificate và kubeconfig

Mỗi worker cần bốn file, đều đã sinh từ trước:

  • ca.pem — CA của cluster, để kubelet xác minh cert của api-server (và xác minh client gọi xuống nó).
  • worker-0.pem + worker-0-key.pem — cert của node. Cert này có CN=system:node:worker-0, O=system:nodes (Bài 4) — đúng định dạng Node authorizer chờ đợi. Nó vừa làm client cert (chiều lên), vừa làm serving cert của kubelet (chiều xuống), vì profile cấp cả clientAuth lẫn serverAuth.
  • worker-0.kubeconfig — kubeconfig trỏ tới load balancer https://10.0.1.10:6443 (Bài 5), nhúng sẵn cert node làm danh tính.

kubeconfig của worker trỏ vào IP nội bộ của lb-0 (10.0.1.10), không phải Elastic IP. Worker và load balancer ở cùng VPC; đi qua IP nội bộ thì ngắn hơn và không vòng ra Internet. Elastic IP chỉ dành cho kubectl từ laptop bạn (Bài 9).

Copy lên từng worker rồi đặt vào chỗ kubelet sẽ tìm. Đặt cert node thành tên trung lập (kubelet.pem) để unit và config dùng chung một đường dẫn trên mọi node:

# từ máy có thư mục pki (đổi worker-0 -> worker-1 cho node thứ hai)
W=worker-0
scp ca.pem $W.pem $W-key.pem $W.kubeconfig $W:/tmp/

ssh $W
sudo mkdir -p /var/lib/kubelet /var/lib/kubernetes
sudo mv /tmp/ca.pem            /var/lib/kubernetes/ca.pem
sudo mv /tmp/$W.pem            /var/lib/kubelet/kubelet.pem
sudo mv /tmp/$W-key.pem        /var/lib/kubelet/kubelet-key.pem
sudo mv /tmp/$W.kubeconfig     /var/lib/kubelet/kubeconfig
sudo chmod 600 /var/lib/kubelet/kubelet-key.pem /var/lib/kubelet/kubeconfig

Bước 2 — Cài binary kubelet

Một binary tĩnh, pin đúng v1.36.1 để khớp control plane:

# trên worker-0
cd /tmp
curl -fsSL -o kubelet https://dl.k8s.io/v1.36.1/bin/linux/amd64/kubelet
ls -la kubelet
sudo install -m 755 kubelet /usr/local/bin/kubelet
kubelet --version
-rw-rw-r-- 1 ubuntu ubuntu 60080393 kubelet
Kubernetes v1.36.1

60MB, liếc lại kích thước, vì như các bài control plane đã nhắc, binary tải cụt không báo lỗi mà chỉ vỡ lúc chạy.

Bước 3 — Viết KubeletConfiguration

kubelet hiện đại đọc phần lớn tham số từ một file KubeletConfiguration thay vì một rừng cờ dòng lệnh, dễ đọc và dễ quản lý version hơn. Tạo /var/lib/kubelet/kubelet-config.yaml:

# trên worker-0
sudo tee /var/lib/kubelet/kubelet-config.yaml >/dev/null <<'EOF'
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: "/var/lib/kubernetes/ca.pem"
authorization:
  mode: Webhook
cgroupDriver: systemd
clusterDomain: "cluster.local"
clusterDNS:
  - "10.32.0.10"
containerRuntimeEndpoint: "unix:///run/containerd/containerd.sock"
resolvConf: "/run/systemd/resolve/resolv.conf"
runtimeRequestTimeout: "15m"
tlsCertFile: "/var/lib/kubelet/kubelet.pem"
tlsPrivateKeyFile: "/var/lib/kubelet/kubelet-key.pem"
registerNode: true
EOF

Từng trường đáng giải thích:

  • authentication + authorization lo chiều api-server → kubelet. anonymous.enabled: false tắt truy cập nặc danh. x509.clientCAFile bảo kubelet tin các client trình cert do CA của ta ký (chính là api-server với cert apiserver-kubelet-client). webhook.enabled: true cùng authorization.mode: Webhook khiến kubelet không tự quyết quyền mà hỏi ngược api-server: "danh tính này được làm việc X trên node API không?", và api-server trả lời dựa trên RBAC ta dựng ở Bài 9. Nếu để authorization.mode mặc định (AlwaysAllow), bất kỳ ai chạm được cổng kubelet đều toàn quyền; đừng làm vậy.
  • cgroupDriver: systemd phải khớp SystemdCgroup = true của containerd (Bài 10). Đây là cặp đôi: lệch driver thì node mất ổn định dưới áp lực tài nguyên.
  • containerRuntimeEndpoint trỏ thẳng vào socket CRI của containerd. Đây là sợi dây nối kubelet với runtime ta dựng ở bài trước.
  • clusterDNS + clusterDomain là địa chỉ DNS nội bộ (10.32.0.10, nằm trong dải Service 10.32.0.0/24 của Bài 7) mà kubelet sẽ ghi vào /etc/resolv.conf của mỗi pod. Bản thân CoreDNS chưa chạy (đó là Bài 15), nhưng khai báo trước ở đây để khỏi sửa lại.
  • resolvConf: /run/systemd/resolve/resolv.conf: Ubuntu chạy systemd-resolved, và file /etc/resolv.conf mặc định trỏ vào 127.0.0.53. Nếu để kubelet dùng file đó làm nguồn upstream cho pod, DNS trong pod sẽ quay về một địa chỉ loopback vô nghĩa với pod. Trỏ thẳng vào file resolv của systemd để tránh.
  • tlsCertFile/tlsPrivateKeyFile là serving cert cho chiều api-server → kubelet.

Bước 4 — systemd unit và khởi động

Unit phụ thuộc containerd: nếu runtime chưa lên thì kubelet khởi động cũng vô ích, nên khai báo Requires + After.

# trên worker-0
sudo tee /etc/systemd/system/kubelet.service >/dev/null <<'EOF'
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=containerd.service
Requires=containerd.service

[Service]
ExecStart=/usr/local/bin/kubelet \
  --config=/var/lib/kubelet/kubelet-config.yaml \
  --kubeconfig=/var/lib/kubelet/kubeconfig \
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now kubelet
sleep 5
systemctl is-active kubelet
active

Chỉ còn hai cờ dòng lệnh: --config trỏ tới file vừa viết, --kubeconfig là danh tính để gọi lên api-server. Xem kubelet làm gì ngay sau khi lên:

sudo journalctl -u kubelet --no-pager -n 6
kubelet_node_status.go:75] "Attempting to register node" node="worker-0"
kubelet_node_status.go:78] "Successfully registered node" node="worker-0"
apiserver.go:51] "Watching apiserver"
reflector.go:507] "Caches populated" type="*v1.Pod"
kubelet.go:2709] "SyncLoop ADD" source="api" pods=[]
desired_state_of_world_populator.go:154] "Finished populating initial desired state of world"

kubelet tự đăng ký node worker-0 lên api-server (nhờ registerNode: true), rồi mở watch để theo dõi pod nào được giao cho mình, hiện pods=[] vì chưa có gì.

Bước 5 — Lặp lại trên worker-1

worker-1 làm y hệt, chỉ khác bộ cert (worker-1.*) đã copy ở Bước 1. Tải binary, viết đúng KubeletConfigurationkubelet.service như trên (hai file này giống hệt giữa hai node vì đường dẫn đã trung lập), rồi:

# trên worker-1
sudo systemctl daemon-reload
sudo systemctl enable --now kubelet
systemctl is-active kubelet
kubelet --version
active
Kubernetes v1.36.1

Bước 6 — Nhìn node từ cluster

Giờ về laptop, dùng kubeconfig admin (qua Elastic IP):

kubectl get nodes -o wide
NAME       STATUS     ROLES    AGE   VERSION   INTERNAL-IP   CONTAINER-RUNTIME
worker-0   NotReady   <none>   44s   v1.36.1   10.0.1.20     containerd://2.3.1
worker-1   NotReady   <none>   10s   v1.36.1   10.0.1.21     containerd://2.3.1

Cả hai worker đã vào cluster: đúng version, đúng IP nội bộ, và CONTAINER-RUNTIME ghi containerd://2.3.1, bằng chứng kubelet đã bắt tay được với containerd qua CRI. Nhưng trạng thái là NotReady. Đó không phải lỗi; đó là điều ta cố ý để lại. Hỏi node vì sao:

kubectl get node worker-0 -o jsonpath='{range .status.conditions[*]}{.type}={.status} -> {.message}{"\n"}{end}'
MemoryPressure=False -> kubelet has sufficient memory available
DiskPressure=False -> kubelet has no disk pressure
PIDPressure=False -> kubelet has sufficient PID available
Ready=False -> container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized

Mọi điều kiện về tài nguyên đều khỏe; chỉ Ready=False, và lý do nói thẳng: cni plugin not initialized. kubelet từ chối báo node sẵn sàng khi chưa có cấu hình mạng pod, vì một node Ready ngụ ý nó có thể chạy pod có mạng, mà điều đó chưa đúng. Ta đã cài binary CNI vào /opt/cni/bin ở Bài 10 nhưng cố tình chưa viết config; việc đó là của Bài 14. Đến lúc ấy, hai node này sẽ tự chuyển sang Ready mà không phải đụng lại kubelet.

Một thứ nữa kiểm chứng được ngay: đường api-server → kubelet, tức RBAC ta dựng ở Bài 9. Gọi proxy healthz của kubelet xuyên qua api-server:

kubectl get --raw "/api/v1/nodes/worker-0/proxy/healthz"; echo
ok

api-server đã trình cert apiserver-kubelet-client tới kubelet, kubelet hỏi ngược api-server qua webhook xem danh tính đó có quyền không, RBAC ở Bài 9 trả lời có, và kubelet đáp ok. Cả hai chiều giao tiếp đều thông, đúng như sơ đồ đầu bài.

🧹 Dọn dẹp

kubelet là dịch vụ thường trú, để nguyên (đã enable, tự lên lại sau reboot). Dọn binary tải về:

# trên mỗi worker
rm -f /tmp/kubelet

Lưu ý bảo mật: các file trong /var/lib/kubelet, đặc biệt kubelet-key.pemkubeconfig, là danh tính system:node của worker. Đã đặt quyền 600, đừng copy chúng ra ngoài. Nếu tạm dừng cụm EC2, kubelet tự đăng ký lại khi khởi động (private IP nội bộ không đổi).

Script đầy đủ ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 11-kubelet.

Tổng kết

Hai worker đã là thành viên thật của cluster: kubelet chạy, đăng ký node, nối được với containerd, và cả hai chiều giao tiếp với api-server đều hoạt động dưới đúng cơ chế xác thực/phân quyền ta dựng từ các bài trước. Điều đáng ghi nhớ là vì sao node NotReady lại là dấu hiệu đúng ở thời điểm này: nó phản ánh trung thực rằng mạng pod chưa có, chứ không phải kubelet hỏng.

Còn một thành phần nữa phải chạy trên worker trước khi ta bàn tới mạng: kube-proxy, thứ hiện thực Service ảo của Kubernetes thành luật chuyển tiếp trên mỗi node. Bài 12 cài kube-proxy lên hai worker, giải thích nó biến một ClusterIP thành các pod thật phía sau như thế nào, và đặt nền cho ba bài mạng tiếp theo.

Related Posts