The kubeadm tool offers the possibility of creating a minimum viable cluster. Some advantages are its simplicity, availability for almost any system and integration for automated provision and cluster life cycle management. All this without giving up the ability to provide consistent and interoperable clusters within the Certified Kubernetes Conformance Program.
In general, the creation and operation of a Kubernetes cluster is nothing more than that of its components and requires us to deal with a series of considerations and orderly instructions that it is often necessary to gather from somewhat scattered documentation.
In particular, I am collecting the necessary instructions to bootstrap a Kubernetes cluster based on the following components and versions:
- Ubuntu 22.04.3 LTS
- Docker Engine 24.0.6
- cri-dockerd 0.3.4
- Kubernetes 1.27.5
- Calico 3.26.1
This post assumes you are familiar with the components of a Kubernetes cluster and is intended as a guide only.
Before starting
Before starting, it is necessary to make sure that each node that is going to be part of the cluster has certain characteristics configured to ensure the correct behavior of the components to be installed.
Forwarding IPv4 and letting iptables see bridged traffic
Pods need to communicate across the cluster transparently and independently of the node where they are deployed, so it is essential that each node has traffic forwarding enabled. Also it is necessary to ensure that the filesystem overlay required for the composition of the container layers is available.
So create a file containing the names of kernel modules that should be loaded at boot time:
1
2
3
4
cat << EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
Then load the kernel modules:
1
2
sudo modprobe overlay
sudo modprobe br_netfilter
And verify that modules are loaded:
1
2
lsmod | grep br_netfilter
lsmod | grep overlay
Now create a file containing the system variables configuration files that should be loaded at boot time:
1
2
3
4
5
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
Let’s apply the kernel parameters at runtime so that the above changes take effect without rebooting:
1
sudo sysctl --system
Finally verify that system variables are indeed set to 1 in your sysctl config:
1
sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
Verifying MAC and product_uuid uniqueness
Some components use the network adapter ID (MAC address) and/or the motherboard ID (product_uuid) to uniquely identify the nodes.
Verify the MAC address is unique for every node:
1
ip link
Verify the product_uuid is unique for every node:
1
sudo cat /sys/class/dmi/id/product_uuid
Checking required ports
When running Kubernetes in an environment with strict network boundaries, be aware of ports and protocols used by its components and verify that they are open to allow communication between them.
On control-plane / master node(s):
1
nc -zv 127.0.0.1 6443 2379-2380 10250 10259 10257
On worker node(s):
1
nc -zv 127.0.0.1 10250 30000-32767
Disabling swap memory
Cluster components are mainly performance- and reliability-oriented. The introduction of swap memory under disk pressure makes it somewhat unpredictable to achieve this purpose.
So disable swapping on all known swap devices and files:
1
sudo swapoff -a
Check that the swap area has indeed been disabled:
1
free -h
And remove the unneeded swap space file:
1
sudo rm /swap.img
Finally comment the entire line in the fstab file to permanently prevent swap space from mounting at startup:
1
2
3
sudo vi /etc/fstab
...
# /swap.img none swap sw 0 0
Installing the container runtime
Installing Docker Engine
Next it’s necessary to install a container runtime that conforms with the Container Runtime Interface (CRI) so that pods/containers can run into each node in the cluster.
As we are choosing to install Docker Engine, add Docker’s official GPG key:
1
2
3
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
Set up the repository:
1
2
3
4
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Update the apt package index and install packages to allow apt to use a repository over HTTPS:
1
sudo apt update && sudo apt install -y ca-certificates curl gnupg
Install Docker Engine, containerd, and Docker Compose for the latest version:
1
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
To install a specific version, first check available versions for each package:
1 2 3 4 5 apt list -a docker-ce | awk '{print $2}' apt list -a docker-ce-cli | awk '{print $2}' apt list -a containerd.io | awk '{print $2}' apt list -a docker-buildx-plugin | awk '{print $2}' apt list -a docker-compose-plugin | awk '{print $2}'And then indicate such version in the installation command:
1 sudo apt install -y docker-ce=<version> docker-ce-cli=<version> containerd.io=<version> docker-buildx-plugin=<version> docker-compose-plugin=<version>
Add your user to the docker group to manage Docker as a non-root user:
1
sudo usermod -aG docker $USER
Re-evaluate groups to activate changes without having to log out/in:
1
newgrp docker
Finally verify that you can run docker commands without sudo:
1
docker run hello-world
You should get an output like this:
1
2
3
Hello from Docker!
This message shows that your installation appears to be working correctly.
...
Configure Docker to start on boot:
1
2
sudo systemctl enable docker.service
sudo systemctl enable containerd.service
Installing cri-dockerd
Originally, Docker Engine was integrated directly with the kubelet code. When Kubernetes moved to use the CRI layer, a temporary adapter called dockershim was added between the CRI and Docker Engine. With Kubernetes 1.24, dockershim is no longer a part of the Kubernetes core and users need to install the cri-dockerd third-party adapter to provide the integration of Docker Engine with Kubernetes.
Use the pre-built cri-dockerd package to install the binary and setup the system to run it as a service:
1
2
wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.4/cri-dockerd_0.3.4.3-0.ubuntu-jammy_amd64.deb -P /tmp
sudo apt install -y /tmp/cri-dockerd_0.3.4.3-0.ubuntu-jammy_amd64.deb
Check the service is running and listening on unix:///var/run/cri-dockerd.sock (the endpoint socket by default):
1
2
systemctl status cri-docker.service
systemctl status cri-docker.socket
Installing the kube tools
Next it’s time to install the command to bootstrap the cluster (kubeadm), the component to start pods and containers (kubelet) and the command line tool to talk to your cluster (kubectl).
kubeadmwill not install or managekubeletorkubectlfor you, so you will need to ensure they match the version of the Kubernetes control-plane you wantkubeadmto install for you.
Update the apt package index and install packages needed to use the Kubernetes apt repository:
1
sudo apt update && sudo apt install -y apt-transport-https ca-certificates curl
Download the Google Cloud public signing key:
1
curl -fsSL https://dl.k8s.io/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg
Add the Kubernetes apt repository:
1
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
Update apt package index and install kubelet, kubeadm and kubectl for the latest version:
1
sudo apt update && sudo apt install -y kubelet kubeadm kubectl
To install a specific version, first check available versions for packages:
1 curl -s https://packages.cloud.google.com/apt/dists/kubernetes-xenial/main/binary-amd64/Packages | grep Version | awk '{print $2}' | sort -V | uniqAnd then indicate such version in the installation command:
1 sudo apt install -y kubelet=<version> kubeadm=<version> kubectl=<version>
Pin kubelet, kubeadm and kubectl versions:
1
sudo apt-mark hold kubelet kubeadm kubectl
Enable kubectl autocompletion:
1
echo 'source <(kubectl completion bash)' >> ~/.bashrc
Extend shell completion to work with the k alias (optional):
1
2
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -F __start_kubectl k' >> ~/.bashrc
Reload .bashrc in order to new configuration take effect in current session:
1
source ~/.bashrc
kubectlis supported within one minor version (older or newer) of kube-apiserver.
To know at any time which versions of
kubectland kube-apiserver are running, check the client and server version respectively from the output of the following command:
1 kubectl version
Installing the cluster control-plane components
The control-plane node (master node) is the machine where all the decisions are made, running components such as the etcd (the cluster database) and the API Server (which the kubectl command line tool communicates with).
Initialize the control-plane node specifying a suitable CIDR block for the CNI based Pod network add-on and the container runtime endpoint:
1
sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --cri-socket unix:///var/run/cri-dockerd.sock
Take care that your pod network must not overlap with any of the host networks.
Copy the
kubeadmoutput join command with the token and the discovery-token-ca-cert-hash to later join additional nodes to the cluster by:
1 kubeadm join <control-plane-host>:<control-plane-port> --token <token> --discovery-token-ca-cert-hash sha256:<hash>
Make kubectl work for your non-root user:
1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Installing the pod network add-on
Now it’s time to deploy a Container Network Interface (CNI) based Pod network add-on so that your pods can communicate with each other.
We’ve chosen Calico as is a widely adopted, battle-tested open source networking solution for Kubernetes, providing two major services for Cloud Native applications such as:
- Network connectivity between workloads.
- Network security policy enforcement between workloads.
Requirements
Create the following configuration file to prevent NetworkManager from interfering with the interfaces:
1
2
3
4
5
sudo mkdir -p /etc/NetworkManager/conf.d/
cat << EOF | sudo tee /etc/NetworkManager/conf.d/calico.conf
[keyfile]
unmanaged-devices=interface-name:cali*;interface-name:tunl*;interface-name:vxlan.calico;interface-name:vxlan-v6.calico;interface-name:wireguard.cali;interface-name:wg-v6.cali
EOF
Installing Calico by operator
Get the latest stable version tag from the projectcalico/calico repository:
1
VERSION=$(curl -sL https://api.github.com/repos/projectcalico/calico/releases/latest | jq -r ".name")
Install the Calico operator and the custom resource definitions (CRDs):
1
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/$VERSION/manifests/tigera-operator.yaml
Download the custom resources necessary to configure Calico and customize the manifest for the 10.244.0.0/16 CIDR:
1
2
curl https://raw.githubusercontent.com/projectcalico/calico/$VERSION/manifests/custom-resources.yaml -O
sed -i 's/cidr:.*/cidr: 10\.244\.0\.0\/16/' custom-resources.yaml
Finally create the manifest in order to install Calico:
1
kubectl create -f custom-resources.yaml
Wait until each pod is in running status:
1
watch kubectl get pods -n calico-system
Checking the installation
Cluster DNS (CoreDNS) will only start up after a network is properly installed.
Verify that CoreDNS pods are running:
1
kubectl get pods --all-namespaces
Verify your node is also ready:
1
kubectl get nodes -o wide
Remove the taint on the control-plane so that you can schedule pods on it:
1
kubectl taint nodes --all node-role.kubernetes.io/control-plane-
In case you plan to keep the master node dedicated to the control-plane you can skip the previous step, but some worker node must be joined later in order to deploy your applications.
Install calicoctl
The calicoctl command line tool is used to manage Calico network and security policy, to view and manage endpoint configuration, and to manage a Calico node instance.
To install calicoctl as a binary on a single host:
1
2
3
4
POD=$(kubectl -n calico-system get pod -l k8s-app=calico-kube-controllers -o jsonpath="{.items[0].metadata.name}")
VERSION=$(kubectl -n calico-system describe pod $POD | grep Image: | cut -d ':' -f3)
sudo curl -L https://github.com/projectcalico/calico/releases/download/$VERSION/calicoctl-linux-amd64 -o /usr/local/bin/calicoctl
sudo chmod +x /usr/local/bin/calicoctl
Verify the command was properly installed by:
1
calicoctl version
Make sure you always install the version of
calicoctlthat matches the version of Calico running on your cluster.
Joining a new worker node
Whether or not you have allowed the deployment of pods on the master node, run this on any machine you wish to join an existing cluster:
1
sudo kubeadm join <control-plane-host>:<control-plane-port> --token <token> --discovery-token-ca-cert-hash sha256:<hash>
Check that the new node has been properly added by running the following command on the control-plane node:
1
kubectl get nodes
In case you need to retrieve the token, run the following command on the control-plane node:
1 kubeadm token list
In case you need to retrieve the discovery-token-ca-cert-hash, run the following command on the control-plane node:
1 2 openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | \ openssl dgst -sha256 -hex | sed 's/^.* //'
In case you are joining a node after token expired, run the following command on the control-plane node:
1kubeadm token create --print-join-command
Deprovisioning a node cleanly
Everything that has a beginning has an end, if you want to deprovision your cluster and revert all changes made by the kubeadm command cleanly, you should first drain the node and make sure that the node is empty, then deconfigure the node.
To drain the node marking the node as unschedulable to prevent new pods from arriving and evicts or deletes all pods, run:
1
kubectl drain <node_name> --delete-emptydir-data --force --ignore-daemonsets
Then reset the state installed by kubeadm:
1
sudo kubeadm reset
If you wish to reset iptables rules manually run:
1
sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F && sudo iptables -X
If you wish reset IPVS tables manually run:
1
sudo ipvsadm -C
Now you are safe to remove the node from the cluster:
1
kubectl delete node <node_name>
You are now ready to start over, running kubeadm init or kubeadm join with the appropriate arguments.
Controlling your cluster from machines other than the control-plane node
In order to get a kubectl on some other computer to talk to your cluster, you need to copy the administrator kubeconfig file from your control-plane node to your workstation.
Assuming SSH access is disabled for root in your control-plane node, copy the kubeconfig file to your user home dir and set your user as owner:
1
2
sudo cp /etc/kubernetes/admin.conf $HOME/.
sudo chown $USER: admin.conf
Copy the kubeconfig file from your remote control-plane node and give a name of your choice:
1
2
scp $USER@<control-plane-host>:~/admin.conf ~/.kube/
mv ~/.kube/admin.conf ~/.kube/config-new
Append the kubeconfig file to your current KUBECONFIG env variable and update your current kubeconfig file:
1
2
export KUBECONFIG=~/.kube/config:~/.kube/config-new
kubectl config view --flatten > ~/.kube/config
Check that your new cluster is listed as a Kubernetes context and set it to control remotelly:
1
2
kubectl config get-contexts
kubectl config set-context <cluter_name>
Summary
Congratulations! At this point you must have a perfectly operational and remotely accessible Kubernetes cluster ready to receive the deployment of your favorite applications.
In future posts I will try to explain how to deploy some of them.

Comments powered by Disqus.