Home Bootstrapping Kubernetes clusters with kubeadm
Post
Cancel

Bootstrapping Kubernetes clusters with kubeadm

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).

kubeadm will not install or manage kubelet or kubectl for you, so you will need to ensure they match the version of the Kubernetes control-plane you want kubeadm to 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 | uniq

And 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

kubectl is supported within one minor version (older or newer) of kube-apiserver.

To know at any time which versions of kubectl and 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 kubeadm output 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 calicoctl that 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:

1
kubeadm 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.

This post is licensed under CC BY 4.0 by the author.

-

Managing different versions of Kubernetes clusters with asdf

Comments powered by Disqus.