Skip to main content

11. Install - Kubernetes the kubeadm way

Mục lục


1. Kubeadm là gì và tại sao cần nó?

Tình huống: Sếp giao nhiệm vụ setup một Kubernetes cluster 3 node cho team development. Không phải single-node Minikube hay kind để thử nghiệm — cả hai đều là công cụ dựng cluster "một máy" chỉ để học và test, Minikube chạy cluster trong một VM, còn kind (Kubernetes IN Docker) chạy mỗi node như một Docker container — mà là production-ready cluster thật sự: một master node, hai worker node. Ngồi xuống, mở documentation, bắt đầu liệt kê những thứ cần làm...

Kubernetes cluster không phải một phần mềm đơn lẻ. Nó là một hệ thống gồm hàng chục thành phần phân tán: kube-apiserver đón nhận mọi request, etcd lưu trữ trạng thái cluster, kube-controller-manager điều phối các controller, kube-scheduler phân bổ Pod, kubeletkube-proxy chạy trên từng node, chưa kể container runtime như containerd hay CRI-O. Mỗi thành phần lại cần certificate TLS, cần file cấu hình riêng, cần kết nối đúng với các thành phần khác.

Cài từng cái bằng tay? Được, nhưng mất cả ngày, và sai một chỗ là cluster không hoạt động.

Đó là lý do kubeadm ra đời. kubeadm là công cụ chính thức từ Kubernetes SIG (Special Interest Group), giúp bootstrap cluster theo đúng best practices chỉ trong vài lệnh.

💡 Hình dung: kubeadm giống bộ nấu ăn thông minh. Thay vì phải mua từng nguyên liệu, sơ chế, nấu từng món riêng lẻ và hy vọng chúng hợp nhau, bạn cho vào nguyên liệu, bấm nút — bếp tự động setup bếp ga đúng áp suất, nấu đúng nhiệt, và cho ra món ăn hoàn chỉnh.

Quy trình kubeadm tạo cluster gồm các bước chính:

  1. Chuẩn bị hệ thống — cấp phát VMs hoặc bare-metal servers.
  2. Phân biệt vai trò — một node làm control plane (master), các node còn lại là worker.
  3. Chuẩn bị node — tắt swap, nạp kernel module, bật sysctl, mở port trên mọi node.
  4. Cài container runtime — containerd trên mọi node.
  5. Cài kubeadm, kubelet, kubectl — các công cụ quản lý cluster.
  6. Khởi tạo master bằng kubeadm init.
  7. Deploy Pod Network addon — để các Pod giao tiếp được với nhau.
  8. Join worker nodes vào cluster bằng kubeadm join.
kubeadm: tạo Kubernetes cluster từ đầukubemaster(Control Plane)kube-node-1(Worker)kube-node-2(Worker)Các bước thực hiện trên TẤT CẢ nodes1. Chuẩn bị node: swap, module, sysctl2. Cài containerd3. Cài kubeadm, kubelet, kubectl4. kubeadm init (CHỈ master)5. Deploy Flannel (CHỈ master)6. kubeadm join(CHỈ worker nodes)Dùng token từ outputkubeadm init (TTL 24h)

2. Chuẩn bị môi trường với Vagrant và VirtualBox

Để thực hành, cần môi trường với nhiều máy ảo. Hai công cụ phổ biến cho việc này:

VirtualBox là hypervisor chạy các máy ảo trên máy thật. Nó miễn phí và hỗ trợ nhiều hệ điều hành.

Vagrant là công cụ automation giúp tạo và quản lý VMs bằng code. Thay vì click từng bước trong VirtualBox GUI, chỉ cần viết cấu hình vào một file — Vagrantfile — rồi chạy vài lệnh.

💡 Hình dung: Vagrant giống file docker-compose.yml cho VMs. Thay vì tạo container bằng docker run với hàng chục flags, viết docker-compose.yml liệt kê services, networks, volumes một lần, rồi docker compose up — Vagrant hoạt động y hệt với VMs, chỉ thay Docker bằng VirtualBox.

2.1. Cấu hình Vagrantfile

Vagrantfile định nghĩa cluster gồm 3 node:

git clone <repository-url>
cd <repository-folder>

Cấu hình cluster trong Vagrantfile:

NodeRoleIP Address
kubemasterControl Plane (Master)192.168.56.11
kube-node-1Worker192.168.56.21
kube-node-2Worker192.168.56.22

Mạng 192.168.56.0/24 dùng interface enp0s8 để các node giao tiếp với nhau.

2.2. Quản lý VMs

Kiểm tra trạng thái:

vagrant status

# Output (minh họa):
# Current machine states:
#
# kubemaster not created (virtualbox)
# kube-node-1 not created (virtualbox)
# kube-node-2 not created (virtualbox)

Khởi tạo tất cả VMs:

vagrant up

# Output (minh họa):
# ==> kubemaster: Importing base box 'ubuntu/jammy64'...
# ==> kubemaster: Running provisioner...
# ==> kube-node-1: Importing base box 'ubuntu/jammy64'...
# ==> kube-node-1: Running provisioner...
# ==> kube-node-2: Importing base box 'ubuntu/jammy64'...
# ==> kube-node-2: Running provisioner...

Lệnh này tải Ubuntu base image và provision mỗi VM theo Vagrantfile. Base box hiện hành trong Vagrantfile của khoá là ubuntu/jammy64 (Ubuntu 22.04 LTS) — các box Ubuntu cũ hơn như ubuntu/bionic64 (18.04) đã hết vòng đời hỗ trợ, không nên dùng nữa.

Kết nối SSH:

vagrant ssh kubemaster # Kết nối tới master
vagrant ssh kube-node-1 # Kết nối tới worker 1
vagrant ssh kube-node-2 # Kết nối tới worker 2

Thoát session:

logout

Kiểm tra uptime:

uptime

# Output (minh họa):
# 15:32:01 up 5 min, 1 user, load average: 0.15, 0.10, 0.05

3. Chuẩn bị node trước khi cài đặt

Ba VM vừa dựng ở phần trước là Ubuntu nguyên bản — chưa có gì trong đó được chuẩn bị cho Kubernetes. Cài thẳng containerd rồi chạy kubeadm init ngay lúc này sẽ vấp phải hai kiểu hỏng, và cả hai đều rất khó đoán nếu chưa từng gặp:

  • Control plane không lên được. kubeadm init dừng ngay ở bước preflight với [ERROR Swap], vì node còn bật swap. Đây không phải cảnh báo có thể bỏ qua: kubelet mặc định tự từ chối khởi động khi phát hiện swap trên node, nên dù có ép qua preflight thì control plane vẫn không bao giờ chạy.
  • Pod network không thông. Cluster init xong, node báo Ready, nhưng Pod không gọi được ClusterIP của Service khi endpoint nằm ngay trên chính node đó, và DNS lookup fail mỗi khi Pod CoreDNS tình cờ ở cùng node với Pod đang truy vấn. Nguyên nhân là hai thứ vô hình: kernel module chưa nạp và tham số sysctl chưa bật, khiến gói tin đi qua bridge nội bộ của node không bao giờ chạm tới các rule iptables mà kube-proxy dựng lên.

Toàn bộ các bước trong phần này chạy trên TẤT CẢ nodes — kubemaster, kube-node-1 và kube-node-2 — và phải hoàn tất trước khi đụng tới containerd ở phần sau.

💡 Hình dung: Phần này giống việc san nền và chôn ống ngầm trước khi đổ móng. Không ai nhìn thấy nó sau khi ngôi nhà đã xây xong, nên rất dễ sốt ruột bỏ qua để nhảy thẳng vào phần "xây". Nhưng nền lún hay ống đặt thiếu thì lỗi chỉ lộ ra khi tường đã lên tới tầng hai — và lúc đó chi phí đập ra làm lại lớn gấp nhiều lần so với làm tử tế ngay từ đầu.

3.1. Kiểm tra yêu cầu phần cứng và định danh node

Yêu cầu tối thiểu chính thức để cài kubeadm: 2 GB RAM trở lên trên mỗi máy, 2 CPU trở lên cho control plane node, và các node phải thông mạng hoàn toàn với nhau. Worker node không bị ràng buộc số CPU tối thiểu, nhưng ngưỡng RAM thì vẫn áp dụng.

Kiểm tra RAM:

free -h

# Output minh họa
# total used free shared buff/cache available
# Mem: 3.8Gi 415Mi 2.9Gi 1.0Mi 498Mi 3.2Gi
# Swap: 2.0Gi 0B 2.0Gi

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Cột total ở dòng Mem là con số cần đối chiếu với ngưỡng 2 GB — ở đây 3.8Gi là dư. Nhưng dòng Swap mới đáng chú ý hơn: total đang là 2.0Gi, nghĩa là swap còn bật và node này chưa dùng được cho kubelet — đó là việc của mục 3.2.

Đếm số CPU:

nproc

# Output minh họa
# 2

nproc in ra số logical CPU mà hệ điều hành nhìn thấy. Trên kubemaster giá trị này phải từ 2 trở lên: kubeadm có preflight check NumCPU cho đúng ngưỡng đó, và thiếu thì nó báo lỗi the number of available CPUs 1 is less than the required 2 rồi dừng. Tương tự, check Mem đọc tổng RAM của máy và đòi tối thiểu 1700 MB — thấp hơn ngưỡng 2 GB trong tài liệu một chút, vì đó là mức sàn tuyệt đối mà kubeadm từ chối vượt qua, không phải mức khuyến nghị để chạy cho tốt.

Ngoài phần cứng, mỗi node còn phải có hostname, MAC address và product_uuid khác nhau — Kubernetes dùng ba giá trị này để phân biệt các node, trùng nhau thì quá trình cài đặt có thể fail. Hostname đã được Vagrantfile đặt sẵn (kubemaster, kube-node-1, kube-node-2), còn hai giá trị kia cần kiểm tra bằng tay.

Kiểm tra MAC address:

ip link

# Output minh họa
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
# link/ether 08:00:27:1f:8a:4c brd ff:ff:ff:ff:ff:ff
# 3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
# link/ether 08:00:27:d4:9b:22 brd ff:ff:ff:ff:ff:ff

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, địa chỉ cụ thể chỉ mang tính ví dụ.)

Giá trị đứng sau link/ether chính là MAC address. Interface cần đối chiếu là enp0s8 — card mạng mà các node dùng để nói chuyện với nhau trong dải 192.168.56.0/24 — và MAC của nó phải khác nhau trên cả ba node.

Kiểm tra product_uuid:

sudo cat /sys/class/dmi/id/product_uuid

# Output minh họa
# 2a7f4c18-3b6e-4d51-9a2c-7e8f01b3d5a9

(Output minh họa — cấu trúc và định dạng UUID đúng theo lệnh thật, giá trị cụ thể chỉ mang tính ví dụ.)

product_uuid là định danh phần cứng do hypervisor sinh ra cho từng VM, đọc ra từ bảng DMI của máy. Chạy lệnh này trên cả ba node và so sánh — ba chuỗi phải khác nhau hoàn toàn. Nếu hai VM được clone từ cùng một ảnh đĩa mà không sinh lại UUID, chúng sẽ trùng định danh và node thứ hai join vào cluster sẽ gặp lỗi khó hiểu.

3.2. Tắt swap

Swap là vùng trên đĩa mà kernel dùng làm bộ nhớ dự phòng: khi RAM cạn, các trang nhớ ít dùng bị đẩy xuống đĩa để nhường chỗ. Với một máy chủ thông thường, đây là cơ chế cứu hộ hữu ích. Với Kubernetes thì nó phá vỡ giả định nền tảng của scheduler.

Lý do: kube-scheduler quyết định đặt Pod lên node nào dựa trên lượng RAM node báo là còn trống, và kubelet thực thi memory limit của container dựa trên lượng RAM thật sự được cấp. Khi swap bật, ranh giới "hết RAM" trở nên mờ — một container vượt quá limit có thể tiếp tục chạy nhờ swap thay vì bị kill, chạy chậm đi hàng chục lần mà vẫn được báo cáo là khoẻ mạnh, và node có thể nhận thêm Pod trong khi RAM vật lý đã cạn từ lâu. Vì vậy mặc định kubelet từ chối khởi động nếu phát hiện swap trên node.

Hỗ trợ swap trên node (feature gate NodeSwap) đã lên Stable từ Kubernetes 1.34, nhưng "Stable" ở đây chỉ nghĩa là cơ chế đã ổn định — hành vi mặc định vẫn không đổi. Muốn giữ swap thì phải khai báo failSwapOn: false trong cấu hình kubelet, và kể cả khi đó workload vẫn không được dùng swap trừ khi đổi memorySwap.swapBehavior sang một giá trị khác mặc định NoSwap (ví dụ LimitedSwap). Với cluster dựng để học, cách đơn giản và đúng nhất vẫn là tắt hẳn swap.

Tắt swap ngay lập tức:

sudo swapoff -a

swapoff -a vô hiệu hoá mọi swap area đang active và không in ra gì khi thành công — im lặng ở đây là dấu hiệu tốt. Xác nhận lại bằng free -h:

free -h

# Output minh họa
# total used free shared buff/cache available
# Mem: 3.8Gi 418Mi 2.9Gi 1.0Mi 498Mi 3.2Gi
# Swap: 0B 0B 0B

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Dòng Swap giờ toàn 0B ở cả ba cột — đó là trạng thái kubelet cần. Nhưng swapoff chỉ có tác dụng tới lần reboot kế tiếp, nên còn một bước nữa.

Tắt swap vĩnh viễn qua /etc/fstab:

sudo sed -i '/[[:space:]]swap[[:space:]]/s/^/#/' /etc/fstab
grep swap /etc/fstab

# Output minh họa
# #/swap.img none swap sw 0 0

(Output minh họa — cấu trúc dòng fstab đúng theo file thật, đường dẫn cụ thể chỉ mang tính ví dụ.)

Lệnh sed tìm dòng nào có từ swap đứng riêng giữa hai khoảng trắng (tức cột filesystem type trong /etc/fstab) rồi chèn dấu # vào đầu dòng để comment nó lại. Dấu # ở đầu output là toàn bộ bằng chứng cần thiết: dòng mount swap đã bị vô hiệu hoá, nên sau reboot kernel sẽ không bật lại nó.

Lưu ý: /etc/fstab không phải chỗ duy nhất swap có thể được khai báo. Một số bản phân phối bật swap qua systemd unit (.swap) hoặc qua zram. Nếu sau khi reboot mà free -h vẫn thấy swap, kiểm tra thêm bằng systemctl --type swap --all và disable unit tương ứng.

3.3. Nạp kernel module overlay và br_netfilter

Hai kernel module cần có mặt trước khi container runtime và CNI plugin chạy:

  • overlay — module của OverlayFS, một filesystem xếp lớp cho phép chồng nhiều thư mục read-only lên nhau rồi phủ một lớp ghi đè lên trên. Đây chính là cách containerd ghép các layer của một container image thành rootfs cho container mà không phải copy dữ liệu.
  • br_netfilter — module cho phép gói tin đi qua một Linux bridge được đưa vào netfilter, tức là bị iptables nhìn thấy và xử lý. Điều này quan trọng vì Flannel tạo trên mỗi node một bridge tên cni0, và mọi Pod trên node đó đều cắm vào bridge này. Không có br_netfilter, traffic giữa hai Pod cùng node đi thẳng trong bridge ở tầng 2 và không bao giờ chạm tới các rule DNAT mà kube-proxy dựng lên — hệ quả là ClusterIP của Service không được dịch về Pod IP, và mọi request tới Service có endpoint nằm trên chính node đó rơi vào hư không.

Khai báo module để tự nạp mỗi lần boot:

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# Output
# overlay
# br_netfilter

Thư mục /etc/modules-load.d/ được systemd-modules-load đọc ở mỗi lần khởi động và nạp từng module liệt kê trong đó, nên hai module này sẽ sống sót qua reboot. Output ở đây là do tee vừa ghi file vừa in lại nội dung — nó chính là nội dung file vừa tạo.

Nạp module ngay cho phiên hiện tại:

sudo modprobe overlay
sudo modprobe br_netfilter

systemd-modules-load chỉ chạy lúc boot, nên hai lệnh modprobe này là thứ nạp module vào kernel ngay bây giờ mà không cần reboot. Cả hai không in ra gì khi thành công.

Xác nhận module đã nạp:

lsmod | grep br_netfilter

# Output minh họa
# br_netfilter 32768 0
# bridge 303104 1 br_netfilter

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, kích thước cụ thể chỉ mang tính ví dụ.)

Ba cột của lsmod lần lượt là tên module, kích thước tính bằng byte, và số lượng module khác đang phụ thuộc vào nó kèm tên chúng. Dòng thứ hai là dòng đáng chú ý: module bridge liệt kê br_netfilter ở cột cuối, chứng tỏ br_netfilter đã gắn đúng vào bridge subsystem của kernel. Nếu lệnh không in ra dòng nào, module chưa được nạp.

lsmod | grep overlay

# Output minh họa
# overlay 196608 0

(Output minh họa — cấu trúc và tên cột đúng theo lệnh thật, kích thước cụ thể chỉ mang tính ví dụ.)

Cột use count của overlay0 ở thời điểm này vì chưa có container nào chạy — hoàn toàn bình thường. Con số đó sẽ tăng lên sau khi containerd bắt đầu tạo container.

3.4. Bật các tham số sysctl

Nạp module mới chỉ là lắp đường ống; còn phải mở van bằng ba tham số kernel:

  • net.ipv4.ip_forward = 1 — mặc định kernel Linux không route gói IPv4 giữa các interface, nó chỉ nhận gói gửi cho chính mình. Nhưng node Kubernetes bắt buộc phải chuyển tiếp gói giữa interface ảo của Pod và card mạng thật, nên thiếu tham số này thì không có traffic nào ra vào Pod được. Đây cũng là tham số duy nhất trong nhóm mà kubeadm thực sự kiểm tra ở bước preflight: nó đọc /proc/sys/net/ipv4/ip_forward, đòi đúng giá trị 1, và nếu sai thì báo lỗi FileContent--proc-sys-net-ipv4-ip_forward rồi dừng.
  • net.bridge.bridge-nf-call-iptables = 1net.bridge.bridge-nf-call-ip6tables = 1 — hai công tắc bật đúng cái hành vi mà module br_netfilter cung cấp ở mục trên, cho IPv4 và IPv6. Chúng chỉ tồn tại sau khi br_netfilter đã được nạp, nên thứ tự 3.3 trước 3.4 là bắt buộc chứ không phải tuỳ ý.

Lưu ý: Tài liệu kubernetes.io hiện hành chỉ còn yêu cầu net.ipv4.ip_forward — hai tham số bridge từng nằm trong danh sách prerequisite chính thức cho tới Kubernetes v1.28 rồi được gỡ khỏi trang đó, vì phần lớn CNI plugin ngày nay tự đặt lấy. Với Flannel và bridge cni0, đặt sẵn vẫn là lựa chọn an toàn: nếu CNI plugin có đặt lại thì cũng vô hại, còn nếu nó không đặt thì đây là thứ cứu cả cluster.

Ghi file cấu hình sysctl:

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

# Output
# net.bridge.bridge-nf-call-iptables = 1
# net.bridge.bridge-nf-call-ip6tables = 1
# net.ipv4.ip_forward = 1

Đặt vào /etc/sysctl.d/ thay vì chỉnh trực tiếp /etc/sysctl.conf là cách làm được khuyến nghị: file riêng dễ xoá đi khi gỡ cluster, và không đụng vào cấu hình mặc định của bản phân phối. Giá trị ở đây bền qua reboot.

Áp dụng ngay mà không cần reboot:

sudo sysctl --system

# Output minh họa (rút gọn)
# * Applying /etc/sysctl.d/10-console-messages.conf ...
# * Applying /etc/sysctl.d/k8s.conf ...
# net.bridge.bridge-nf-call-iptables = 1
# net.bridge.bridge-nf-call-ip6tables = 1
# net.ipv4.ip_forward = 1
# * Applying /etc/sysctl.conf ...

(Output minh họa — cấu trúc và định dạng đúng theo lệnh thật, danh sách file cụ thể tuỳ hệ thống.)

sysctl --system đọc lại toàn bộ file cấu hình sysctl trên máy theo đúng thứ tự ưu tiên thư mục và áp dụng từng giá trị. Dòng cần tìm trong output là * Applying /etc/sysctl.d/k8s.conf ... cùng ba dòng giá trị ngay dưới nó — nếu không thấy, file vừa ghi đã sai tên hoặc sai vị trí.

Xác nhận cả ba tham số:

sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward

# Output
# net.bridge.bridge-nf-call-iptables = 1
# net.bridge.bridge-nf-call-ip6tables = 1
# net.ipv4.ip_forward = 1

Cả ba đều phải là 1. Nếu thay vào đó nhận được cannot stat /proc/sys/net/bridge/bridge-nf-call-iptables: No such file or directory, nghĩa là module br_netfilter chưa được nạp — quay lại mục 3.3 chạy modprobe rồi thử lại.

3.5. Mở các port bắt buộc

Các thành phần Kubernetes nói chuyện với nhau qua một tập port cố định. kubeadm không tự mở firewall giúp — nếu node đang bật ufw hoặc firewalld mà chưa mở các port này, worker sẽ không join được và control plane sẽ không điều khiển được kubelet.

Control plane node:

ProtocolHướngPortDùng choBên gọi tới
TCPInbound6443Kubernetes API serverTất cả
TCPInbound2379-2380etcd server client APIkube-apiserver, etcd
TCPInbound10250Kubelet APIChính node đó, control plane
TCPInbound10259kube-schedulerChính node đó
TCPInbound10257kube-controller-managerChính node đó

Worker node:

ProtocolHướngPortDùng choBên gọi tới
TCPInbound10250Kubelet APIChính node đó, control plane
TCPInbound10256kube-proxyChính node đó, load balancer
TCPInbound30000-32767NodePort ServicesTất cả
UDPInbound30000-32767NodePort ServicesTất cả

Vài điểm đáng nhớ trong hai bảng trên. 6443 là port duy nhất worker node thực sự cần gọi tới trên master — nó xuất hiện lại trong lệnh kubeadm join ở phần sau. 10250 có mặt ở cả hai bảng vì mọi node đều chạy kubelet, và chính control plane là bên gọi vào port này để lấy log hay exec vào container. Dải 30000-32767 là khoảng port mặc định mà Kubernetes cấp cho Service kiểu NodePort, cần mở cả TCP lẫn UDP. Mọi giá trị ở đây đều là mặc định và đổi được — đổi rồi thì phải mở port mới thay cho port cũ.

Kiểm tra firewall có đang bật không:

sudo ufw status

# Output minh họa
# Status: inactive

Với Status: inactive thì không cần làm gì thêm — đây là trạng thái thường gặp trên VM Vagrant dựng từ box Ubuntu chuẩn. Nếu output là Status: active, mở từng port trong hai bảng trên bằng sudo ufw allow.

Kiểm tra một port cụ thể có thông không:

nc 127.0.0.1 6443 -zv -w 2

# Output minh họa
# nc: connect to 127.0.0.1 port 6443 (tcp) failed: Connection refused

Lệnh nc với -z chỉ dò cổng mà không gửi dữ liệu, -v in kết quả, -w 2 bỏ cuộc sau 2 giây. Ở thời điểm này, Connection refused là kết quả đúng — nó có nghĩa gói tin tới được đích nhưng chưa có process nào lắng nghe, vì API server còn chưa được tạo. Phép thử có ý nghĩa thật sự là chạy lệnh này từ worker node trỏ tới IP của master (nc 192.168.56.11 6443 -zv -w 2) sau khi đã kubeadm init: succeeded! nghĩa là đường thông, Connection timed out nghĩa là gói tin bị chặn giữa đường — gần như luôn do firewall.

Lưu ý: kubeadm cũng tự kiểm tra các port này ở bước preflight, nhưng theo chiều ngược lại — check PortOpenCheck xác nhận 6443, 1025910257 đang còn trống, chưa bị process nào khác chiếm. Ngoài ra, nếu firewalld đang chạy, kubeadm in cảnh báo nhắc mở 644310250.

Chuẩn bị tới đây là xong phần nền. Từ mục tiếp theo trở đi mới thực sự bắt đầu cài phần mềm.


4. Cài đặt Container Runtime (containerd)

Giải thích ngắn gọn: Mỗi Pod chạy bên trong container. Node cần một Container Runtime — phần mềm responsible cho việc pull image, tạo container, start/stop container. Kubernetes không giao tiếp trực tiếp với container runtime mà thông qua giao diện chuẩn gọi là CRI (Container Runtime Interface).

Từ Kubernetes 1.24 trở đi, thành phần dockershim — lớp tích hợp Docker Engine gắn sẵn trong kubelet — đã bị gỡ khỏi Kubernetes, nên Docker Engine không còn được hỗ trợ trực tiếp. Docker Engine tự nó không implement CRI; muốn tiếp tục dùng nó phải cài thêm adapter cri-dockerd. Runtime được khuyến nghị là containerd hoặc CRI-O. Phần này dùng containerd. Lưu ý thêm: từ Kubernetes 1.26, kubelet chỉ còn làm việc với CRI API v1 — runtime nào không hỗ trợ v1 thì node không đăng ký được vào cluster.

💡 Hình dung: containerd giống đầu bếp trong bếp ăn công nghiệp. Kubernetes là quản lý sản xuất, ra lệnh "nấu 50 phần". Đầu bếp nhận lệnh, lấy nguyên liệu (images), chế biến (containers), và báo lại khi xong. Kubernetes không cần biết đầu bếp nấu bằng gas hay điện — miễn họ nói cùng ngôn ngữ (CRI).

4.1. Cài đặt containerd trên mọi node

Lưu ý quan trọng: Thực hiện trên TẤT CẢ nodes — kubemaster, kube-node-1, và kube-node-2.

sudo apt update
sudo apt install -y containerd

# Output (minh họa):
# Reading package lists... Done
# Building dependency tree
# Reading state information... Done
# The following NEW packages will be installed:
# containerd
# 0 upgraded, 1 newly installed, 0 to remove.

4.2. Cgroup Drivers

Mỗi container cần giới hạn tài nguyên (CPU, memory) để không một container nào chiếm hết máy. Linux dùng cgroups (control groups) để làm việc này. Có hai driver:

  • cgroupfs — driver mặc định của kubelet, phù hợp khi init system không phải systemd.
  • systemd — được khuyến nghị khi init system là systemd (hầu hết Linux hiện đại), và bắt buộc trên thực tế nếu máy dùng cgroup v2.

💡 Quy tắc vàng: kubelet và container runtime phải dùng cùng một cgroup driver. Không khớp nhau → Pod không schedule được, hoặc resource limits không hoạt động đúng.

Từ Kubernetes 1.34, feature gate KubeletCgroupDriverFromCRI đã lên Stable và bật mặc định: kubelet tự hỏi container runtime xem nó đang dùng driver nào rồi đi theo, bỏ qua giá trị cgroupDriver trong cấu hình kubelet. Nói cách khác, container runtime trở thành nguồn sự thật duy nhất — việc đặt SystemdCgroup = true cho containerd ở mục tiếp theo vì thế càng quan trọng, còn nguy cơ hai bên lệch nhau thì gần như biến mất.

Kiểm tra init system:

ps -p 1

# Output (minh họa):
# PID TTY TIME CMD
# 1 ? 00:00:02 systemd

Nếu thấy systemd, cần cấu hình containerd dùng systemd driver.

4.3. Cấu hình containerd dùng systemd driver

Từ Kubernetes 1.22+, kubeadm mặc định dùng systemd driver. Nhưng containerd mặc định dùng cgroupfs. Cần sync hai bên.

Tạo thư mục cấu hình:

sudo mkdir -p /etc/containerd

Generate config với systemd driver:

containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' | sudo tee /etc/containerd/config.toml

# Output (minh họa):
# version = 2
# root = "/var/lib/containerd"
# state = "/run/containerd"
# ...
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# SystemdCgroup = true

containerd config default in ra file cấu hình mặc định đầy đủ; sed lật đúng một dòng SystemdCgroup từ false sang true, còn tee vừa ghi kết quả xuống /etc/containerd/config.toml vừa in lại ra màn hình — nên output ở đây chính là nội dung file vừa ghi.

Kiểm tra cấu hình đã đúng:

sudo grep -B 1 SystemdCgroup /etc/containerd/config.toml

# Output:
# [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# SystemdCgroup = true

Dòng SystemdCgroup = true là thứ duy nhất cần xác nhận; dòng [plugins...] phía trên chỉ cho biết nó nằm đúng trong block runtime runc. Tên block này phụ thuộc thế hệ containerd — containerd 1.x dùng [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] như trên, còn containerd 2.x đổi thành [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]. Khoá SystemdCgroup thì giữ nguyên ở cả hai, nên lệnh sed bên trên chạy đúng cho cả hai thế hệ.

Restart containerd để áp dụng:

sudo systemctl restart containerd

Xác nhận containerd đang chạy:

sudo systemctl status containerd

# Output (minh họa):
# ● containerd.service - containerd container runtime
# Loaded: loaded (/lib/systemd/system/containerd.service; enabled)
# Active: active (running) since Mon 2026-09-09 15:45:00 UTC; 2s ago

Dòng đáng chú ý là Active: active (running) — nếu thấy failed hoặc activating (auto-restart), gần như chắc chắn file config.toml vừa ghi có cú pháp sai. Đường dẫn CRI socket mà containerd lắng nghe là /run/containerd/containerd.sock; kubeadm nhận diện nó qua endpoint unix:///var/run/containerd/containerd.sock (/var/run là symlink tới /run), và đó cũng là giá trị truyền cho flag --cri-socket khi trên máy có nhiều runtime cùng cài.


5. Cài đặt kubeadm, kubelet và kubectl

Trên mỗi node, cần ba công cụ:

ComponentVai trò
kubeletAgent chạy trên mỗi node, nhận lệnh từ API server, tạo/dừng Pod
kubeadmCông cụ bootstrap cluster, join nodes
kubectlCLI để giao tiếp với cluster (chỉ cần trên máy quản lý, thường là master)

💡 Hình dung: kubelet là nhân viên tại mỗi chi nhánh — nhận chỉ thị từ headquarters (API server), thực hiện công việc tại chỗ. kubeadm là bộ công cụ dùng một lần để mở chi nhánh mới (bootstrap). kubectl là điện thoại để headquarters gọi cho nhân viên — người quản lý dùng, không cần cài ở mỗi chi nhánh.

5.1. Thêm Kubernetes APT repository

Thực hiện trên TẤT CẢ nodes. Tài liệu này dùng v1.37 làm ví dụ — thay bằng minor version mong muốn.

Hai lưu ý trước khi gõ lệnh. Thứ nhất, các repository cũ apt.kubernetes.ioyum.kubernetes.io đã bị đóng băng từ 13/09/2023 và gỡ hẳn từ 04/03/2024 — mọi hướng dẫn còn trỏ tới chúng đều không cài được nữa. Repository chính thức hiện nay là pkgs.k8s.io. Thứ hai, pkgs.k8s.iomột repository riêng cho từng minor version — repo v1.37 chỉ chứa package của 1.37, nên muốn đổi sang minor version khác phải sửa cả URL của signing key lẫn URL trong file sources.list.d/kubernetes.list.

Bước 1 — Thêm signing key:

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.37/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

# Output (minh họa):
# gpg: keyring '/etc/apt/keyrings/kubernetes-apt-keyring.gpg' created

Cùng một signing key dùng chung cho mọi repository, nên phần version trong URL của Release.key không quan trọng — nhưng giữ cho khớp với repo vẫn đỡ nhầm lẫn về sau.

Lưu ý: Trên các bản phát hành cũ hơn Debian 12 và Ubuntu 22.04, thư mục /etc/apt/keyrings chưa tồn tại sẵn. Tạo thủ công trước khi chạy lệnh curl ở trên:

sudo mkdir -p -m 755 /etc/apt/keyrings

Bước 2 — Thêm repository:

echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

# Output:
# deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ /

Lệnh này ghi đè toàn bộ nội dung cũ của /etc/apt/sources.list.d/kubernetes.list — đúng ý đồ khi muốn chuyển hẳn sang một minor version khác.

Bước 3 — Cài đặt packages:

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

# Output (minh họa):
# The following NEW packages will be installed:
# kubeadm kubectl kubelet
# kubelet kubeadm kubectl set on hold.

apt-mark hold giữ packages ở version hiện tại, ngăn auto-upgrade có thể gây phiền phức. Kubernetes có quy trình nâng cấp riêng (xem phần Cluster Maintenance) — để apt upgrade tự kéo kubelet lên version mới là cách nhanh nhất làm hỏng cluster.

5.2. Kiểm tra phiên bản

kubeadm version

# Output (minh họa):
# kubeadm version: &version.Info{Major:"1", Minor:"37", GitVersion:"v1.37.0"}

GitVersion là con số cần đối chiếu: nó phải nằm trong cùng minor version với repository vừa thêm, và khớp với version dự định cài cho control plane.


6. Khởi tạo Control Plane Node

Giải thích: Control Plane (master) là "bộ não" của cluster. Nó chứa API server — điểm vào duy nhất để giao tiếp với cluster. Nó cũng chứa etcd (database lưu trạng thái), scheduler (phân bổ Pod), và các controller quản lý desired state.

kubeadm init là lệnh biến một node thường thành control plane.

6.1. Chạy kubeadm init

Chỉ chạy trên kubemaster (không phải worker nodes).

Trước khi chạy, kubeadm init thực hiện một loạt preflight check — kiểm tra trước xem máy có đủ điều kiện không. Nếu đã làm đầy đủ phần 3. Chuẩn bị node trước khi cài đặt, toàn bộ các check này sẽ qua sạch. Còn nếu bỏ qua, đây chính là chỗ chúng lộ ra: SwapCheck đọc /proc/swaps và dừng ngay nếu node còn swap, FileContent--proc-sys-net-ipv4-ip_forward đòi /proc/sys/net/ipv4/ip_forward phải bằng 1, còn NumCPUMem chặn những máy quá yếu. Preflight fail thì kubeadm dừng trước khi tạo bất cứ thứ gì, nên cách xử lý luôn là quay lại mục 3 làm cho đủ rồi chạy lại lệnh — không phải bỏ qua bằng --ignore-preflight-errors.

sudo kubeadm init \
--apiserver-advertise-address=192.168.56.11 \
--pod-network-cidr=10.244.0.0/16 \
--upload-certs

Giải thích các flags:

FlagÝ nghĩa
--apiserver-advertise-addressIP mà API server quảng bá là địa chỉ nó đang lắng nghe. Worker nodes phải kết nối được tới IP này. Không đặt thì kubeadm lấy IP của default network interface — trên VM nhiều card mạng, đây thường là lựa chọn sai
--pod-network-cidrPod network CIDR — dải IP dành riêng cho Pods, tách biệt hẳn với dải IP của node. Khi flag này được đặt, control plane tự cắt dải đó thành từng khối con và cấp cho mỗi node một khối. Flannel mặc định dùng 10.244.0.0/16 — nếu đổi, phải sửa cả Flannel manifest cho khớp
--upload-certsUpload certificates của control plane lên Secret kubeadm-certs trong namespace kube-system, để các control plane node khác join HA cluster mà không phải copy certificate thủ công. Worker node không dùng tới Secret này

Lưu ý: Cụm 3 node trong tài liệu này chỉ có một control plane node, nên --upload-certs không thực sự cần thiết — chỉ giữ lại để sẵn sàng mở rộng thành HA. Nếu dùng thật, nhớ rằng certificate key mà kubeadm in ra chỉ có hiệu lực 2 giờ; quá hạn thì tạo lại bằng kubeadm init phase upload-certs --upload-certs.

Lưu ý: Nếu có kế hoạch nâng cấp lên HA cluster về sau, thêm --control-plane-endpoint flag ngay từ đầu — đổi sau khó hơn.

Output thành công sẽ hiển thị:

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

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

You should now deploy a Pod network to the cluster.

6.2. Thiết lập kubeconfig

kubeconfig là file chứa credentials để kubectl xác thực với API server. Sau kubeadm init, file nằm ở /etc/kubernetes/admin.conf. Cần copy về home directory.

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

Kiểm tra cluster đã nhận:

kubectl get nodes

# Output (minh họa):
# NAME STATUS ROLES AGE VERSION
# kubemaster NotReady control-plane 2m v1.37.0

Hai cột cần nhìn ở đây: ROLES đã là control-plane — đúng như kỳ vọng sau kubeadm init; còn STATUSNotReady. Master node NotReady ở bước này là bình thường, vì chưa có Pod network addon — kubelet báo lại rằng CNI plugin chưa sẵn sàng. Cột VERSION hiển thị version của kubelet đang chạy trên node, không phải version của API server.

6.3. Các thành phần được tạo tự động

kubeadm init không chỉ "cài Kubernetes" — nó tạo ra hạ tầng hoàn chỉnh:

kubeadm init tạo gì trên Control Plane?kubemaster (Control Plane Node)Certificates & Configs/etc/kubernetes/• admin.conf• kubelet.conf• etcd/• pki/Control Plane Podskube-apiserverkube-controller-managerkube-scheduleretcdcoredns (chưa Ready)Token & SecretBootstrap token choworker join (TTL 24h)Secret kubeadm-certscho control plane join(nếu --upload-certs)Static Pod manifests tại /etc/kubernetes/manifests/Kubelet theo dõi thư mục này → tự động tạo PodChưa có Pod Network → CoreDNS Pods NotReady

Hai khái niệm trong sơ đồ đáng được nói rõ. Static Pod là Pod do chính kubelet tạo và quản lý trực tiếp từ file YAML trong /etc/kubernetes/manifests/, không thông qua API server — nhờ vậy kube-apiserver mới có thể tự khởi động dù chưa có API server nào đang chạy để nhận lệnh. Bootstrap token là chuỗi ngắn dạng abcdef.0123456789abcdefkubeadm init sinh ra, đóng vai trò mật khẩu dùng một lần để worker node chứng minh danh tính trong lần đầu liên hệ API server, trước khi nó nhận được certificate riêng qua quá trình TLS bootstrap.


7. Cài đặt Pod Network (Flannel)

Giải thích: Pods trên các node khác nhau cần giao tiếp với nhau. Nhưng container networking trong Linux phức tạp — mỗi Pod cần IP riêng, cần route đúng, cần overlay network. Kubernetes không làm việc này mà ủy quyền cho CNI (Container Network Interface) plugins.

Flannel là một CNI plugin phổ biến, dùng overlay network — một mạng ảo dựng chồng lên mạng vật lý sẵn có, đóng gói gói tin của Pod vào trong gói tin của node rồi bóc ra ở đầu bên kia — để Pods giao tiếp xuyên node.

Flannel không phải lựa chọn duy nhất. Các CNI plugin đang được duy trì tích cực gồm Flannel (đơn giản nhất, thuần kết nối), CalicoCilium (thêm network policy, observability, eBPF). Một cái tên xuất hiện dày đặc trong các tài liệu CKA cũ là Weave Net — repository weaveworks/weave đã chuyển sang trạng thái archived (chỉ đọc) và không còn được phát hành bản mới, nên không nên chọn cho cluster dựng mới.

💡 Hình dung: Trong một tòa nhà văn phòng, mỗi phòng (Pod) có số điện thoại nội bộ riêng. Flannel giống hệ thống tổng đài — biết phòng nào ở tầng nào, chuyển cuộc gọi đúng người, dù họ ở tầng 5 hay tầng 10. Không có tổng đài, hai phòng không gọi được cho nhau.

7.1. Cài đặt Flannel

Chỉ chạy trên kubemaster — Flannel sẽ tự deploy lên mọi node sau khi worker join.

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

# Output (minh họa):
# namespace/kube-flannel created
# serviceaccount/flannel created
# configmap/kube-flannel-cfg created
# daemonset.apps/kube-flannel-ds created

Flannel tạo namespace riêng kube-flannel và một DaemonSet — nghĩa là mỗi node trong cluster, kể cả node join sau này, đều tự động nhận một Pod Flannel.

Lưu ý: Flannel đã chuyển sang tổ chức flannel-io trên GitHub; đường dẫn cũ raw.githubusercontent.com/coreos/flannel/... chỉ còn tồn tại nhờ redirect. Ngoài ra, chính dự án khuyến nghị dùng manifest đính kèm release (releases/latest/download/kube-flannel.yml như trên) thay vì bản trong Documentation/ trên nhánh mặc định — bản trên nhánh có thể lệch pha với container image đã publish và apply vào sẽ lỗi.

Lưu ý: Nếu dùng custom pod CIDR (khác 10.244.0.0/16), tải manifest về và sửa net-conf.json trong ConfigMap trước khi apply:

curl -fsSL https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml -o kube-flannel.yml
# Chỉnh sửa net-conf.json trong file
kubectl apply -f kube-flannel.yml

7.2. Kiểm tra trạng thái

Xem Pod network pods:

kubectl get pods -n kube-flannel

# Output (minh họa):
# NAME READY STATUS RESTARTS AGE
# kube-flannel-ds-abc12 1/1 Running 0 45s
# kube-flannel-ds-def34 1/1 Running 0 45s
# kube-flannel-ds-ghi56 1/1 Running 0 45s

Kiểm tra node đã Ready:

kubectl get nodes

# Output (minh họa):
# NAME STATUS ROLES AGE VERSION
# kubemaster Ready control-plane 5m v1.37.0

Cột STATUS đã chuyển từ NotReady sang Ready — dấu hiệu duy nhất cần chờ ở bước này, cho biết kubelet đã tìm thấy CNI plugin hoạt động được. Cluster đã sẵn sàng nhận worker nodes.


8. Join Worker Nodes vào Cluster

Giải thích: Mỗi worker node chạy kubelet — agent kết nối với API server để nhận lệnh. Khi join, kubelet nhận certificates từ master và bắt đầu Pod scheduling.

8.1. Lệnh Join

Sau kubeadm init, output có chứa lệnh join cho worker nodes:

kubeadm join <control-plane-endpoint>:6443 --token <token> \
--discovery-token-ca-cert-hash sha256:<hash>

Cổng 6443 trong lệnh là cổng mặc định của kube-apiserver (đổi được bằng flag --apiserver-bind-port khi init). Phần --discovery-token-ca-cert-hash là vân tay của CA certificate cluster — nó để worker node xác minh rằng API server mình đang nói chuyện đúng là cluster cần join, chứ không phải một máy giả mạo.

Nếu quên token hoặc token đã hết hạn, tạo lại:

kubeadm token create --print-join-command

# Output (minh họa):
# kubeadm join 192.168.56.11:6443 --token abc123.def456 --discovery-token-ca-cert-hash sha256:xyz789

Bootstrap token mà kubeadm init sinh ra có thời hạn mặc định 24 giờ (--token-ttl mặc định 24h0m0s), nên với cluster dựng để học và tháo đi trong ngày thì hầu như không phải lo. Nhưng nếu quay lại cluster sau vài ngày để thêm worker node, token cũ chắc chắn đã hết hạn — lệnh trên vừa tạo token mới vừa in ra nguyên lệnh kubeadm join hoàn chỉnh, không phải ghép tay hash.

8.2. Join từng Worker Node

Trên kube-node-1:

sudo kubeadm join 192.168.56.11:6443 --token abc123.def456 --discovery-token-ca-cert-hash sha256:xyz789

# Output (minh họa):
# [preflight] Running pre-flight checks
# [preflight] Reading configuration from the cluster...
# [preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
# [kubelet-start] Starting the kubelet...
# [kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
# This node has joined the cluster.

Dòng quyết định là This node has joined the cluster. — trước đó, [kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap... là giai đoạn kubelet đổi bootstrap token lấy certificate riêng của node. Nếu quá trình dừng lại ở dòng này, nguyên nhân gần như luôn là token đã hết hạn hoặc node không mở được cổng 6443 tới control plane.

Trên kube-node-2: Chạy lệnh tương tự.

8.3. Xác nhận Cluster

Trên kubemaster:

kubectl get nodes

# Output (minh họa):
# NAME STATUS ROLES AGE VERSION
# kubemaster Ready control-plane 10m v1.37.0
# kube-node-1 Ready <none> 2m v1.37.0
# kube-node-2 Ready <none> 1m v1.37.0

Tất cả nodes Ready — cluster hoạt động. Cột ROLES của hai worker node hiển thị <none> là hoàn toàn bình thường: kubeadm chỉ gắn label role cho control plane node, worker node không được gắn label nào.

Xem Flannel pods đã chạy trên mọi node:

kubectl get pods -n kube-flannel -o wide

# Output (minh họa):
# NAME READY STATUS RESTARTS AGE IP NODE
# kube-flannel-ds-abc12 1/1 Running 0 10m 10.244.0.2 kubemaster
# kube-flannel-ds-def34 1/1 Running 0 2m 10.244.1.2 kube-node-1
# kube-flannel-ds-ghi56 1/1 Running 0 1m 10.244.2.2 kube-node-2

Cột IP là bằng chứng rõ nhất cho thấy pod network CIDR đang được chia đúng: mỗi node nhận một khối con riêng trong 10.244.0.0/1610.244.0.x cho kubemaster, 10.244.1.x cho kube-node-1, 10.244.2.x cho kube-node-2.

8.4. Test Deployment

Deploy Pod đơn giản:

kubectl run web --image=nginx

# Output (minh họa):
# pod/web created

Theo dõi Pod:

kubectl get pods -w

# Output (minh họa):
# NAME READY STATUS RESTARTS AGE
# web 0/1 ContainerCreating 0 5s
# web 1/1 Running 0 12s

Pod chuyển từ ContainerCreating sang Running — cluster đã hoạt động hoàn chỉnh.


9. Lab Thực hành

9.1. Yêu cầu Lab

Một lab tổng hợp kiểm tra toàn bộ quy trình:

  1. Chuẩn bị cả hai node theo phần 3. Chuẩn bị node trước khi cài đặt.
  2. Cài containerd trên control plane và node01.
  3. Cài kubeadm và kubelet trên cả hai node.
  4. Bootstrap cluster bằng kubeadm.
  5. Join node01 vào cluster.
  6. Deploy Flannel network plugin.
  7. Verify tất cả pods đang chạy.

9.2. Các bước thực hiện chi tiết

Trước Bước 1, chạy đủ các lệnh chuẩn bị ở phần 3. Chuẩn bị node trước khi cài đặt trên cả hai node — tắt swap, nạp overlaybr_netfilter, ghi /etc/sysctl.d/k8s.conf rồi sudo sysctl --system. Bỏ qua bước này thì Bước 6 (kubeadm init) sẽ fail ở preflight.

Bước 1 — Cài containerd và cấu hình systemd driver (trên cả control plane và node01):

sudo apt update
sudo apt install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' | sudo tee /etc/containerd/config.toml
sudo systemctl restart containerd

Bước 2 — Thêm Kubernetes APT repository (trên cả hai node, ví dụ version 1.37.0):

sudo mkdir -p -m 755 /etc/apt/keyrings
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.37/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update

Nhớ rằng URL repo phải khớp minor version muốn cài — nếu bài lab yêu cầu một version khác, đổi v1.37 ở cả hai dòng curlecho, rồi mới đổi số version ở bước 3.

Bước 3 — Cài kubeadm, kubelet, kubectl (trên cả hai node):

sudo apt-get install -y kubeadm=1.37.0-* kubelet=1.37.0-* kubectl=1.37.0-*
sudo apt-mark hold kubelet kubeadm kubectl

Bước 4 — Kiểm tra phiên bản kubelet:

kubelet --version

# Output (minh họa):
# Kubernetes v1.37.0

Bước 5 — Kiểm tra cluster nodes (sẽ lỗi vì cluster chưa init):

kubectl get nodes

# Output (minh họa):
# E0912 15:30:00.000001 1234 memcache.go:265] couldn't get current server API group list
# error: failed to retrieve kubeconfig: /home/vagrant/.kube/config does not exist

Bình thường — phải init cluster trước.

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

kubeadm init \
--apiserver-advertise-address=$(ip -f inet -o addr show eth0 | awk '{print $4}' | cut -d/ -f1) \
--pod-network-cidr=10.244.0.0/16

Đoạn $(...) chỉ đơn giản lấy IPv4 của interface eth0 và cắt bỏ phần prefix (/24) — thay eth0 bằng tên interface thật của máy lab nếu khác.

Bước 7 — Copy kubeconfig và kiểm tra:

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

# Output (minh họa):
# NAME STATUS ROLES AGE VERSION
# cpo1 NotReady control-plane 1m v1.37.0

Lại là NotReady — vẫn đúng như mong đợi, vì bước cài Flannel còn ở phía sau.

Bước 8 — Join node01:

# Trên node01, dùng token từ output kubeadm init
sudo kubeadm join <control-plane-ip>:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>

Bước 9 — Cài Flannel:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

# Output (minh họa):
# namespace/kube-flannel created
# networkpolicy.networking.k8s.io/kube-flannel created
# ...

Bước 10 — Verify hoàn tất:

kubectl get nodes
kubectl get pods -n kube-system
kubectl get pods -n kube-flannel

# Output (minh họa) - kubectl get nodes:
# NAME STATUS ROLES AGE VERSION
# cpo1 Ready control-plane 5m v1.37.0
# node01 Ready <none> 2m v1.37.0

Cả hai node đều Ready, và ba lệnh trên phải cùng xanh: kube-system có đủ apiserver, scheduler, controller-manager, etcd, CoreDNS và kube-proxy ở trạng thái Running, còn kube-flannel có đúng hai Pod — một cho mỗi node. Thiếu một Pod Flannel thường đồng nghĩa node tương ứng chưa join xong.


Nguồn tham khảo

Nguồn gốc: Khóa "Certified Kubernetes Administrator (CKA)" — phần "Install - Kubernetes the kubeadm way" (Giới thiệu triển khai với kubeadm; Provision VMs với Vagrant; Demo cài đặt cluster bằng kubeadm; Practice Test triển khai Kubernetes cluster), nền tảng KodeKloud. Giảng viên: Mumshad Mannambeth.

Repo ghi chú, link tài liệu, và đáp án các practice question của toàn bộ khóa học: kodekloudhub/certified-kubernetes-administrator-course.

Fact-check:

  • Package repository chính thức để cài kubeadm/kubelet/kubectlpkgs.k8s.io, với một repository riêng cho từng minor version (pkgs.k8s.io/core:/stable:/v1.37/deb/); repo cũ apt.kubernetes.io/yum.kubernetes.io đã freeze từ 2023-09-13 và gỡ hẳn từ 2024-03-04 — 2026-09-23, Kubernetes blog: pkgs.k8s.io introduction.
  • Cú pháp hiện hành để thêm repo Kubernetes trên Debian/Ubuntu: cài apt-transport-https ca-certificates curl gpg, tải signing key về /etc/apt/keyrings/kubernetes-apt-keyring.gpg, rồi ghi dòng deb [signed-by=...] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ / vào /etc/apt/sources.list.d/kubernetes.list; thư mục /etc/apt/keyrings chưa tồn tại sẵn trên các bản cũ hơn Debian 12 và Ubuntu 22.04, tạo bằng sudo mkdir -p -m 755 /etc/apt/keyrings — 2026-09-23, Kubernetes: Installing kubeadm.
  • Kubernetes 1.37 là bản stable mới nhất (phát hành 2026-08-26); các minor version còn được hỗ trợ patch là 1.35, 1.36 và 1.37 — 2026-09-23, Kubernetes Releases.
  • dockershim đã bị gỡ khỏi Kubernetes từ v1.24; Docker Engine không implement CRI nên phải dùng adapter cri-dockerd, còn runtime khuyến nghị là containerd hoặc CRI-O. Từ v1.26, kubelet chỉ làm việc với CRI API v1 — 2026-09-23, Kubernetes: Container Runtimes.
  • Cấu hình systemd cgroup driver cho containerd đặt SystemdCgroup = true; tên block khác nhau theo thế hệ — containerd 1.x dùng [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options], containerd 2.x dùng [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]; cách khôi phục cấu hình mặc định là containerd config default > /etc/containerd/config.toml — 2026-09-23, Kubernetes: Container Runtimes — containerd.
  • Feature gate KubeletCgroupDriverFromCRI đã lên Stable từ Kubernetes 1.34 và bật mặc định: kubelet tự phát hiện cgroup driver từ container runtime và bỏ qua giá trị cgroupDriver trong cấu hình kubelet — 2026-09-23, Kubernetes: Feature Gates.
  • Feature gate NodeSwap đã lên Stable từ Kubernetes 1.34, nhưng mặc định kubelet vẫn không khởi động trên node Linux còn bật swap — phải hoặc swapoff -a, hoặc khai báo failSwapOn: false và đổi memorySwap.swapBehavior sang LimitedSwap thì workload mới dùng được swap (mặc định là NoSwap) — 2026-09-23, Kubernetes: Swap memory management.
  • Prerequisite sysctl hiện hành của tài liệu chính thức là net.ipv4.ip_forward = 1 đặt trong /etc/sysctl.d/k8s.conf rồi sudo sysctl --system, verify bằng sysctl net.ipv4.ip_forward; trang này nêu rõ một số network implementation còn đòi thêm sysctl khác hoặc kernel module khác — 2026-09-23, Kubernetes: Container Runtimes — Network configuration.
  • Yêu cầu tối thiểu để cài kubeadm: 2 GB RAM trở lên mỗi máy, 2 CPU trở lên cho control plane node, các node thông mạng hoàn toàn với nhau, và hostname/MAC address/product_uuid duy nhất cho từng node — 2026-09-23, Kubernetes: Installing kubeadm.
  • Lệnh kiểm tra định danh node: ip link (hoặc ifconfig -a) để lấy MAC address, sudo cat /sys/class/dmi/id/product_uuid để lấy product_uuid; trùng giá trị giữa các node có thể làm quá trình cài đặt fail — 2026-09-23, Kubernetes: Installing kubeadm — Verify the MAC address and product_uuid are unique for every node.
  • Cách tắt swap chính thức: sudo swapoff -a để tắt tạm thời, và tắt trong các file cấu hình như /etc/fstab hay systemd.swap để bền qua reboot; cách thay thế là failSwapOn: false trong cấu hình kubelet, khi đó workload vẫn không dùng được swap trừ khi đặt swapBehavior khác giá trị mặc định NoSwap — 2026-09-23, Kubernetes: Installing kubeadm — Swap configuration.
  • kubeadm preflight thực thi NumCPUCheck với ControlPlaneNumCPU = 2 (lỗi: the number of available CPUs %d is less than the required %d) và MemCheck với ControlPlaneMem = 1700 MB (lỗi: the system RAM (%d MB) is less than the minimum %d MB) — 2026-09-23, kubernetes/kubernetes: cmd/kubeadm/app/constants/constants.go, cmd/kubeadm/app/preflight/checks.go.
  • kubeadm preflight kiểm tra swap qua SwapCheck (đọc /proc/swaps) và kiểm tra IPv4 forwarding qua FileContentCheck{Path: "/proc/sys/net/ipv4/ip_forward", Content: "1"} — nhãn lỗi tương ứng là SwapFileContent--proc-sys-net-ipv4-ip_forward; bridge-nf-call-iptables không còn nằm trong danh sách preflight check của kubeadm — 2026-09-23, kubernetes/kubernetes: cmd/kubeadm/app/preflight/checks.go, cmd/kubeadm/app/preflight/checks_linux.go.
  • Nạp kernel module overlaybr_netfilter qua /etc/modules-load.d/k8s.conf rồi sudo modprobe overlay / sudo modprobe br_netfilter; đặt net.bridge.bridge-nf-call-iptables = 1, net.bridge.bridge-nf-call-ip6tables = 1, net.ipv4.ip_forward = 1 trong /etc/sysctl.d/k8s.conf rồi sudo sysctl --system; verify bằng lsmod | grep br_netfilter, lsmod | grep overlaysysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward — đây là nội dung mục "Forwarding IPv4 and letting iptables see bridged traffic" của tài liệu chính thức tới bản v1.28, về sau được rút gọn còn net.ipv4.ip_forward trên trang Container Runtimes — 2026-09-23, kubernetes/website release-1.28: container-runtimes.md.
  • net.bridge.bridge-nf-call-iptables = 1 cần thiết khi container được nối vào một Linux bridge (trường hợp của bridge CNI plugin): thiếu nó thì NAT của kube-proxy không áp dụng cho traffic tới Service có endpoint nằm trên chính node đó — 2026-09-23, kubernetes/kubernetes#120849.
  • Kiểm tra port bằng nc 127.0.0.1 6443 -zv -w 2; kubeadm preflight còn có PortOpenCheck cho 6443, 10259, 10257FirewalldCheck cho 6443, 10250 — 2026-09-23, Kubernetes: Installing kubeadm — Check required ports, kubernetes/kubernetes: cmd/kubeadm/app/preflight/checks.go.
  • kubeadm.k8s.io/v1beta4 xuất hiện từ Kubernetes 1.31; v1beta3 bị deprecated từ 1.31 và đã bị gỡ hẳn trong kubeadm 1.37, migrate bằng kubeadm config migrate với binary kubeadm 1.35 — 2026-09-23, Kubernetes blog: kubeadm v1beta4, kubernetes/kubernetes#136016.
  • --apiserver-bind-port của kubeadm init mặc định 6443; --token-ttl mặc định 24h0m0s; --upload-certs upload certificate của control plane lên Secret kubeadm-certs (dành cho control plane node join, không phải worker) — 2026-09-23, Kubernetes: kubeadm init.
  • Secret kubeadm-certs và decryption key hết hạn sau 2 giờ; tạo lại bằng sudo kubeadm init phase upload-certs --upload-certs — 2026-09-23, Kubernetes: Creating Highly Available clusters with kubeadm.
  • Đường dẫn CRI socket của containerd trên Linux là /run/containerd/containerd.sock, endpoint kubeadm dùng cho --cri-socketunix:///var/run/containerd/containerd.sock — 2026-09-23, Kubernetes: Installing kubeadm — container runtimes.
  • Port cần mở: control plane 6443 (kube-apiserver), 2379-2380 (etcd), 10250 (kubelet), 10259 (kube-scheduler), 10257 (kube-controller-manager); worker node 10250 (kubelet), 10256 (kube-proxy), 30000-32767 (NodePort Services) — 2026-09-23, Kubernetes: Ports and Protocols.
  • Flannel đã chuyển sang tổ chức flannel-io/flannel; lệnh cài chính thức là kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml, và dự án khuyến nghị dùng manifest đính kèm release thay vì bản Documentation/kube-flannel.yml trên nhánh mặc định vì bản này có thể lệch pha với container image đã publish — 2026-09-23, flannel-io/flannel: README.
  • Weave Net không còn được duy trì — repository weaveworks/weave ở trạng thái archived (read-only), commit cuối 2024-08; các CNI plugin còn được duy trì tích cực gồm Flannel, Calico và Cilium — 2026-09-23, weaveworks/weave.
  • Vagrantfile hiện hành của khóa học dùng base box ubuntu/jammy64 (Ubuntu 22.04 LTS), mạng 192.168.56.0/24 với control plane ở .11 và worker từ .21 — 2026-09-23, kodekloudhub/certified-kubernetes-administrator-course: kubeadm-clusters/virtualbox/Vagrantfile.