This guide builds a kubeadm cluster by hand on Alma Linux (the same steps apply to RHEL and Rocky). Run the steps under all nodes on every node, then the control-plane and worker steps where indicated.
I have Ansible roles for this. These steps are the manual version, as a refresher. For a scripted version, see the Ubuntu + Ansible guide.
All nodes¶
Select a release¶
- https://kubernetes.io/releases
Choose a release that's still supported and use it everywhere below. Each minor version has its own package repo:
KUBERNETES_VERSION=v1.34
Install the Kubernetes tools¶
- https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/${KUBERNETES_VERSION}/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/${KUBERNETES_VERSION}/rpm/repodata/repomd.xml.key
EOF
sudo dnf -y install kubeadm kubectl kubelet
sudo systemctl enable kubelet
The kubelet restarts in a loop until kubeadm init/join runs. That's expected.
Lock the versions so a routine dnf update can't upgrade the cluster out from under you:
sudo dnf -y install 'dnf-command(versionlock)'
sudo dnf versionlock add kubelet kubeadm kubectl
Disable swap¶
sudo swapoff -a # now
sudo sed -i '/\sswap\s/ s/^\([^#]\)/#\1/' /etc/fstab # on boot
SELinux¶
kubeadm's documented setup assumes SELinux is permissive:
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
Kernel modules¶
cat <<EOF | sudo tee /etc/modules-load.d/kubernetes.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
The file loads the modules on boot, and modprobe loads them now.
sysctl¶
cat <<EOF | sudo tee /etc/sysctl.d/kubernetes.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
Firewall ports¶
- https://kubernetes.io/docs/reference/networking/ports-and-protocols/
kubeadm warns if firewalld is active (please ensure ports [6443 10250] are open). Those two are only the minimum. The full set:
Control plane:
sudo firewall-cmd --permanent --add-port={6443,2379-2380,10250,10257,10259}/tcp
sudo firewall-cmd --reload
Workers:
sudo firewall-cmd --permanent --add-port={10250,10256,30000-32767}/tcp
sudo firewall-cmd --reload
Also open the ports your CNI plugin needs. For Calico, see its firewall table.
Install a container runtime¶
Install and configure containerd as described in containerd: install containerd.io, write the default config, set SystemdCgroup = true, and enable the service. That page also covers crictl and nerdctl for debugging.
No kubelet unit edits are needed. kubeadm autodetects the containerd socket. The --container-runtime=remote flag in older guides was removed in Kubernetes 1.27.
Control plane¶
Initialize the cluster¶
Use a pod CIDR that doesn't overlap the LAN or the service CIDR (see CNI plugins):
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
On a multi-NIC host, add --apiserver-advertise-address=<node-lan-ip>.
Set up kubectl for your user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Install a CNI plugin¶
Nodes stay NotReady until a CNI plugin is installed:
Workers¶
Join the cluster¶
Print a join command on the control plane:
sudo kubeadm token create --print-join-command
Run its output with sudo on each worker. Then, from the control plane:
kubectl get nodes -o wide
Join tokens expire after 24 hours. Generate a new one whenever you add a node later.
Troubleshooting¶
If the API server is down and kubectl can't connect, inspect the control-plane containers directly on the node with crictl:
sudo crictl ps -a
sudo crictl logs <container-id>
sudo journalctl -u kubelet -f
Other tools are covered in containerd debugging tools.
Next steps¶
- MetalLB for
LoadBalancerservices - Reset a node if something needs redoing