A Kubernetes cluster on bare-metal VMs, with kubeadm
From a fresh Ubuntu VM to a working single control-plane cluster: static networking, containerd, the kernel modules nobody mentions until it fails, kubeadm init, Flannel, Helm and the dashboard.
هذه الأدلّة محفوظة بالإنجليزية. فالأوامر وملفّات الإعداد واحدة في كلّ اللغات، وتوزيعها على ثلاث نسخ لا يضيف إلّا احتمال تخلّف إحداها عن التحديث.
العناوين في هذا الدليل عناوين بديلة ضمن النطاق 10.0.0.0/24 والنطاق المستخدم هو example.dev. وما عدا ذلك منقول حرفياً عن الملاحظات الأصلية.
- 01
Pin the node's address
A control plane that changes IP is a control plane you rebuild. Give every node a static address before anything else — kubeadm bakes the advertise address into the cluster's certificates.
sudo nano /etc/netplan/50-cloud-init.yaml network: version: 2 ethernets: enp0s3: dhcp4: no addresses: [10.0.0.79/24] routes: - to: default via: 10.0.0.1 nameservers: addresses: [1.1.1.1, 8.8.8.8]sudo netplan apply - 02
Disable swap, permanently
The kubelet refuses to start with swap enabled.
swapoff -ahandles the running system; thefstabedit is what stops it coming back on the next reboot.sudo swapoff -a sudo sed -i '/ swap / s/^/#/' /etc/fstab sudo apt update && sudo apt upgrade - 03
Install containerd via the Docker repository
Remove anything the distro shipped first — mixed container runtimes are a long afternoon.
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg doneAdd Docker's official GPG key and repository sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - 04
Set SystemdCgroup — the one that bites
containerd's default config uses the cgroupfs driver while the kubelet uses systemd. They disagree silently: the cluster comes up, then pods restart under memory pressure for reasons that make no sense. Generate the default config and flip this one value.
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null sudo nano /etc/containerd/config.tomlFind SystemdCgroup and set it to true SystemdCgroup = truesudo systemctl restart containerd sudo systemctl enable containerd sudo systemctl status containerd - 05
Load the kernel modules and sysctls the CNI needs
Without
br_netfilterand IP forwarding, pod-to-pod traffic across nodes simply does not route — and nothing tells you why. Both are declared as files so they survive a reboot.sudo tee /etc/modules-load.d/containerd.conf <<EOF overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter sudo tee /etc/sysctl.d/kubernetes.conf <<EOF net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system - 06
Install kubelet, kubeadm and kubectl
sudo apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/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.33/deb/ /' \ | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update sudo apt-get install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl sudo systemctl enable --now kubelet - 07
Initialise the control plane
The pod CIDR has to match what the CNI expects —
10.244.0.0/16is Flannel's default, and changing one without the other produces a cluster where nothing ever becomes Ready.sudo kubeadm init \ --apiserver-advertise-address=10.0.0.55 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.33.3Take ownership of the kubeconfig mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config - 08
Install the CNI, then Helm
Nodes stay
NotReadyuntil a network plugin is installed. That is expected, not a fault.kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh - 09
Reach the cluster from your workstation
Run kubectl from the desktop, not the server. Copy
admin.confacross and install the client binary.Windows curl.exe -LO "https://dl.k8s.io/release/v1.33.0/bin/windows/amd64/kubectl.exe" curl.exe -LO "https://dl.k8s.io/v1.33.0/bin/windows/amd64/kubectl.exe.sha256" kubectl version --client - 10
Expose the dashboard and mint a token
The dashboard ships as a ClusterIP service, which is unreachable from outside the cluster. Switching it to NodePort is the quickest way in on a lab cluster.
kubectl -n kubernetes-dashboard edit svc kubernetes-dashboard-kong-proxy # change type: ClusterIP -> type: NodePort kubectl -n kubernetes-dashboard get svc kubernetes-dashboard-kong-proxy # then browse to https://<node-ip>:<nodeport>admin-user.yaml apiVersion: v1 kind: ServiceAccount metadata: name: admin-user namespace: kubernetes-dashboard --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: admin-user roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: admin-user namespace: kubernetes-dashboardkubectl apply -f admin-user.yaml kubectl -n kubernetes-dashboard create token admin-user