18 min read

[Deprecated] K8s Installation: The What's and Why's.

To understand, one must do and to understand well, one must do and teach others. In this post, we install K8s 'on-prem' and discuss the reasons behind some of the steps taken.
[Deprecated] K8s Installation: The What's and Why's.
☣️
Updated March 14th 2024

The conceptual basis for this article has not changed however the repo's used as part of the installation flow are deprecated and provided scripts may not work.

I am equal parts terrified and positively awed by K8s. Its a beast that can crush your confidence or become your road to tech fame and glory.

I am on a personal journey to tame this beast in anyway I am able to and where else should I start but with its installation process and understand the WHY behind its WHAT.

Requirements.

  • 4 nodes (I am using AWS EC2) with Ubuntu 20.04
  • Each node should have 2 vCPU and 2 GB RAM
  • Each node should have about 20GB of storage space
  • Swap needs to be turned off on each of node
4 nodes w/ their Private IPs. CP = Control Plane, Wx = Worker Node
📢
Though not expected, some understanding of Linux networking will likely make understanding the rationale provided for each step easier.

Kubernetes installation has 3 distinct stages.

Figure 1: The 3 stages in installing Kubernetes.
📢
These set of steps are repeated once on what will be the Control Plane and, barring some steps, once on each Worker Node that has to join the cluster.

Stage 1: Pre-Bootstrap.

This phase is all about downloading the dependencies that will support different functions in the cluster. Some major steps taken during this phase are:

  • Disable swap.
  • Downloading, configuring and activating Containerd (our chosen container run time of choice) and its dependencies like Overlay and Br_NetFilter.
  • Downloading other helper libraries and modules.
  • Downloading Kubectl, Kubeadm and Kubelet.

Stage 2: During Bootstrapping.

The actual bootstrapping of a cluster happens a lot later in the sequence of events. During bootstrapping, the nuts and bolts of a cluster are set up, tuned, fixed, tested and when everything is finished, if you are on the Control Plane, you will be provided a kubeadm join command that can be used for adding Worker Nodes to the cluster.

Stage 3: Post Bootstrapping.

Once the node(s) has/have gone through the bootstrapping, we have some additional steps to perform:

  • Downloading and configuring Calico, the CNI Standard based networking interface.
  • Configuring and updating Ubuntu system files where needed.

Stage 1: Pre-Bootstrap.

📢
The steps below are for the Control Plane.

Assume root privileges on all nodes.

Type sudo -i at the console prompt. Your prompt should change colors and/or the text being displayed as the prompts name.

Disable swaps.

Type swapoff -a in each terminal and press enter. Your prompt should return nothing and moves to the next line without any output being printed.

Figure 1: Turn disk swapping off.

Why: Opinion on this matter is divided. The fine folks who develop Kubernetes have not included the use of Linux swap feature in the way some K8s components (like the Kube-Proxy) behave. Refer to online discussion boards for deeper insight.
In particular, this post has an explanation and a complaint about swap being turned off.

Perform a general system update.

Before downloading any software on Linux, it is advisable to update and upgrade existing libraries. Figure 2 below shows the commands to use:

Figure 2: Update and Upgrade existing libraries and installations.

Download modules/softwares that are needed for different parts of the pre and actual bootstrapping stages.

The various dependencies that need to be downloaded are shown below:

Figure 3: Additional dependencies that need to be downloaded as part of our k8s install.
🐒
The command in Figure 3 is provided here for easy cut and paste:
apt install curl apt-transport-https vim git wget gnupg2 ca-certificates software-properties-common -y.

The purpose for some of the downloaded modules is shown below:

Figure 4: Modules and their purposes. All three do not have any immediate use but will be important in following steps.

Download Pre-requisites for Containerd.

Containerd has a dependency on 2 Linux modules: overlay and br_netfilter. Both of these have to be loaded before any other downloads for Containerd should happen, but what are they and why do we need them?

  • overlay allows one, usually read-write, directory tree to be overlaid onto another, read-only directory tree.
  • br_netfilter module is required to enable transparent masquerading and to facilitate Virtual Extensible LAN (VxLAN) traffic for communication between Kubernetes pods across the cluster.
📢
Containerd is a container runtime that follows the OCI standards. Think of it as a competitor to Docker.
Figure 5: Containerd needs overlay and br_netfilter modules.

Update kernel networking rules to allow traffic forwarding in the cluster.

Containerd requires the ability to forward traffic between containers and, as mentioned, requires the overlay and br_netfilter modules for this purpose. However, we have to explicitly set 3 kernel level system parameters to let the Containerd + overlay + br_netfilter trifecta achieve its purpose.

This done by creating a file called kubernetes.conf in the /etc/sysctl.d/ folder.

Figure 6: The contents of /etc/sysctl.d folder before we add the kubernetes.conf file in it.

Type the commands outlined in red. On pressing enter, the file is created and saved with the flags for kernel networking parameters set to 1 (true).

Figure 7: Create a kubernetes.conf file with networking flags set to 1.
⌨️
Copy and paste the commands in Figure 7:
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

Once the commands, as shown in Figure 4 are completed, go back to the /etc/sysctl.d folder and you should see the brand new kubernetes.conf file there.

Figure 8: The kubernetes.conf file has been created.
Figure 9: Confirm the contents of the kubernetes.conf file.

Since the changes were made to kernel level variables, it is important to 'soft reboot' the OS (i.e. reload the latest list of variables without having to restart the VM/computer). Typing sysctl --system achieves this purpose.

Figure 10: Soft reboot will load the newly added configuration files content.
📢
The sysctl --system command will load the contents of EACH AND EVERY file in the /etc/sysctl.d folder. Therefore, it is not entirely necessary to use kubernetes.conf as the name of the file we saved. You can choose any file name you prefer OR you can use one of the existing .conf files for recording your entries.

Let the OS know 'overlay' and 'br_netfilter' need to be loaded on each boot.

To ensure overlay and br_netfilter modules are loaded every time a new boot occurs, let the OS (or the modules-load.d folder) know about them. Before doing this, confirm there is no configuration file for Containerd already present. This can be done by typing the commands shown in Figure 8 below:

Figure 11: A pre-step look inside the modules-load.d directory. Notice there is no .conf file for Containerd.

To create this configuration file for Containerd, type the commands in Figure 9 below:

Figure 12: The commands outlined in red will create a containerd.conf file and contains the name of the modules needed for Containerd to work.
⌨️
Copy and paste for Figure 12:
cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF

For sake of caution, its always advisable to confirm that dynamic configuration files (like containerd.conf or kubernetes.conf) were actually created where they were supposed to be:

Figure 13: Running the cat command inside the /etc/modules-load.d directory shows containerd.conf was successfully created.

Soft-reboot the kernel again. Refer to Figure 7 to see how.

Download and install Containerd.

Containerd installation is a multi-step activity.

1st: Download a PGP key for the repo from where the binary will be taken and save it into /etc/apt/keyrings folder.

There is no folder called /etc/apt/keyrings and therefore has to be created as shown in Figure 14 below:

Figure 14: The red outlined code creates a directory and the blue outlined code downloads a PGP key from the provided URL, and converts it into a GPG key before saving it to the /etc/apt/keyrings folder.
⌨️
Copy and paste for Figure 14:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

Once the folder has been made, download the authentication key for the repo we are using to download Containerd from. This authentication key will be used by the OS and the repo to authenticate each other and reduce risk of malicious men-in-the-middle.

🚥
Even though the download url ends in /ubuntu/gpg, in reality the key we download is in the PGP format. After downloading it, we pass it through the gpg tool with the --dearmor flag and ask for the converted (from PGP to GPG) file to be saved in the keyrings folder.

2nd: Add the URL for the actual repo from where Containerd will be downloaded to a source list.

Download of software/binaries in Linux can happen in 2 ways:

  • Direct download using wget
  • Adding the location URL for a Debian packages repo to a sources list and then using apt or apt-get to download the packages from ONE of the sources in the list

For Containerd, we have to get it from an online repo that contains Debian packages. The way to proceed is to either use an existing source list or create a new one specifically for Containerd - I will create a file called docker.list in /etc/apt/sources.list.d.

Before going on, lets quickly confirm that there is no docker.list in the /etc/apt/sources.list.d.

Figure 15: sources.list.d is empty (as seen by total 0).

Figure 16 shows the commands that need to be entered at the terminal to (a) create a source list file (docker.list) and (b) save the URL for Containerd in docker.list.

Figure 16: The URL for the repo will get saved in the docker.list file.
⌨️
Copy and paste for Figure 16:
echo \
"deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list 

Confirm that docker.list was indeed created and a URL was saved inside it:

Figure 17: The docker.list file has the URL for the Containerd download.

3rd: Download Containerd using apt-get.

Figure 18: Download Containerd using the URL saved in the docker.list file.

Configure Containerd.

Figure 19: Pipe the default values for Containerd configuration to the config.toml file.
🚥
The command in Figure 19 is printed below:
$ containerd config default | tee /etc/containerd/config.toml

Update config.toml file by setting SystemdCgroup flag to 'True'.

Being a container run time, Containerd will need to create process control groups for each container launched. In Debian systems like Ubuntu, the control groups are controlled by Systemd and in default Containerd configuration, SystemdCgroup is set to false (see Figure 20 below):

Figure 20: SystemdCgroup = false in /etc/containerd/config.toml file.

Use the sed command to replace the 'false' with 'true':

Figure 21: Enable the use SystemdCgroups for Containerd.
⌨️
Copy and paste for Figure 21:
sed -e 's/SystemdCgroup = false/SystemdCgroup = true/g' \
-i /etc/containerd/config.toml

Finally, 'soft-reboot' the kernel.

Figure 22: Soft-Reboot Containerd.

You can also confirm the Containerd service is active by finding its status.

Figure 23: Containerd is active and running as it should.

Download and install Kubernetes binaries.

As was the case with Containerd, downloading and installing kubernetes binaries is a multi-step activity.

1st: Create a kubernetes.list file and save the URL from where to download binaries.

As was done for Containerd, make a kubernetes.list file and save the URL for a Kubernetes repo in it. We can use the existing /etc/apt/sources.list.d folder (which already containes the docker.list file for our Containerd repo).

Figure 24: Notice there is no kubernetes.list file in /etc/apt/sources.list.d folder.

Make a file (kubernetes.list) inside the /etc/apt/sources.list.d folder:

Figure 25: Use vim or nano to make an empty file inside the /etc/apt/sources.list.d folder.

Type the URL for the repo from where binaries will be downloaded:

Figure 26: The url starts at deb and ends at main.

2nd: Add a GPG key for the repo.

This is an important step for ensuring authenticity of the binaries being downloaded (and was also done for the Containerd repo earlier).

Figure 27: Save the GPG key to apt-key folder.
⌨️
Copy and paste for Figure 27:
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -

Figure 28: Update using apt-get.

3rd: Install Kubernetes.

Figure 29: Install kubeadm, kubelet and kubectl v=1.25.1-00
⌨️
Copy and paste for Figure 29:
apt-get install -y kubeadm=1.25.1-00 kubelet=1.25.1-00 kubectl=1.25.1-00

4th: Freeze the version of kubeadm, kubelet and kubectl.

Every time the nodes housing kubernetes are rebooted for security updates or other reasons, updates for installed softwares may be installed. Since we want to make sure this does not happen to Kubernetes components like Kubeadm, Kubelet and Kubectl, we have to explicitly let the OS know this.

By using the apt-mark hold module, we can do this easily:

Figure 30: Freeze versions of kubelet, kubeadm and kubectl.

Downloads Calico for the clusters pod network.

A Pod network should allow:

  • Container-to-Container
  • Pod-to-Pod
  • Pod-to-Service, and
  • External-to-Service communications.

Calico is one such networking interface, and we will download it's yaml manifest and deploy it:

Figure 31: Download Calico's YAML Manifest from https://raw.githubusercontent.com/projectcalico/calico/v3.25.0/manifests/calico.yaml

On a tangent: The CIDR for Pods that will be launched in the cluster can be found inside the calico.yaml file.

Figure 32: All Pods will have an IP from the 192.168.0.0/16 CIDR.

Add a DNS entry for the Control Plane Node into the /etc/hosts file.

Ubuntu has a file called /etc/hosts where IP addresses and an alias to refer to them by is documented.

There are 2 ways of finding out the IP address for the node you are in:

Figure 33: Use hostname -i or ip addr show to get the nodes IP address. In this example, the address is 192.168.0.51.

Once you have the IP address, note in the /etc/hosts file as shown:

Figure 34: Add your nodes IP address and the k8scp alias (or any alias of your choice).

We installed kubelet as a service and we can check its health like we can for any Ubuntu service:

Figure 35: Note status=1/FAILURE along with Active: activating.

Kubelet is failing to remain active. This does not mean the kubelet service is not functioning but rather it is trying to perform its role but doesn't know what to do.

On deeper investigation, notice that the kubelet.service reads its instructions from the 10-kubeadm.conf file in /etc/systemd/system/kubelet.service.d folder:

Figure 36: Environment variables are pointing to files that do NOT exist.

The first variable "KUBELET_KUBECONFIG_ARGS" points towards a file called bootstrap-kubelet.conf and kubelet.conf in /etc/kubernetes folder.

Figure 37: No bootstrap-kubelet.conf and kubelet.conf can be seen inside /etc/kubernetes.

The second variable "KUBELET_CONFIG_ARGS" points towards a directory & file (/kubelet/config.yaml) in /var/lib location:

Figure 38: /var/lib does not have any directory called kubelet (and that also means there is no config.yaml file either).

Since kubelet does not have what it needs to continue functioning, it is stuck in a Crash Loop. The bootstrap process will generate and save these missing files in their respective locations and that will make the kubelet come out of the crash loop.


Stage 3: Bootstrapping

The moment of truth. We spent a lot of time getting ready for this stage.

Create a Cluster Configuration file for initiating the bootstrapping journey.

Using a text editor (vim or nano), create a file titled kubeadm-config.yaml.

Figure 39: The kubeadm-config.yaml file

In Figure 39 above, note the values used for kubernetesVersion (=1.25.1), the controlPlaneEndpoint (the alias for the Control Plane and the default port kube-api server listens on) and the podSubnet (which has the same CIDR as we saw earleir inside Calico.yaml).

Kick off the bootstrapping process.

Figure 40: kubeadm init --config=kubeadm-config.yaml --upload-certs | tee kubeadm-init.out
⌨️
Copy and paste for Figure 40:
kubeadm init --config=kubeadm-config.yaml --upload-certs | tee kubeadm-init.out

What happens during the bootstrapping process?

As bootstrapping starts and continues, there are a number of phases that the cluster creation goes through:

Figure 41: The various phases bootstrapping involves.

Performing Pre-Flight Checks.

Figure 41: Installer checks if the right images needed for the cluster are downloaded and if not, they are.

Create a Certificate Authority.

Figure 42: A series of certificates and keys are generated for securing communication between the kube-api server and other kubernetes components.
🚥
What is the Certificate Authority?

Kubeadm creates a self-signed certificate authority. This is used for:
- Securing cluster communications: The CA will generate certificates that will be used by the kube-api server to encrypt its HTTP messages with the rest of the cluster. Because of the use of a certificate, all HTTP messages get encrypted (TLS) and are sent as HTTPS streams.
- Authentication of users and cluster components: Various components like kubelets, kube-schedulers etc will have to provide some sort of "its me, really" comfort to the kube-api server, and this is achieved through certificates issued by the CA (which is not to say that we can't use an external PKI to do this job if that is needed).

All certificates in use are kept in the /etc/kubernetes/pki folder and will be distributed to each node that joins the cluster.
Figure 43: The generated certificates and keys are saved in the /etc/kubernetes/pki folder.

Generate kubeconfig files for various kubernetes components.

Figure 44: Generation of kubeconfig files.

In Figure 37, we noted only one directory inside /etc/kubernetes. However, now we can confirm that the directory has been used to store a number of directories and files.

Figure 45: As part of bootstrapping, 36 items have been written to the /etc/kubernetes folder.

We can check the health of the kubelet service again:

Figure 46: Unlike in Figure 35, kubelet is now active.

Generate static pod manifests.

Figure 47: The kubelet, now active, is going to deploy k8s system pods which are inside /etc/kubernetes/manifests.

Taint Control Plane Node.

Figure 48: Taints and Labels are needed to ensure make sure at least one node is designated the Control authority.

Generate bootstrap tokens for other nodes to join.

For other nodes to join the cluster, they must present authentication and authorization tokens. During the bootstrapping, these tokens are generated and mapped to the various roles and permissions that will be invoked as more nodes are added to the cluster:

Figure 49: Bootstrapping tokens.

Start DNS and Kube-Proxy pods.

These are add-ons that get deployed as part of bootstrapping:

Figure 50: Deploy add-ons pods.
🚀
Successful kubernetes installation will show you a message starting with kubeadm join. This is a command that helps joining worker nodes to the cluster.

Stage 3: Post Bootstrapping.

Get out of root mode

Type logout or exit at the prompt.

Move the admin.conf file to the /.kube/config folder.

Figure 51: Move admin.conf to its new location (as a non-root user).

Install the Calico Pod Networking Interface that was downloaded earlier.

Figure 52: Use kubectl apply -f calico.yaml to set up the pod networking for the cluster.

Use kubectl to confirm the Control Plane is ready.

Figure 53: 'Houston, we have lift-off'.


We can now add additional nodes (Worker Node) to the cluster.

The instructions are the same as for setting up the Control Node.

📢
Any text in RED should be read as clarification texts and not as a script that needs to be executed.

Steps:

$ sudo -i

$ sudo -i
$ apt-get update && apt-get upgrade -y
$ sudo apt install curl apt-transport-https vim git wget gnupg2 \
   software-properties-common lsb-release ca-certificates uidmap -y

$ sudo swapoff -a
$ sudo modprobe overlay
$ sudo modprobe br_netfilter
$ cat >>EOF | 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
$ sudo sysctl --system
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
   | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

$ echo "deb [arch=$(dpkg --print-architecture) \
   signed-by=/etc/apt/keyrings/docker.gpg]    https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

$ apt-get update && apt-get install containerd.io -y
$ containerd config default | tee /etc/containerd/config.toml
$ sed -e 's/SystemdCgroup = false/SystemdCgroup = true/g'
    -i /etc/containerd/config.toml

$ systemctl restart containerd
$ nano /etc/apt/sources.list.d/kubernetes.list
   Inside the file editor, type: deb http://apt.kubernetes.io/ kubernetes-xenial main
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
$ apt-get update
$ apt-get install -y kubeadm=1.25.1-00 kubectl=1.25.1-00 kubelet=1.25.1-00
$ apt-mark hold kubeadm kubectl kubelet
    Go back to the Control Node and using hostname -i and find its private IP address
📢
The next steps are typically done if the tokens generated and shared with us at the end of the Control Plane installation have expired (they have a 2 hour limit). If its been under 2 hours since the Control Plane was set up, the kubeadm join command in the kubeadm-out.init can be used and the next three steps can be avoided.

Note: the output from the next 2 steps should be copied for future use.

$ sudo kubeadm token create
$ openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa \
   -pubin -outform der 2</dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'

$ nano /etc/hosts
    Enter the private IP address for the Control Plane and alias it as k8scp.
$ kubeadm join --token <the value from sudo kubeadm token create> k8scp:6443
   --discovery-token-ca-crt-hash sha256:<value from the openssl command above>
📢
As mentioned, if the time elapsed time between now and the set up of the Control Plane is under 2 hours, you can use the kubeadm join command (including token and discovery-token-ca-crt-hash value).
$ exit
📢
The steps described for one Worker Node can be repeated on as many more as you need for your cluster.

Confirm the cluster is set up and has 4 nodes.

Figure 54: We have a 4 node cluster, 1 Control Plane Node and 3 Worker Nodes.

I write to remember and if in the process, I can help someone learn about Containers, Orchestration (Docker Compose, Kubernetes), GitOps, DevSecOps, VR/AR, Architecture, and Data Management, that is just icing on the cake.