This post details a different way to experiment with a Kubernetes cluster that is a bit more advanced and powerful than utilizing minikube on your local workstation. The tutorial walks you through setting up a Kubernetes cluster on a suite of VMs hosted on your workstation using VirtualBox and Vagrant, with Ansible for configuration management of the cluster nodes (to transform them into their identities and form the cluster).

Jump Right In

If you’re wanting to simply pull the trigger on getting the cluster up and running, you can utilize this repo project which contains the configuration and instructions for ~3 commands that you can run to get to the finish line. The repo project is the complement to this tutorial where everything detailed here is automated as a one-click solution for those wanting to dive right in and start using a cluster.

Hardware Requirements

This tutorial lays the foundation for future posts that expand on the cluster being created and, as such, specs the nodes in the k8s cluster to be quite large. It is recommended you have a workstation with at least 32GB RAM and 8 CPU cores available to support both the cluster and existing operating system and other processes running. You can certainly attempt this tutorial with 16GB RAM, but if you elect to start all 3 nodes (master plus 2 worker nodes) with the defined specs in the Vagrantfile, you will likely have your workstation fans working quite hard once everything is up and running.

Software Requirements

Software used in this tutorial is summarized below - there is no need to obtain any of this up front, we will walk through each of these components throughout the tutorial:

  • Vagrant
  • VirtualBox
  • Ansible
  • Docker
  • Kubernetes

Components used for the resulting k8s cluster include:

  • calico (the linked repository project also has the ability to use flannel, which is commented out in the ansible scripts)

Prerequisites

It is assumed that you already have a working environment with Vagrant and VirtualBox at the ready. If not, follow the instructions on the vendor sites to get the components installed and working together.

In addition, once you have a functioning Vagrant + VirtualBox environment, you’ll need to enable a plugin for Vagrant to automatically install Guest Additions in VMs being provisioned that do not have it pre-installed. This is required in order to enable local directory mapping which is useful for transferring files, etc. and is the basis for the automation repository if you get to the end of this tutorial and want to use that functionality:

vagrant plugin install vagrant-vbguest

Next, you’ll want to use a box that already has guest additions installed if at all possible. The above sets the environment up in case you discover/use boxes that don’t already have guest additions installed, but ideally you’ll want these pre-installed as the provisioning process takes a few minutes (longer than reasonable). If you decide not to run the following commands, you’ll want to edit the Vagrantfile to specify the correct box to use as the existing Vagrantfile in this repo declares the box to be used in this tutorial (note that you’ll also almost certainly want to find a most recent/up to date build of the Vagrant box you wish to use from a trusted source):

$ vagrant box add centos7-with-guest https://github.com/vezzoni/vagrant-vboxes/releases/download/0.0.1/centos-7-x86_64.box

This is essentially the only local software configuration you will need - the rest will be taken care of in this tutorial.

Creating the VMs

The cluster in this tutorial will have 3 nodes total - one master, and two worker nodes. We will first create the VMs (all 3) to get them ready for installing, configuring, and joining the k8s cluster. Create a Vagrantfile that defines the spec for your 3 VMs, each having 4GB RAM and 2x CPUs minimum, sharing a private network IP space. A good reference to use would be the Vagrantfile specified in the automation project here, but ensure you remove the provision specs for each of the nodes as we will be crafting things by hand in this tutorial.

vagrant up

Once all 3 of your nodes have started, move along to configuration.

Common/Foundational Configuration

NOTE: All commands should be run as the root user unless otherwise specified, so become the root user on each of the nodes and remain root for the duration of this section:

sudo su -

There is a common set of components and configurations that exist on all nodes, regardless of role. We’ll start here, meaning you’ll do the following on each (all 3) of the nodes.

First, we’ll install some useful packages for debugging and working on the nodes. Note that some items in this list of packages to install are optional and you can add items to suit your needs/what you expect to need on each node when troubleshooting.

yum -y install vim \
               device-mapper-persistent-data \
               lvm2 \
               net-tools \
               nc \
               yum-utils

Next we’ll enable IPv4 forwarding:

echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p

Swap is not useful so we’ll disable it:

swapoff -a

vi /etc/fstab
# comment out or remove the line containing
# the swap specification

Unfortunately, SELinux is also not supported, so we’ll need to disable it as well:

setenforce 0

vi /etc/selinux/config
# ensure the following is set:
#   SELINUX=disabled

A useful item to enable intra-cluster name resolution is to create an /etc/hosts file on each of the hosts in the cluster with static IP addresses for each other host. Edit the /etc/hosts file and add the following to the bottom of each file, leaving out whichever hostname of the host you’re already on (given that the loopback adapter will handle host communication with itself). Note that the IPs specified here correspond to the IP addresses in the sample Vagrantfile linked earlier, so if you’re using a different Vagrantfile or IP address scheme, update this section appropriately:

vi /etc/hosts
# append the following to the bottom, leaving out the
# current host/ip of the host you're currently on
#   master 10.11.12.13
#   node1  10.11.12.14
#   node2  10.11.12.15

Now that the OS-level changes are complete, we’ll move into the Docker Engine installation and configuration. Add the Docker repository and then perform the package installation required for the various components (the instructions for this are taken straight from the Docker site here:

yum install docker-ce \
            docker-ce-cli \
            containerd.io

Next, in order to interact with the Docker engine, you’ll need to add your user to the docker group. We’ll do this for both the root user (which you currently are) as well as the vagrant user:

usermod -aG docker root
usermod -aG docker vagrant

Once you’ve completed adding the users to the group, you’ll need to either log out/log back in again, or use the newgrp docker command to reset your terminal to realize the new group configurations.

Start the docker engine and enable it to start on boot of the VM:

systemctl start docker
systemctl enable docker

You should then be able to run the docker info command to see details about the Docker environment, confirming that your install is working as expected.

We’re getting closer - next step is to install and configure common components for k8s across all nodes. For this, we’ll be installing 3 core packages on each node related to running the k8s cluster, and starting/enabling the kubelet process which is the primary node agent that handles registering nodes, etc.:

# configure the k8s repo
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kubelet kubeadm kubectl
EOF

# install the required components
yum install kubelet \
            kubeadm \
            kubectl \
            --disableexcludes=kubernetes

# start and enable the kubelet service
systemctl enable --now kubelet

Finally, there are bridge filtering system kernel parameters that need to be set and only show up once Docker and the core k8s components have been installed and started. These kernel settings enable application of IPTables within scope of bridge interfaces and are required for k8s to function as expected:

echo "net.bridge.bridge-nf-call-iptables = 1" >> /etc/sysctl.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.conf
sysctl --system

As a note, if the above fails, the br_netfilter module may not be loaded. Load this module via modprobe br_netfilter and then re-run the sysctl --system command to re-parse the sysctl.conf settings with the module available.

This largely concludes the common k8s installation requirements. We’ll now move on to the master-specific configs.

k8s Master Configuration

NOTE: All commands should be run as the root user and on the master node unless otherwise specified, so become the root user on the master node for the duration of this section:

sudo su -

The master is the control node for the cluster. Getting the k8s cluster initialized and installing/configuring the pod network add-on are critical to having a successful cluster join and auto-configuration of the worker nodes. Joining of worker nodes is done using a token, and we’ll generate the token (and capture the token hash, mentioned later) to be used for joining nodes:

kubeadm token generate

Keep the token accessible - we’ll refer to the value of the token using <TOKEN> in future sections.

Next, we’ll initialize the cluster using the token and specify our pod network. In this case, we’re using a 10.x CIDR range/private IP range for our pod network, and we’re using the master IP as specified in the example Vagrantfile mentioned earlier, with the <TOKEN> access token generated just before this step.

Note: It’s not generally adviseable to have the k8s nodes in the same network range as the pods and services you wish to manage. However, in order to avoid some very complicated network routing configuration, we’ll keep everything in the 10.x IP space to ensure routing and paths are easily available for the node to pod communication.

kubeadm init --pod-network-cidr=10.12.0.0/24 \
             --apiserver-advertise-address=10.11.12.13 \
             --apiserver-cert-extra-sans=k8s.cluster.home \
             --token=<TOKEN>

The above step will take a minute or two while it initializes the cluster and readies the master. Once complete, we can configure the pod network, but in order to do so, we need to be able to interact with the k8s master node, which requires the credentials and configuration to do so. During the init, there was a configuration file created which contains credentials for interacting with the k8s master as an administrator. While this is generally dangerous and least-priv access should be configured, the admin access is the starting point for configuring add-ons, so we will configure our root user to utilize these credentials:

mkdir /root/.kube
cp /etc/kubernetes/admin.conf /root/.kube/

Once you’ve copied the admin credentials to the respective directory, you can then query the master node to prove the creds are working the way you expect. Run kubectl get nodes and you should see your master node showing up with a status of NotReady (because we haven’t yet added a pod network add-on, which we’re about to do).

Now that we can communicate with the master, we’ll install the pod network add-on. in this case, we’ll use the calico network add-on as it also affords network rules for traffic management (you can use something simpler such as flannel, but flannel does not have the ability to control network traffic rules). Installation of calico is straightforward, with one catch where we want to configure the pod network of the default configuration to be the CIDR range of the pod network that we configured using kubeadm above:

# obtain the spec for the calico configuration
wget https://docs.projectcalico.org/v3.11/manifests/calico.yaml

# edit the calico spec
vi calico.yaml
# search for the key/value pair having name 'CALICO_IPV4POOL_CIDR'
# or search for "192.", and replace the "192.168.0.0/16" default
# with the pod cidr range we specified earlier: 10.12.0.0/24

# apply calico resources
kubectl apply -f calico.yaml

It will take several minutes for the calico network add-on to configure the network on the master. Check the status of the master node routinely using kubectl get nodes until you see Ready showing in the status column for the master node, indicating your pod network and the resulting master node are ready to move forward.

Prior to moving to the worker nodes, there are 2 things remaining. First, it would be best if you copied the /etc/kubernetes/admin.conf configuration file to the shared/mapped drive on the master so that you can access it from the worker nodes easily for performing tasks and interacting with the cluster. Second, we’ll create a token CA cert hash that will be used in addition to the <TOKEN> parameter to enable joining worker nodes. On the master, discover the token and keep it available (we’ll refer to this parameter in the future with <CERT_HASH>):

openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | \
    openssl rsa -pubin -outform der 2>/dev/null | \
    openssl dgst -sha256 -hex | \
    sed 's/^.* //'"

Again, copy the value printed above - we’ll refer to this as <CERT_HASH> in the next section.

k8s Worker Configuration

NOTE: All commands should be run as the root user and on the worker nodes (node1 and node2) unless otherwise specified, so become the root user on the worker nodes for the duration of this section:

sudo su -

Now that our master node is up and available/ready to accept worker join requests, we’ll move over to the worker nodes, node1 and node2. On each of the nodes, first enable communication with the cluster by installing the admin configuration for the kubectl command to function/interact with the cluster:

mkdir /root/.kube
cp <admin.conf_from_shared_folder> /root/.kube

Next, we’ll run the join command, using the token and hash we captured previously on the master node, including the master node IP address 10.11.12.13 as specified in the sample Vagrantfile:

kubeadm join --token=<TOKEN> \
             10.11.12.13:6443 \
             --discovery-token-ca-cert-hash=sha256:<CERT_HASH>

Once the command is run, you can monitor the status of the node join by watching the kubectl get nodes command output. You should quickly see the node1 and node2 nodes join the cluster and show a status of NotReady for several minutes while the calico network components are configured/extended to the worker nodes. Once the initialization is completed after several minutes, you will see all 3 nodes showing Ready, indicating your cluster is now available for use!

Next Steps

Stay tuned for some potential follow-ups where we’ll explore adding add-ons to the cluster and eventually building up to configuring an Istio service mesh on top of the cluster for more advanced traffic and GitOps-like flows with a security-first approach!

Credit

The above tutorial was pieced together with some information from the following sites/resources: