kubelet: Đưa Worker Vào Cluster
Ở 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ínhsystem:node:worker-0và 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ầnauthentication/authorizationtrong 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ảclientAuthlẫnserverAuth.worker-0.kubeconfig— kubeconfig trỏ tới load balancerhttps://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 chokubectltừ 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+authorizationlo chiều api-server → kubelet.anonymous.enabled: falsetắt truy cập nặc danh.x509.clientCAFilebảo kubelet tin các client trình cert do CA của ta ký (chính là api-server với certapiserver-kubelet-client).webhook.enabled: truecùngauthorization.mode: Webhookkhiế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.modemặc định (AlwaysAllow), bất kỳ ai chạm được cổng kubelet đều toàn quyền; đừng làm vậy.cgroupDriver: systemdphải khớpSystemdCgroup = truecủ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.containerRuntimeEndpointtrỏ 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+clusterDomainlà địa chỉ DNS nội bộ (10.32.0.10, nằm trong dải Service10.32.0.0/24của Bài 7) mà kubelet sẽ ghi vào/etc/resolv.confcủ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ạysystemd-resolved, và file/etc/resolv.confmặc định trỏ vào127.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/tlsPrivateKeyFilelà 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 KubeletConfiguration và kubelet.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.pem và kubeconfig, 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.