A few months ago I got the urge to refresh and expand my Kubernetes skills. I know the basics, but have never managed a cluster. Several companies I worked at used Kubernetes in some capacity, but sadly I was never on those teams. But that’s ok! I’m no stranger to learning on my own, and it’s a great excuse to expand my homelab.
I thought about using minikube or kind to create a cluster, but wanted a “k8s on bare metal” experience because I like tinkering with real hardware. So I searched ebay and purchased three Lenovo 8th-generation Intel i3 machines (model m720s), for $60 each. They came with 8 GB of RAM and 128GB NVMEs, which should be plenty for a learning cluster.
Prepping for installation
While the machines were in transit, I read through the kubeadm cluster creation docs, and put together an installation script for Debian. I chose to install Kubernetes version 1.36 since it’s older and will let me practice cluster upgrades without having to wait for new versions to come out.
Here’s the script that prepares a Debian machine for becoming either a k8s control plane node or worker node.
#!/bin/bash
# K8S SETUP FOR DEBIAN# https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/# This installs the foundation for control plane nodes AND worker nodesCONTAINERD_VERSION="2.3.1"RUNC_VERSION="1.4.2"CNI_VERSION="1.9.1"K8S_VERSION="1.36"KUBECTL_VERSION="1.36.0"set -e
# TODO: root needs this in .bashrcexport PATH="/usr/sbin:/sbin:$PATH"apt install neovim tmux sudo curl
# ensure swap is offsystemctl mask swap.target
# CONTAINER RUNTIME SETUP# https://kubernetes.io/docs/setup/production-environment/container-runtimes/# sysctl params required by setup, params persist across rebootscat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF# As of 1.22, kubeadm defaults to systemd as a cgroup driver, so nothing for us to do regarding cgroups# CONTAINERD SETUP# https://github.com/containerd/containerd/releasescurl -LO https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz
curl -LO https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz.sha256sum
curl -LO https://raw.githubusercontent.com/containerd/containerd/main/containerd.service
cat containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz.sha256sum | sha256sum --check
tar Cxzvf /usr/local containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz
# Debian doesn't seem to have this folder by defaultmkdir -p /usr/local/lib/systemd/system
cp containerd.service /usr/local/lib/systemd/system/containerd.service
systemctl daemon-reload
systemctl enable --now containerd
# RUNC INSTALLATION# https://github.com/opencontainers/runc/releasescurl -LO https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.amd64
curl -LO https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.sha256sum
# runc.sha256sum has lots of entries that cause problemsgrep runc.amd64 runc.sha256sum | sha256sum --check
install -m 755 runc.amd64 /usr/local/sbin/runc
# CNI PLUGINS# https://github.com/containernetworking/plugins/releasescurl -LO https://github.com/containernetworking/plugins/releases/download/v${CNI_VERSION}/cni-plugins-linux-amd64-v${CNI_VERSION}.tgz
curl -LO https://github.com/containernetworking/plugins/releases/download/v${CNI_VERSION}/cni-plugins-linux-amd64-v${CNI_VERSION}.tgz.sha256
cat cni-plugins-linux-amd64-v${CNI_VERSION}.tgz.sha256 | sha256sum --check
mkdir -p /opt/cni/bin
tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v${CNI_VERSION}.tgz
# KUBECTL INSTALLATIONapt-get install -y apt-transport-https ca-certificates curl gpg
curl -fsSL https://pkgs.k8s.io/core:/stable:/v${K8S_VERSION}/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:/v${K8S_VERSION}/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list
apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
systemctl enable --now kubelet
After the machines arrived, I used the above script to configure the first one. Then, I created a local DNS record for my control plane endpoint in dnsmasq (not shown), and created the cluster with the following:
1
2
3
4
5
6
7
8
9
10
# kube-router requires setting a --pod-network-cidrkubeadm init --control-plane-endpoint k8s.home.arpa --pod-network-cidr 10.0.0.0/16
export KUBECONFIG=/etc/kubernetes/admin.conf
# kube-router networking addonkubectl apply -f https://raw.githubusercontent.com/cloudnativelabs/kube-router/master/daemonset/kubeadm-kuberouter.yaml
# permit this control plane node to be my first worker nodekubectl taint nodes --all node-role.kubernetes.io/control-plane-
Note that I’m specifying a control plane endpoint. The cluster creation docs mention why this is recomended:
(Recommended) If you have plans to upgrade this single control-plane kubeadm cluster to high availability you should specify the –control-plane-endpoint to set the shared endpoint for all control-plane nodes. Such an endpoint can be either a DNS name or an IP address of a load-balancer.
Moving to HA would require at least 3 control plane nodes, and may or may not be something I dive into later.
Don’t change IPs willy-nilly
At this point, I realized I was so excited to get k8s running that I forgot to fully plan how this cluster would fit into my home network. So I took a pause, and decided to reserve 3 addresses outside of my normal DHCP range. I updated dnsmasq and got back to the control plane node.
Given that I’d just configured dnsmasq to give this machine a different IP address, I rebooted to pick up the new address. When the machine came back up, Kubernetes was not happy! From what I read, it certainly seems possible to change the IP address of an existing control plane node, but it’s quite involved. So instead of spelunking through several lengthy configuration files, I decided to simply wipe the system and reinstall.
Adding a worker
With that completed, the next task was to add a worker node. I used the same Debian shell script as above to get kubelet running, and then ran the additional command to join the node to the cluster as a worker.
alan@thinkpad:~/Projects/server-setup/20260521.k8s$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
kube1 Ready control-plane 24d v1.36.1
kube2 Ready <none> 24d v1.36.1
I mentioned at the start of this post that I purchased three machines for this adventure, and as you can see above my cluster only has two. I’m currently using the third to experiment with hardening Linux servers, so it may join the cluster at a later date.
Now that I’ve got a cluster, I need some containers to run. But I’ll save that journey for another post. Thanks for reading.