Running a kubernetes cluster locally with kubeadm
I’m going to show you how to get a real kubernetes cluster setup locally on top of virtual machines! I’ll be using multipass but feel free to use virtualbox, proxmox, or whatever your favorite cloud provider is.
kubeadm a production ready kubernetes install tool and I prefer to use it over minikube, kind, etc. because it gives you a more real world experience for managing the kubernetes cluster. This isn’t important if you are a user of the cluster but if you have to run your own this is a great way to gain some daily experience.
The kubernetes documentation on kubeadm is great and you can find it here.
The differences between this blog and the kubernetes docs is that they leave a lot of decisions up to the reader such as:
- choosing a container runtime
- Selecting and installing a CNI (container network interface)
I’m going to be opinionated and make specific technology decisions such as using containerd and cilium so that you don't have to think about those decisions.
Getting your Virtual Machines setup!
The minimum requirements for a control plane node in kubernetes is 2gb of RAM and 2 CPUs. Since we actually want to be able to schedule workloads on the workers afterwards we are going to setup a cluster that looks like this:
- Control Plane: 2gb RAM, 2 CPU
- Worker: 4gb RAM, 2 CPU
Since we’ll be using multipass to launch the nodes, we can do that now:
❯ multipass launch -c 2 -m 4G -d 10G -n controlplane 22.04
❯ multipass launch -c 2 -m 4G -d 10G -n worker 22.04
❯ multipass list
Name State IPv4 Image
controlplane Running 192.168.64.7 Ubuntu 22.04 LTS
worker Running 192.168.64.8 Ubuntu 22.04 LTS
Now we can start working on our controlplane first, lets shell in:
❯ multipass shell controlplane
Lets first add the kubernetes repo to the system so we have access to all the kubernetes tools:
❯ echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
❯ curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg|sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/k8s.gpg
❯ sudo apt-get update && sudo apt-get upgrade -y
Now that our system is setup, we can move on to getting a container runtime.
Getting your Container Runtime!
Before we start pulling in kubernetes components we need to get a container runtime setup on the machine. We we are going to use containerd for this purpose. You can view the docs of for it here.
Which will download the latest binary and set it up. I’m going to walk you through how to do it using the version packaged with Ubuntu which could be older than the latest release.
First thing we want to do is configure the networking to allow iptables to manage:
❯ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
❯ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
We also need to disable some default systemd settings for rp_filter
because
they are not compatible with cilium. See the bug report
here
❯ sudo sed -i -e '/net.ipv4.conf.*.rp_filter/d' $(grep -ril '\.rp_filter' /etc/sysctl.d/ /usr/lib/sysctl.d/)
❯ sudo sysctl -a | grep '\.rp_filter' | awk '{print $1" = 0"}' | sudo tee -a /etc/sysctl.d/1000-cilium.conf
Then we need to refresh sysctl so those settings are applied:
❯ sudo systemctl restart systemd-modules-load
❯ sudo sysctl --system
You should see it applying all the changes:
* Applying /etc/sysctl.d/k8s.conf ...
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
If you do not, the netfilter module may not have loaded properly:
❯ lsmod |grep br_netfilter
br_netfilter 28672 0
bridge 176128 1 br_netfilter
You want to make sure rp_filter
is 0
everywhere as well for cilium:
❯ sudo sysctl -a | grep '\.rp_filter'
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.cilium_host.rp_filter = 0
net.ipv4.conf.cilium_net.rp_filter = 0
net.ipv4.conf.cilium_vxlan.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.enp0s1.rp_filter = 0
net.ipv4.conf.lo.rp_filter = 0
net.ipv4.conf.lxc0965b7b545f7.rp_filter = 0
net.ipv4.conf.lxcb05ffd84ab74.rp_filter = 0
Now lets pull down the container runtime we’ll be using which is containerd.
Ubuntu ships with a very old version of containerd so you need to upgrade to the version shipped from the docker repos: You can find which versions are available by running:
❯ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/docker.gpg
❯ echo "deb https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
❯ sudo apt-get update
❯ sudo apt-cache madison containerd.io
containerd.io | 1.6.8-1 | https://download.docker.com/linux/ubuntu jammy/stable arm64 Packages
containerd.io | 1.6.7-1 | https://download.docker.com/linux/ubuntu jammy/stable arm64 Packages
containerd.io | 1.6.6-1 | https://download.docker.com/linux/ubuntu jammy/stable arm64 Packages
containerd.io | 1.6.4-1 | https://download.docker.com/linux/ubuntu jammy/stable arm64 Packages
containerd.io | 1.5.11-1 | https://download.docker.com/linux/ubuntu jammy/stable arm64 Packages
containerd.io | 1.5.10-1 | https://download.docker.com/linux/ubuntu jammy/stable arm64 Packages
We are going to use the latest version available which was 1.6.8-1
❯ sudo apt-get install containerd.io=1.6.8-1 -y
Then we'll setup a configuration that enables containerd to use the systemd
cgroup. We are hard coding this config instead of using containerd config default
because that currently has had a bug
for many years that generates an invalid config.
❯ cat <<EOF | sudo tee /etc/containerd/config.toml
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
EOF
❯ sudo systemctl restart containerd.service
You can verify its running with ctr:
❯ sudo ctr --address /var/run/containerd/containerd.sock containers list
CONTAINER IMAGE RUNTIME
Now that this is working we can move on to getting kubernetes installed!
Using kubeadm!
Now we need to get the kubernetes tools installed onto the system. I’m going to be using 1.23 but to find the latest version you can run:
❯ sudo apt-cache madison kubeadm|head -n2
kubeadm | 1.23.5-00 | http://apt.kubernetes.io kubernetes-xenial/main amd64 Packages
kubeadm | 1.23.4-00 | http://apt.kubernetes.io kubernetes-xenial/main amd64 Packages
Then install the version you want, we install kubelet and kubeadm here to make sure the versions align:
❯ sudo apt-get install kubeadm=1.23.5-00 kubelet=1.23.5-00 kubectl=1.23.5-00 -y
This will pull in a few tools, including an alternative to ctr
that we used earlier called
crictl
. You can check that it is available to you doing this:
❯ sudo crictl --runtime-endpoint=unix:///var/run/containerd/containerd.sock ps
We can finally init our cluster:
❯ sudo kubeadm init
Once that finishes running it should give you some tips setup your configuration, it should look like this:
❯ mkdir -p $HOME/.kube
❯ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
❯ sudo chown $(id -u):$(id -g) $HOME/.kube/config
You can run those on the master node for now, but later I'll show you how to move the config to your host computer.
Now you should be able to check that your node is not ready yet:
❯ kubectl get nodes
NAME STATUS ROLES AGE VERSION
controlplane NotReady control-plane,master 4m16s v1.23.5
Note: If you recieve "The connecto to the server was refused" error,
The cluster starting up and getting all the dependencies running could take
a bit of time. So if you aren't able to communicate right away you can check
which pods are up and running with crictl
. You'll need kube-apiserver
up
and running. If it isn't you can check:
❯ sudo crictl --runtime-endpoint=unix:///var/run/containerd/containerd.sock ps -a
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
8322192c4605c bd8cc6d582470 36 seconds ago Running kube-proxy 4 344c4f7fffbe8 kube-proxy-drm46
30ce27c40adb2 81a4a8a4ac639 2 minutes ago Exited kube-controller-manager 4 3a819c3a864b2 kube-controller-manager-controlplane
7709fd5e92898 bd8cc6d582470 2 minutes ago Exited kube-proxy 3 7cc6922c82015 kube-proxy-drm46
10432b81d7c61 3767741e7fba7 2 minutes ago Exited kube-apiserver 4 e64ddf3679d98 kube-apiserver-controlplane
which will show you pods that have exited. You can grab the container ID for kube-apiserver and read its logs:
❯ sudo crictl --runtime-endpoint=unix:///var/run/containerd/containerd.sock logs 10432b81d7c61
There are a few ways to figure out why the node isn’t ready yet. Usually I would check the
kubelet
logs first:
❯ sudo journalctl -flu kubelet
-- Logs begin at Sun 2022-04-17 19:22:19 AST. --
Apr 17 20:53:15 controlplane kubelet[19727]: E0417 20:53:15.951350 19727 kubelet.go:2347] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized"
Apr 17 20:53:20 controlplane kubelet[19727]: E0417 20:53:20.952148 19727 kubelet.go:2347] "Container runtime network not ready" networkReady="NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized"
It is clear the problem is that we are missing the CNI. The other way you can find out what is going on is describing the node:
❯ kubectl describe node controlplane
This will have a lot of information but if you scroll through there looking at Reason
you
might see something useful. In this case under Lease
you would see:
❯ kubectl describe node controlplane|grep NotReady
Ready False Sun, 17 Apr 2022 20:53:37 -0400 Sun, 17 Apr 2022 20:43:07 -0400 KubeletNotReady container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialize
Lets get our CNI installed, we’ll be using cilium!
Setting up your CNI!
Cilium has great documentation over here,
but I’ll walk you through it anyways. I do recommend checking out their documentation so you
are familiar with it. We will use helm
to pull down the version of cilium we want:
❯ curl -fsSL https://baltocdn.com/helm/signing.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/helm.gpg
❯ sudo apt-get install apt-transport-https --yes
❯ echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
❯ sudo apt-get update
❯ sudo apt-get install helm
Now we can install cilium! It is very important that you pay attention to the compatibility of cilium with the version of kubernetes you are intstalling. Check the compatibility list here.
❯ helm repo add cilium https://helm.cilium.io/
❯ helm repo update
Once the repo is added you can list the versions available:
❯ helm search repo -l|head -n8
NAME CHART VERSION APP VERSION DESCRIPTION
cilium/cilium 1.12.1 1.12.1 eBPF-based Networking, Security, and Observability
cilium/cilium 1.12.0 1.12.0 eBPF-based Networking, Security, and Observability
cilium/cilium 1.11.8 1.11.8 eBPF-based Networking, Security, and Observability
cilium/cilium 1.11.7 1.11.7 eBPF-based Networking, Security, and Observability
cilium/cilium 1.11.6 1.11.6 eBPF-based Networking, Security, and Observability
cilium/cilium 1.11.5 1.11.5 eBPF-based Networking, Security, and Observability
cilium/cilium 1.11.4 1.11.4 eBPF-based Networking, Security, and Observability
So we want 1.11.4
:
❯ helm install cilium cilium/cilium --namespace kube-system --version 1.11.4
Now our node should be ready!
❯ kubectl get node
NAME STATUS ROLES AGE VERSION
controlplane Ready control-plane,master 24m v1.23.5
Time to join our worker to the cluster!
Joining a worker to the cluster!
We have to go through the same steps as the controlplane to get the point that we have a
container runtime and kubeadm
. I’m not going to talk about the commands a second time but
I’ll re-iterate them here for ease of following along.
First open up another shell and connect to the worker:
❯ multipass shell worker
Now run the following commands:
❯ echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
❯ curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg|sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/k8s.gpg
❯ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/docker.gpg
❯ echo "deb https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
❯ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
❯ sudo sed -i -e '/net.ipv4.conf.*.rp_filter/d' $(grep -ril '\.rp_filter' /etc/sysctl.d/ /usr/lib/sysctl.d/)
❯ sudo sysctl -a | grep '\.rp_filter' | awk '{print $1" = 0"}' | sudo tee -a /etc/sysctl.d/1000-cilium.conf
❯ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
❯ sudo systemctl restart systemd-modules-load
❯ sudo sysctl --system
❯ sudo apt-get update && sudo apt-get upgrade -y
❯ sudo apt-get install containerd.io=1.6.8-1 -y
❯ cat <<EOF | sudo tee /etc/containerd/config.toml
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
EOF
❯ sudo systemctl restart containerd.service
❯ sudo apt-get install kubeadm=1.23.5-00 kubelet=1.23.5-00 kubectl=1.23.5-00 -y
From there we should be ready to join the cluster. When we ran kubeadm init
previously it
printed a join command out that we could use but I’m going to show you how to do it if you
were coming back later and no longer had that token.
Back on the controplane node run:
❯ kubeadm token create --print-join-command
kubeadm join 192.168.64.7:6443 --token wxs197.cco6mjj9ricvu8ov --discovery-token-ca-cert-hash sha256:bd01c065240fa76f30a02ecb70a8cea6e329c9678994d4da1f6ccac7694b97fb
Now copy that command and run it with sudo
on the worker:
❯ sudo kubeadm join 192.168.64.7:6443 --token wxs197.cco6mjj9ricvu8ov --discovery-token-ca-cert-hash sha256:bd01c065240fa76f30a02ecb70a8cea6e329c9678994d4da1f6ccac7694b97fb
After this completes it’ll take a minute or two for everything to be synced up but if you go back to the master node you should have 2 ready nodes now:
❯ kubectl get nodes
NAME STATUS ROLES AGE VERSION
controlplane Ready control-plane,master 46m v1.23.5
worker Ready <none> 79s v1.23.5
Accessing the cluster outside of the VMs!
Now the final part is to get the admin.conf
as a kubeconfig on your machine so you can control
it from outside of the cluster. To do this we can use scp
multipass transfer controlplane:/home/ubuntu/.kube/config local.config
Normally kubernetes configuration is in ~/.kube/config but I like to maint a separate file for
each cluster and then I set the KUBECONFIG
env var to access it.
❯ export KUBECONFIG=local.config
❯ kubectl get nodes
NAME STATUS ROLES AGE VERSION
controlplane Ready control-plane,master 56m v1.23.5
worker Ready <none> 11m v1.23.5