How To Create a Kubernetes Cluster Using Kubeadm on Ubuntu 18.04

Photo of author

By admin

Kubernetes is a container orchestration system. It helps in managing the containers at scale. Alongside, it automates the installation and configuration of Kubernetes components. Kubernetes components include Controller Manager, API server, and Kube DNS. This post will guide you regarding setting up a Kubernetes cluster from the very beginning with the use of Ansible and Kubeadm. Afterward, we will be proceeding with deploying a containerized Nginx application to it.

Prerequisites for Creating Kubernetes Cluster Using Kubeadm

Before you proceed with this post, there are certain prerequisites that you need to consider:

  • An SSH key pair on your local Linux/macOS/BSD machine.
  • Three servers that run Ubuntu 18.04 having at least 2GB RAM and 2 vCPUs each. You should be able to SSH into each server and that too as the root user using your SSH key pair.
  • Ansible installed on your local machine.
  • You must be familiar with Ansible playbooks.
  • You must have an idea regarding the process of launching a container from a rocker image.

Steps for Creating Kubernetes Cluster on Ubuntu 18.04 Using Kubeadm

You can create a Kubernetes cluster on your Ubuntu system with ease by following the steps mentioned as follows:

Step 1: Set up the Workspace Directory and Ansible Inventory File

This step requires you to create a directory on the local machine that primarily serves as the workspace. To facilitate communication and execute demand in your remote servers, you would be required to configure Ansible locally. Afterward, you will be creating a hosts file that will be containing the inventory information like the IP addresses of your servers as well as the groups that each server belongs to.

There will be three servers, out of which one will be the master with an IP displayed as master_ip. The other two will be workers having the IPs worker_1_ip and worker_2_ip.

Then proceed towards creating a directory that will be named ~/kube-cluster in the home directory of your local machine. Then you need to navigate into this directory. Here are the commands that you need to run on the terminal:

$ mkdir ~/kube-cluster

$ cd ~/kube-cluster

This directory will serve as your workspace for this tutorial. It will also contain all the Ansible playbooks. All the local commands will be run inside this directory only.

Now, create a new file named ~/kube-cluster/hosts with the use of nano or your favorite text editor:

$ nano ~/kube-cluster/hosts

Now, you will be required to add the below-mentioned text to the file. This will aid in specifying the logical structure of the cluster.:

~/kube-cluster/hosts

[masters]

master ansible_host=master_ip ansible_user=root

[workers]

worker1 ansible_host=worker_1_ip ansible_user=root

worker2 ansible_host=worker_2_ip ansible_user=root

[all:vars]

ansible_python_interpreter=/usr/bin/python3

You must remember that inventory files in Ansible are used in specifying server information. This server information comprises IP addresses, remote users, and groupings of servers. All of these are targeted as a single unit for executing commands. Also, ~/kube-cluster/hosts act as your inventory file to which you’ve added two Ansible groups (masters and workers).

These aid in specifying your cluster’s logical structure.

Talking about the master’s group, there is a server entry that is named “master”. It helps in listing the master node’s IP (master_ip) along with specifying that Ansible should run remote commands as the root user.

Talking about the workers group, there are two separate entries for the worker servers as well (worker_1_ip and worker_2_ip). They also aid in specifying the ansible_user as root.

The bottom-most line of the file informs Ansible to utilize the remote servers’ Python 3 interpreters to facilitate its management operations.

After adding the text, simply Save and Close the file.

Once you set up the server inventory with groups, you can now install operating system level dependencies along with creating configuration settings.

Step 2: Creating a Non-Root User on All Remote Servers

In this step, you are required to create a non-root user with sudo privileges on all the servers. This allows you to SSH into them manually. You do this as an unprivileged user. During the maintenance of a cluster, these operations are performed routinely with the use of a non-root user. It aids in minimizing the risks that are present. These risks include the modification or deletion of important files. It also reduces the chances of unintentionally performing other dangerous operations.

Create a file named ~/kube-cluster/initial.yml in the workspace:

$ nano ~/kube-cluster/initial.yml

Then proceed towards adding the below-mentioned play to the file. It will create a non-root user with sudo privileges on all of the servers. A play in Ansible is a collection of steps that are required to be performed. It targets specific groups along with servers. The below-mentioned play will help in creating a non-root sudo user:

~/kube-cluster/initial.yml

– hosts: all

become: yes

tasks:

– name: create the ‘ubuntu’ user

user: name=ubuntu append=yes state=present createhome=yes shell=/bin/bash

– name: allow ‘ubuntu’ to have passwordless sudo

lineinfile:

dest: /etc/sudoers

line: ‘ubuntu ALL=(ALL) NOPASSWD: ALL’

validate: ‘visudo -cf %s’

– name: set up authorized keys for the ubuntu user

authorized_key: user=ubuntu key=”{{item}}”

with_file:

– ~/.ssh/id_rsa.pub

Now let’s have a look at the stuff which this playbook performs:

  • It aids in creating a non-root user in Ubuntu.
  • It helps in configuring the sudoers file. It further allows the Ubuntu user to run sudo commands, and that too without requiring a password prompt.
  • It adds the public key to your local machine (usually ~/.ssh/id_rsa.pub) to the authorized key list of remote ubuntu users. It further allows you to SSH into each server as an Ubuntu user.

Afterward, Save and close the file once you’ve added the text.

Next, you will be required to execute the playbook. You will do this by running it locally:

$ ansible-playbook -i hosts

~/kube-cluster/initial.yml

The command will be completed within two to five minutes. Once it gets completed, you will see an output that will resemble the following:

PLAY [all] ****

TASK [Gathering Facts] ****

ok: [master]

ok: [worker1]

ok: [worker2]

TASK [create the ‘ubuntu’ user] ****

changed: [master]

changed: [worker1]

changed: [worker2]

TASK [allow ‘ubuntu’ user to have passwordless sudo] ****

changed: [master]

changed: [worker1]

changed: [worker2]

TASK [set up authorized keys for the ubuntu user] ****

changed: [worker1] => (item=ssh-rsa AAAAB3…)

changed: [worker2] => (item=ssh-rsa AAAAB3…)

changed: [master] => (item=ssh-rsa AAAAB3…)

PLAY RECAP ****

master : ok=5 changed=4 unreachable=0 failed=0

worker1 : ok=5 changed=4 unreachable=0 failed=0

worker2 : ok=5 changed=4 unreachable=0 failed=0

The above-mentioned steps will complete the preliminary setup. Now, you can move towards installing the Kubernetes-specific dependencies.

Step 3: Installing Kubernetetes’ Dependencies

Following this step, you will be installing the operating-system-level packages that Kubernetes will require with Ubuntu’s package manager. These packages are:

Docker – It is a container runtime. Docker is the component that facilitates the running of the containers. When it comes to support for other runtimes such as rkt, it is under active development in Kubernetes.

kubeadm – It is a CLI tool. It allows the installation and configuration of the various components of a cluster. It does so in a standard way.

kubelet – It is a system service/program. It runs on all nodes along with handling node-level operations.

kubectl – It is a CLI tool. It facilitates issuing commands to the cluster with its API Server.

Create a file named ~/kube-cluster/kube-dependencies.yml in the workspace:

$ nano ~/kube-cluster/kube-dependencies.yml

Then, proceed to add the following plays to the file; this will aid in installing these packages to your servers:

~/kube-cluster/kube-dependencies.yml

– hosts: all

become: yes

tasks:

– name: install Docker

apt:

name: docker.io

state: present

update_cache: true

– name: install APT Transport HTTPS

apt:

name: apt-transport-https

state: present

– name: add Kubernetes apt-key

apt_key:

url: https://packages.cloud.google.com/apt/doc/apt-key.gpg

state: present

– name: add Kubernetes’ APT repository

apt_repository:

repo: deb http://apt.kubernetes.io/ kubernetes-xenial main

state: present

filename: ‘kubernetes’

– name: install kubelet

apt:

name: kubelet=1.14.0-00

state: present

update_cache: true

– name: install kubeadm

apt:

name: kubeadm=1.14.0-00

state: present

– hosts: master

become: yes

tasks:

– name: install kubectl

apt:

name: kubectl=1.14.0-00

state: present

force: yes

The first play in the playbook performs the following:

  • It helps in installing Docker which acts as the container runtime.
  • It also facilitates the installation of apt-transport-https, which allows you to add external HTTPS sources to the APT sources list.
  • It facilitates adding the Kubernetes APT repository’s apt-key that is required for key verification.
  • It also adds the Kubernetes APT repository to your remote servers’ APT sources list.
  • It facilitates the installation of both kubelet and kubeadm.

The second play primarily consists of a single task. It helps in installing kubectl on your master node.

Finally, Save and close the file when you are done.

Afterward, execute the playbook by running it locally:

$ ansible-playbook -i hosts ~/kube-cluster/kube-dependencies.yml

On completion, you will see an output that will resemble the following:

PLAY [all] ****

TASK [Gathering Facts] ****

ok: [worker1]

ok: [worker2]

ok: [master]

TASK [install Docker] ****

changed: [master]

changed: [worker1]

changed: [worker2]

TASK [install APT Transport HTTPS] *****

ok: [master]

ok: [worker1]

changed: [worker2]

TASK [add Kubernetes apt-key] *****

changed: [master]

changed: [worker1]

changed: [worker2]

TASK [add Kubernetes’ APT repository] *****

changed: [master]

changed: [worker1]

changed: [worker2]

TASK [install kubelet] *****

changed: [master]

changed: [worker1]

changed: [worker2]

TASK [install kubeadm] *****

changed: [master]

changed: [worker1]

changed: [worker2]

PLAY [master] *****

TASK [Gathering Facts] *****

ok: [master]

TASK [install kubectl] ******

ok: [master]

PLAY RECAP ****

master : ok=9 changed=5 unreachable=0 failed=0

worker1 : ok=7 changed=5 unreachable=0 failed=0

worker2 : ok=7 changed=5 unreachable=0 failed=0

Once the execution is completed, Docker, kubeadm, and kubelet will be installed on all of the remote servers. But, kubectl won’t be as it is needed only to execute cluster commands. Hence, installing it only on the master node is quite relevant. This is because you will run kubectl commands only from the master. However, it must be noted that kubectl commands can be run from any of the worker nodes or any machine. Those machines and nodes must be able to install and configure it to point to a cluster.

All system dependencies will be installed by now. Now you can proceed towards setting up the master node along with initializing the cluster.

Step 4: Setting Up the Master Node

Following this step, you will be setting up the master node. Before you create any playbooks, it is quite imperative to cover some concepts like Pods and Pod Network Plugins. This is because your cluster will include both of them.

A pod refers to an atomic unit that runs one or more containers. These containers help in sharing resources, including file volumes and network interfaces that are in common. Pods act as the basic unit of scheduling in Kubernetes: all containers in a pod run on the exact same node to which the pod is scheduled.

Each pod has its IP address. A pod present on one node should be able to access a pod on another node with the use of a pod’s IP. Containers on a single node can communicate easily through a local interface. However, communication between the pods is more complicated. This kind of communication requires a separate networking component. It must be capable of transparently route traffic from a pod on one node to a pod on another.

Pod network plugins provide this functionality. For this cluster, you will be using Flannel. It serves as a stable and performant option.

Create an Ansible playbook named master.yml on your local machine:

$ nano ~/kube-cluster/master.yml

Then, add the following play to the file to initialize the cluster and install Flannel:

~/kube-cluster/master.yml

– hosts: master

become: yes

tasks:

– name: initialize the cluster

shell: kubeadm init –pod-network-cidr=10.244.0.0/16 >> cluster_initialized.txt

args:

chdir: $HOME

creates: cluster_initialized.txt

– name: create .kube directory

become: yes

become_user: ubuntu

file:

path: $HOME/.kube

state: directory

mode: 0755

– name: copy admin.conf to user’s kube config

copy:

src: /etc/kubernetes/admin.conf

dest: /home/ubuntu/.kube/config

remote_src: yes

owner: ubuntu

– name: install Pod network

become: yes

become_user: ubuntu

shell: kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/a70459be0084506e4ec919aa1c114638878db11b/Documentation/kube-flannel.yml >> pod_network_setup.txt

args:

chdir: $HOME

creates: pod_network_setup.txt

Here we present before you a breakdown of this play:

The first task facilitates the initialization of the cluster by running kubeadm init. Passing the argument –pod-network-cidr=10.244.0.0/16 aids in specifying the private subnet that the pod IPs are assigned from. Flannel makes use of the above subnet by default; here we are telling kubeadm to use the exact same subnet.

The second task helps in creating a .kube directory at /home/ubuntu. This directory holds configuration information including the admin key files. These are required to connect to the cluster, and the cluster’s API address.

The third task allows copying the /etc/kubernetes/admin.conf file that was earlier generated from kubeadm init to your non-root user’s home directory. This further allows you to use kubectl which aids in accessing the newly-created cluster.

The last task that runs is kubectl apply, which intends to install Flannel.

kubectl apply -f descriptor.[yml|json] is the syntax that tells kubectl to create the objects that are described in the descriptor.[yml|json] file. The kube-flannel.yml file contains the descriptions of objects that are required for setting up Flannel in the cluster.

Once you are finished with these, Save and Close the file.

Now, you will be executing the playbook locally by using the command:

$ ansible-playbook -i hosts ~/kube-cluster/master.yml

On completion, you will see an output resembling the following:

PLAY [master] ****

TASK [Gathering Facts] ****

ok: [master]

TASK [initialize the cluster] ****

changed: [master]

TASK [create .kube directory] ****

changed: [master]

TASK [copy admin.conf to user’s kube config] *****

changed: [master]

TASK [install Pod network] *****

changed: [master]

PLAY RECAP ****

master : ok=5 changed=4 unreachable=0 failed=0

In order to check the status of the master node, just SSH into it using the following command:

$ ssh ubuntu@master_ip

Once you are inside the master node, execute the following:

$ kubectl get nodes

You will now see the following output:

NAME STATUS ROLES AGE VERSION

master Ready master 1d v1.14.0

The output states that all initialization tasks have been completed by the master node. It is now in a Ready state. From there onwards, it can start accepting worker nodes along with executing tasks that are sent to the API Server. Now, you can proceed with adding the workers from your local machine.

Step 5: Setting Up the Worker Nodes

This step involves adding workers to the cluster, which aids in executing a single command on each. It comprises the necessary cluster information, including the IP address and port of the master’s API Server and a secure token. Only the nodes that pass in the secure token are capable of joining the cluster.

Now, navigate back to your workspace. Create a playbook named workers.yml:

$ nano ~/kube-cluster/workers.yml

Then add the following text to the file. This will add the workers to the cluster:

~/kube-cluster/workers.yml

– hosts: master

become: yes

gather_facts: false

tasks:

– name: get join command

shell: kubeadm token create –print-join-command

register: join_command_raw

– name: set join command

set_fact:

join_command: “{{ join_command_raw.stdout_lines[0] }}”

– hosts: workers

become: yes

tasks:

– name: join cluster

shell: “{{ hostvars[‘master’].join_command }} >> node_joined.txt”

args:

chdir: $HOME

creates: node_joined.txt

The playbook does the following:

  • The first play receives the join command. It needs to be run on the worker nodes. This command will be given in the following format:

kubeadm join –token <token> <master-ip>:<master-port> –discovery-token-ca-cert-hash sha256:<hash>.

Once it gets the actual command with the proper token and hash values, the task will set it as a fact. It will aid the next play to be able to access that info.

  • The second play comprises a single task that runs the join command on all the worker nodes. Once this task gets completed, the two worker nodes will become a part of the cluster.

When you are finished, simply Save and Close the file.

Now, you can execute the playbook by using the following command:

$ ansible-playbook -i hosts ~/kube-cluster/workers.yml

Once it gets completed, you will see an output resembling the following:

PLAY [master] ****

TASK [get join command] ****

changed: [master]

TASK [set join command] *****

ok: [master]

PLAY [workers] *****

TASK [Gathering Facts] *****

ok: [worker1]

ok: [worker2]

TASK [join cluster] *****

changed: [worker1]

changed: [worker2]

PLAY RECAP *****

master : ok=2 changed=1 unreachable=0 failed=0

worker1 : ok=2 changed=1 unreachable=0 failed=0

worker2 : ok=2 changed=1 unreachable=0 failed=0

After adding the worker nodes, your cluster is now completely set up and functional. Your workers are now ready to run workloads. Before you start scheduling the applications, you must verify that the cluster is working in an intended manner.

Step 6: Verifying the Cluster

At times a cluster may fail while setting up. This happens either because a node is down or network connectivity between the master and worker is not working perfectly. To verify the cluster and make sure that the nodes are operating correctly, the below-mentioned protocol must be followed.

You will be checking the present state of the cluster from the master node. This will ensure that the nodes are ready. If you are disconnected from the master node, you can simply SSH back into it by using the below-mentioned command:

$ ssh ubuntu@master_ip

Then you are required to execute the following command to receive the status of the cluster:

$ kubectl get nodes

You will see an output resembling the following:

NAME STATUS ROLES AGE VERSION

master Ready master 1d v1.14.0

worker1 Ready <none> 1d v1.14.0

worker2 Ready <none> 1d v1.14.0

If all of your nodes show the value of Ready for STATUS, it means that they’re part of the cluster and are ready to run workloads.

If a few of the nodes have NotReady as the STATUS, it could mean that the worker nodes haven’t finished their setup till now. You must wait for around five to ten minutes before re-running kubectl get nodes, and inspect the new output in such a scenario. If a few nodes still have NotReady as their status, you might have to verify and re-run the commands that are mentioned in the previous steps.

Now that your cluster is verified successfully, we can proceed towards scheduling an example Nginx application on the cluster.

Step 7: Running an Application on the Cluster

In this step, you can deploy any containerized application to your cluster. Let’s start with deploying Nginx using Deployments and Services to see how this application can be deployed to the cluster. The commands given below can be used for other containerized applications too. All you need to do is to change the Docker image name and any relevant flags (such as volumes and ports).

Still, within the master node, you must execute the following command to create a deployment that will be named Nginx:

$ kubectl create deployment nginx –image=nginx

A deployment is basically a type of Kubernetes object that makes sure that there is a specified number of pods that run on a defined template, even if the pod crashes during the lifetime of a cluster. The above deployment aids in creating a pod that has one container from the Docker registry’s Nginx Docker Image.

Afterward, you are required to run the following command, which will create a service named Nginx. It will lead to exposing the app publicly. It will do so through a NodePort. It is basically a scheme that will make the pod accessible through an arbitrary port that is opened on each node of the cluster:

$ kubectl expose deploy nginx –port 80 –target-port 80 –type NodePort

Services are just another type of Kubernetes object. They expose cluster internal services to both internal and external clients. They are even capable of load balancing requests to multiple pods and are an integral component in Kubernetes. They frequently interact with other components too.

Run the following command:

$ kubectl get services

This will output text resembling the following:

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE

kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 1d

nginx NodePort 10.109.228.209 <none> 80:nginx_port/TCP 40m

From the third line of the above output, you can have an idea about the port that Nginx is running on. Kubernetes will automatically assign a random port to it that will be greater than 30000. It will also ensure that the port is not bound by any other service.

To verify that everything is working, you can visit

http://worker_1_ip:nginx_port or http://worker_2_ip:nginx_port through a browser on your local machine. There you will see Nginx’s welcome page.

If you would like to remove the Nginx application, start with deleting the Nginx service from the master node using the command shown below:

$ kubectl delete service nginx

You must run the following to verify that the service has been deleted:

$ kubectl get services

You will see an output resembling the following:

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE

kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 1d

Then, you need to delete the deployment with this command:

$ kubectl delete deployment nginx

Finally, run the following to confirm that this has worked:

$ kubectl get deployments

Output

No resources found.

Final Words

With this guide, you can successfully set up a Kubernetes cluster on your Ubuntu 18.04 system. Also, you would be using Kubeadm and Ansible for automation. Once the cluster is set up, you can proceed with deploying your applications and services into the cluster.

Leave a Comment