Start your free 14-day ContainIQ trial

Using kubeadm to Create a Cluster | Installation & Tutorial

Kubeadm is a tool to get a jump start on building Kubernetes clusters that conform to best practices. In this article, you’ll learn how to install and create clusters using kubeadm.

March 13, 2023
Damaso Sanoja

The flexibility of Kubernetes, the leading open-source platform for managing containerized applications and services, is not limited to its portability or ease of customization. It also has to do with the options available for deploying it in the first place. You can use K3S, kops, minikube, and similar tools to deploy a basic cluster. However, if you’re looking for a tool that’s both simple and powerful starting point, kubeadm is the best choice.

kubeadm not only allows you to bootstrap a Kubernetes cluster with ease, but also allows you to configure and customize cluster components. This article will walk you through deploying a Kubernetes cluster using kubeadm.

What are the Use Cases for kubeadm?

The power of kubeadm lies in the fact that it can interact with both the core components of the cluster—such as the kube-apiserver, kube-controller, kube-scheduler, and etcd backend—as well as other components, like kubelet and the container runtime. This flexibility makes it the ideal choice for many use cases. For example:

  • Automate the creation of clusters using scripts or tools like Ansible.
  • Knowing how to use kubeadm is required for the CKA and CKS exams, making it a must for novice users who want to learn how to initialize and configure a Kubernetes cluster.
  • Since kubeadm can be used for both local and remote clusters, it’s an ideal tool for both test and production environments.

Implementing kubeadm | Step By Step Tutorial

As detailed in the documentation, you’ll need the following to bootstrapping clusters with <terminal inline>kubeadm<terminal inline>.

  • One or more instances running Linux OS, preferably Debian or Red Hat based distros. For the purposes of this tutorial, we will use two virtual machines running Ubuntu 20.04 LTS, one for the control-plane node and one for the worker node.
  • At least 2 GB of RAM for each instance; however, 4 GB is recommended to ensure that your test environment runs smoothly.
  • The node that will serve as the control plane should have at least 2 vCPUs. For this tutorial, both nodes will use 2 vCPUs.
  • Full network connectivity between the nodes that make up the cluster, using either a public or private network.
  • A unique hostname, MAC address, and <terminal inline>product_uuid<terminal inline> for each node.
  • Traffic allowed through your firewall using the ports and protocols described in the documentation.
  • Disabled swap memory on each node; otherwise, <terminal inline>kubeadm<terminal inline> will not work correctly.

In addition to the prerequisites above, you’ll need a version of <terminal inline>kubeadm<terminal inline> capable of deploying the version of Kubernetes you require. You can find more information about Kubernetes’ version and version-skew support policies in the documentation. For this tutorial, you will install Kubernetes v1.23.

Last but not least, this tutorial will also require a user with administrative privileges (sudo user) to avoid executing commands as <terminal inline>root<terminal inline>.

Creating a Sudo User

Connect via SSH to the control-plane node and create a user by executing the command below. In this example, the user is called <terminal inline>containiq<terminal inline>, but you can use any name that suits your needs.

sudo adduser containiq

Add the new user to the <terminal inline>sudo<terminal inline> group with the command:

sudo usermod -aG sudo containiq

Change to the new user with the following command:

su - containiq

Before proceeding to the next step, update the operating system by running the command:

sudo apt update && sudo apt upgrade -y

Disabling Swap Memory

To ensure that the node does not use swap memory, run the following command:

sudo swapoff -a && sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

This command performs two operations. First, it disables swap memory, then it comments out the swap entry in <terminal inline>/etc/fstab<terminal inline>, which ensures that swap will remain disabled after each reboot.

Setting Up Unique Hostnames

As explained in the documentation, you need to ensure that each node in the cluster has a unique hostname, otherwise initialization will fail. For this tutorial, the control-plane node will be called <terminal inline>primary<terminal inline>, and the <terminal inline>worker<terminal inline> node worker.

In the control-plane node, use the following command to change the hostname:

sudo hostnamectl set-hostname primary

Next, edit <terminal inline>/etc/host<terminal inline>s to match the chosen hostname:

sudo nano /etc/hosts

The file should look similar to the following:

# /etc/hosts primary

# The following lines are desirable for IPv6 capable hosts

::1 primary ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

Once the changes have been saved, restart the node:

sudo reboot

You must follow the same procedure explained for the <terminal inline>primary<terminal inline> node in the <terminal inline>worker<terminal inline> node. That is to say, assigning a unique hostname (in this case, <terminal inline>worker<terminal inline>), disabling swap memory, and creating a sudo user. Once you have completed this, you can continue with the next steps, where you’ll install the operating-system-level packages required by Kubernetes on each node.

Installing Docker Engine

In order to work, Kubernetes requires you to install a container runtime. Available options include containerd, CRI-O, and Docker. For this test environment, you will install the Docker container runtime using the procedure described in the official documentation.

Start by installing the following packages on each node:

sudo apt install ca-certificates curl gnupg lsb-release

Add Docker’s official GPG key:

curl -fsSL | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

With the key installed, add the stable repository using the following command:

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Once the new repository is added, you’ll only have to update the <terminal inline>apt<terminal inline> index and install Docker:

sudo apt update && sudo apt install docker-ce docker-ce-cli -y

All that remains is to start and enable the Docker service. To do this, use the following commands:

sudo systemctl start docker && sudo systemctl enable docker 

Before proceeding to the next step, verify that Docker is working as expected.

sudo systemctl status docker

The output should look similar to the image below.

Docker service status
Docker service status

Configuring Cgroup Driver

For the <terminal inline>kubelet<terminal inline> process to work correctly, its cgroup driver needs to match the one used by Docker.

To do this, you can adjust the Docker configuration using the following command on each node:

cat <<EOF | sudo tee /etc/docker/daemon.json
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  "storage-driver": "overlay2"

For more details, see configuring a cgroup driver.

Once you’ve adjusted the configuration on each node, restart the Docker service and its corresponding daemon.

sudo systemctl daemon-reload && sudo systemctl restart docker

With Docker up and running, the next step is to install <terminal inline>kubeadm<terminal inline>, <terminal inline>kubelet<terminal inline>, and <terminal inline>kubectl<terminal inline> on each node.

Installing kubeadm, kubelet, and kubectl

Start by installing the following dependency required by Kubernetes on each node:

sudo apt install apt-transport-https

Download the Google Cloud public signing key:

sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg

Add the Kubernetes <terminal inline>apt<terminal inline> repository using the following command:

echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list

Update the <terminal inline>apt<terminal inline> package index and install <terminal inline>kubeadm<terminal inline>, <terminal inline>kubelet<terminal inline>, and <terminal inline>kubectl<terminal inline> on each node by running the following command:

sudo apt update \
&& sudo apt install -y kubelet kubeadm kubectl \
&& sudo apt-mark hold kubelet kubeadm kubectl

The last line with the <terminal inline>apt-mark hold<terminal inline> command is optional, but highly recommended. This will prevent these packages from being updated until you unhold them using the command:

sudo apt-mark unhold kubelet kubeadm kubectl

Blocking these packages ensures that all nodes will run the same version of <terminal inline>kubeadm<terminal inline>, <terminal inline>kubelet<terminal inline>, and <terminal inline>kubectl<terminal inline>. For more details about how to avoid updating a specific package, you can look at this Ask Ubuntu question.

In production environments, it’s common to deploy a specific version of Kubernetes that has already been tested instead of the most recent one. For instance, to install version 1.23.1, you can use the following command:

sudo apt install -y kubelet=1.23.1-00 kubectl=1.23.1-00 kubeadm=1.23.1-00

Regardless of the path you decide to take to install Kubernetes dependencies, you should see a message similar to the following when you finish installing the packages.

kubelet set on hold.
kubeadm set on hold.
kubectl set on hold.

This message indicates that your cluster is almost ready, and just needs to be initialized. Before proceeding with the initialization, though, double check that you’ve followed all the steps described so far on both nodes: install Docker container runtime, configure cgroup drivers, and install <terminal inline>kubeadm<terminal inline>, <terminal inline>kubelet<terminal inline>, and <terminal inline>kubectl<terminal inline>.

Initializing the Control-Plane Node

At this point, you have two nodes with <terminal inline>kubeadm<terminal inline>, <terminal inline>kubelet<terminal inline>, and <terminal inline>kubectl<terminal inline> installed. Now you initialize the Kubernetes control plane, which will manage the worker node and the pods running within the cluster.

Run the following command on the <terminal inline>primary<terminal inline> node to initialize your Kubernetes cluster:

sudo kubeadm init --apiserver-advertise-address= --apiserver-cert-extra-sans= --pod-network-cidr= --node-name primary

If you have followed the steps in the tutorial, you should see a message similar to the following:

kubeadm initialization success
kubeadm initialization success

Behind the scenes, <terminal inline>kubeadm init<terminal inline> has configured the control-plane node based on the specified flags. Below, we will discuss each of them.

  • <terminal inline bold>--apiserver-advertise-address<terminal inline bold>: This is the IP address that the Kubernetes API server will advertise it’s listening on. If not specified, the default network interface will be used. In this example, the public IP address of the <terminal inline>primary<terminal inline> node, <terminal inline><terminal inline>, is used.
  • <terminal inline bold>--apiserver-cert-extra-sans<terminal inline bold>: This flag is optional, and is used to provide additional Subject Alternative Names (SANs) for the TLS certificate used by the API server. It’s worth noting that the value of this string can be both IP addresses and DNS names.
  • <terminal inline bold>--pod-network-cidr<terminal inline bold>: This is one of the most important flags, since it indicates the range of IP addresses for the pod network. This allows the control-plane node to automatically assign CIDRs for each node. The range used in this example, <terminal inline><terminal inline> has to do with the Calico network plugin, which will be discussed further in a moment.
  • <terminal inline bold>--node-name<terminal inline bold>: As the name indicates, this is the name of this node.

Configuring kubectl

Kubectl is a command line tool for performing actions on your cluster. Before moving forward, you need to configure <terminal inline>kubectl<terminal inline>. To do this, run the following command from your control-plane node:

mkdir -p $HOME/.kube \
&& sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config \
&& sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, you can copy the <terminal inline>admin.conf<terminal inline> file created when you ran <terminal inline>kubeadm init<terminal inline> to <terminal inline>$HOME/.kube/config<terminal inline> on your local machine, which will allow you to use <terminal inline>kubectl<terminal inline> from it.

Installing Calico CNI

In most implementations, a container network interface (CNI) is used so the pods can accept traffic directly, which keeps network latency as low as possible.

Calico is used in this example because it’s one of the most feature-rich CNIs currently available. However, Kubernetes is compatible with other CNIs such as Flannel. Keep in mind that some CNIs require a specific <terminal inline>--pod-network-cidr<terminal inline> value. You can find more details about Kubernetes network plugins in the documentation.

To install Calico, use the following command:

kubectl apply -f

Before you can use the cluster, you have to wait for the pods required by Calico to be downloaded. To verify that the installation has been successful, run the following commands:

kubectl get nodes
kubectl get pods --all-namespaces

The output should be similar to the following:

Calico pods
Calico pods

Managing Components with kubeadm

As you have seen, <terminal inline>kubeadm init<terminal inline> allows you to bootstrap a Kubernetes control-plane node using a series of command line flags. However, <terminal inline>kubeadm<terminal inline> also allows you to use a configuration file for this purpose, and some features that allow <terminal inline>kubeadm<terminal inline> to manage Kubernetes components are only available as configuration file options.

You can also customize different Kubernetes components using the kubeadm API. To do this, you can run the <terminal inline>kubeadm init<terminal inline> command with the flag <terminal inline>--config <PATH TO CONFIG YAML><terminal inline>.

Alternatively, you can customize each kubelet by passing a directory with patch files to override the flags used during the deployment of the control-plane node. This can be useful in some use cases, such as nodes using a different distro or different network configuration.

To find more information about the options available in <terminal inline>kubeadm<terminal inline>, the best resource is the documentation. For more details on how to configure Kubernetes using <terminal inline>kubeadm<terminal inline>, you can look at the kubelet configuration documentation.

Setting Up the Worker Node

During the initialization of the control-plane node, the information necessary to join the worker node(s) was displayed. However, you can display it again using the following command from the <terminal inline>primary<terminal inline> node:

kubeadm token create --print-join-command

It should look similar to the following:

sudo kubeadm join --token 14dk9q.lhpx0l64bvpkvyi1 --discovery-token-ca-cert-hash sha256:8453b9ed77443de032bfa8ae77dabab20688523c3caebd0a5022831cacf0f0f2

You’ll need to copy the command, connect via SSH to your worker node, and execute the command from there. Once you run the <terminal inline>kubeadm join<terminal inline> command, you should see output similar to this:

Join worker
Join worker

A quick way to confirm that the node has correctly joined your cluster is to use the following command from your <terminal inline>primary<terminal inline> node or your local workstation:

kubectl get nodes

You should see something similar to:

Cluster nodes
Cluster nodes

Testing the Cluster by Running an Application

In order to make sure that your Kubernetes cluster is operating as expected, you can use a demo application.

Create a new namespace called yelb:

kubectl create ns yelb

Change the context to the newly created namespace:

kubectl config set-context --current --namespace=yelb

Deploy the application using the following command:

kubectl apply -f

Confirm that all pods are running before continuing:

kubectl get pods

Run the following command to verify that the yelb UI is running on the IP address of the worker node:

kubectl -n yelb describe pod $(kubectl -n yelb get pods | grep yelb-ui | awk '{print $1}') | grep "Node:"

The output should be similar to the following:

yelb pods
yelb pods

Now that the demo application is running, you can use your browser to interact with it. In this example, the address of the application is <terminal inline><terminal inline>. Remember to change the IP to that of your worker node.

yelb app
yelb app

Final Thoughts

This tutorial has shown you the step-by-step procedure for bootstrapping a Kubernetes cluster using the kubeadm command line tool, as well as the most common configuration and customization options.

Start your free 14-day ContainIQ trial
Start Free TrialBook a Demo
No card required
Damaso Sanoja

Damaso has been in the automotive/IT world since the age of 14, when his father decided to buy him a Commodore computer. Years later, his passion for electronics, computer science, and automotive mechanics motivated him to graduate in Mechanical Engineering from Universidad Metropolitana. For years, Damaso specialized in software engineering and networks. Today, Damaso is doing what he loves the most: writing engaging content for engineers and DevOps professionals looking to advance their careers