18

Container Orchestration

In the previous chapter, we started learning concepts around containerization. We’ve learned what containers are and how they differ from Virtual Machines (VMs), as well as how to run two different types of containers (Docker and LXD). As you are now aware, containers are typically lightweight (which means you run a larger number of them than VMs on the same hardware) and are easy to manage with a command syntax that’s rather logical, such as docker run myapp to launch a container named myapp. Depending on the size of your organization, you may only need to run one or two containers to suit your needs, or perhaps you plan on scaling up to hundreds of them. While it’s rather simple to maintain a small number of containers, the footprint can quickly expand and become much harder to keep track of.

In this chapter, we’ll start looking into the concept of Container Orchestration, which can help us to better maintain the containers that we run. Without such orchestration, the onus is on you, the administrator, to ensure your containers are running properly. While someone still needs to be responsible for mission-critical containers, orchestration provides us with tools we can use to manage them much more efficiently. In addition, orchestration allows us to create an entire cluster that’s easily scalable, so we’re better equipped to handle the demand of our users or clients. As we navigate this topic, we will work on:

  • Container orchestration
  • Preparing a lab environment for Kubernetes testing
  • Utilizing MicroK8s
  • Setting up a Kubernetes cluster
  • Deploying containers via Kubernetes

Excited? I know I am—containerization can be a lot of fun to learn and work with. We’ll even create our very own cluster in this chapter! But before we can do that, let’s make sure we have an understanding of what container orchestration is.

Container orchestration

In the last chapter, we covered the basics of running containers on your server. Of special note is the coverage of Docker, which will play a very important role in this chapter. We saw how to pull a Docker image as well as how to use such an image to create a container. There are many more advanced concepts we can learn when it comes to Docker, but understanding the essentials is good enough for the scope of this chapter. And now that we know how to run containers, looking further into how to more efficiently manage them is a logical next step.

Traditionally, as an administrator, you’ll ensure the critical apps and services for your organization are always healthy and available. If a critical resource stops working for any reason, it falls on you to return it to a healthy state. Regardless of whether we’re utilizing applications on a physical server, or in a VM or container, this need doesn’t change—production apps need to be available at all times with minimal or no downtime. Specific to containers, what orchestration does for us is help us maintain them much more efficiently. Orchestration allows us to not only manage our containers in one place, but it also provides us with additional tooling we can use to more intelligently handle load and recover from faults.

Consider this example: let’s assume you work at an organization that has an important website that needs to be available online at all times, and it’s currently set up inside a VM. As the administrator, you test the application by running it inside a Docker container and discover that it not only functions the same as in the VM, it also requires less of the server’s memory to run it in a container, and it’s even more responsive than before. Everyone at your company is impressed, and the project of converting your company’s site to run inside a container is a complete success.

Now, let’s assume your organization is getting ready to release a brand new version of your company’s product. It’s expected that the demand for your website will increase ten times for at least the first few weeks after the debut. To address this, you can launch however many additional containers you feel will handle the expected load, and then set up a load balancer to spread traffic evenly between them. When the excitement over the new release winds down, you can remove the newly added containers to return to your original population. We haven’t gone over load balancers yet, but these are useful for spreading traffic between multiple nodes. This feature is built into Kubernetes, so you won’t need to install anything extra to take advantage of this.

If the launch of your organization’s newly updated product is expected to go live at 1:00am, you’d have to make sure you’re awake then and execute the necessary commands to launch the extra containers and deploy the load balancer. You would then watch the containers for a while and ensure they’re stable. Perhaps you’ve already tested this in a testing environment, so you have reasonable confidence that the maintenance will go smoothly. After you successfully launch the containers, you set a reminder for yourself in your calendar to log back in after two weeks to undo the changes.

While that approach can technically work and result in a successful rollout, it’s not very efficient. Sure, you can log in at 1:00am to launch the extra containers and deploy the load balancer, but is that an effective use of your time? What if you’re very sleepy at that time and make a mistake that results in a failure during an important launch? And when the launch window is over and the load returns to normal, you may or may not see the alert that you’ve intended to use to remind yourself and lower the container count.

In that situation, you could end up with an expensive bill since your server would be using extra energy to run containers that were no longer needed. Even worse, manually managing your containers means that it can’t handle a situation where the load increases unexpectedly. Despite our best intentions, any process that is run manually may or may not work out well; we’re only human after all.

With container orchestration, we essentially delegate the process of running containers to an application that will automatically create additional containers as demand increases and remove unneeded containers when demand winds down. Orchestration also simplifies the process of setting up applications, by giving us tools we can use to ensure a particular number of containers are running at a minimum and we can set a maximum for when load and demand spikes. This empowers us to have infrastructure that grows and shrinks automatically to match the needs of our users. In addition, container orchestration allows us to automatically heal from failures. If a container runs into a problem and fails for some reason, it will be deleted and recreated from the image.

As you can see, orchestration gives us additional value, it can actually save you time and effort. To be fair, some of the features mentioned are part of containerization itself and not necessarily specific to orchestration, but the latter does give us the ability to manage these tools much more efficiently. Is container orchestration for everyone? No technology meets 100% of all use-cases, but it’s certainly something to consider. If you only run a single container and have no plans to run another, then a technology such as orchestration is overkill—it would take you more time to manage the cluster than it would to manage the container itself. Use your best judgment.

If you do decide to implement containerization, Kubernetes (often abbreviated K8s) is a very popular solution for container orchestration and is what we’ll be exploring in this chapter. What Kubernetes allows us to do is create deployments for our applications, providing us with fine-tuned control we can leverage to determine the actual behavior of our applications. It can automatically re-spawn a container if it stops working for any reason, ensure the number of containers running meets the demand, and spread our workloads across many worker nodes so that no one server becomes overwhelmed with running too many containers. Kubernetes is very popular and is not specific to Ubuntu Server. However, utilizing a technology such as Kubernetes in Ubuntu is one of many ways that we can use to extend the platform.

What do you need in order to set up a Kubernetes cluster? That’s likely a logical question to have at this point. In the next section, we’ll cover some of the considerations to make before deciding how to set it up.

Preparing a lab environment for Kubernetes testing

In an organization, planning a roll-out of an entire Kubernetes cluster can be fairly involved—you may have to purchase hardware and also analyze your existing environment and understand how containerization will fit in. It’s possible that some applications you want to run aren’t a good fit for containers; some don’t support running in a container at all. Assuming you’ve already checked the documentation for the applications you are wanting to run in containers and came to the conclusion that such a technology is supported, the next step is procuring the hardware (if you don’t already have a place to run it) and then setting up the cluster.

Specific to us in this book, we don’t need to contact a server vendor and submit a purchase order to simply test out the technology. If you are actually involved with the rollout of container orchestration at your organization, then it’s a fun project to work on. But for the purposes of this book, let’s discuss some of the details around what’s needed to set up a personal lab in order to set up Kubernetes and start learning the platform.

The best rule of thumb when setting up a lab for testing software is to try to use what you have available. To create a Kubernetes cluster, we’ll need one Ubuntu machine to act as the controller, and one or more additional servers to be used as worker nodes. As you’ll learn later in the book, the goal of Kubernetes is to schedule containers to run in Pods on worker nodes. Pods in Kubernetes are a collection of one or more containers, and every container is run in a pod. With more than one worker node, you can benefit from being able to run more pods (and as an extension, run more containers) and having load spread between multiple workers to have the most control over how your applications are served to the end-user.

But on what type of device should you run Kubernetes on in order to test it out and learn it? For this, we have the same possibilities as we discussed when installing Ubuntu Server back in Chapter 1, Deploying Ubuntu Server—we can use VMs, physical servers, spare desktops or laptops, as well as Raspberry Pi’s (or any combination of those). Again, use whatever you have available. For a rollout in an organization, you may end up using a virtualization server for the controller and worker nodes, or perhaps physical servers. One of my favorite platforms for Kubernetes is the Raspberry Pi, believe it or not. I’ve been running a Kubernetes cluster in production for several years that consists of only Raspberry Pi 4 units with complete success. If nothing else, purchasing a few Pi’s is a relatively low barrier to entry. You can even utilize a cloud provider if you’d like, though doing so goes beyond the scope of this chapter as cloud providers generally have their own tools in place to manage clusters.

In general, it’s recommended to have a controller node and a handful of workers. A single worker will suffice, but in this chapter, I’ll show the process of setting up two workers to better understand how the platform scales. On your end, you can use a single controller and worker, or set up however many workers you’d like. One of the best things about Kubernetes is that you aren’t stuck with the number of workers you set up initially, you can add more nodes to your cluster as time goes on. An organization may start with just a few workers but add additional ones as the need arises.

As far as resources go, requirements for Kubernetes are relatively modest. The node that’s designated as the controller should have a minimum of two CPUs or one CPU with at least two cores. Obviously, it’s better if you have more than that, such as a quad-core CPU for the controller, but two cores are the minimum. The worker nodes can have a single CPU core each, but the more cores they have, the more Pods they’ll be able to run. When it comes to RAM, I recommend a minimum of 2 GB of memory on each, but again, if you have more than that on each node then that’s even better.

It’s important that the IP addresses for the nodes in your cluster do not change. If an IP of a cluster node changes, it can cause the cluster nodes to be unable to find each other. In Chapter 10, Connecting to Networks, we learned how to set up a static IP address, which is a good solution for preventing IP addresses from changing. As an alternative, you can set up a static lease in your DHCP server if you wish. It doesn’t matter which solution you use, so long as you prevent the IP addresses of cluster nodes from changing.

For some readers, a more accessible method of setting up comes in the form of MicroK8s, which allows you to even set up Kubernetes on a single machine. If your only goal is to set up a simple test installation, it’s one of the best and easiest methods of getting started. MicroK8s is not recommended for running a production cluster in an organization, but it’s definitely a great way to learn. I do recommend that you work through the standard Kubernetes procedure with multiple nodes if you can, but in the next section we’ll walk through setting up MicroK8s for those of you that would like to utilize that method.

Utilizing MicroK8s

If you don’t have more than one machine or enough memory on your laptop or desktop to run multiple nodes inside virtualization software such as VirtualBox, MicroK8s is a simple way to set up a Kubernetes instance for testing the platform, as well as going through the examples in this chapter. MicroK8s is actually provided by Canonical, the makers of Ubuntu. That just goes to show you how important Kubernetes is to the Ubuntu distribution, its own creator is going the extra mile to contribute to the platform. MicroK8s is available for Linux, macOS, as well as Windows. So regardless of which operating system you’re running on your laptop or desktop, you should be able to install and use it. If nothing else, it gives you a great test installation of Kubernetes that will come in handy as you learn.

To set it up, follow along with one of the subsections below that matches the operating system installed on your computer.

Installing MicroK8s on Linux

When it comes to Linux, MicroK8s is distributed as a snap package. We covered snap packages back in Chapter 3, Managing Software Packages, and it’s a great solution for cross-distribution package management. If you run a recent version of Ubuntu on your computer, then you should already have support for snap packages and you can proceed to install MicroK8s. If you’re running a distribution of Linux on your computer other than Ubuntu, then you may not have access to the snap command by default. If in doubt, you can use the which to see if the command is available:

which snap

If you do have the ability to install snap packages, then your output should be similar to the following:

Figure 18.1: Checking if the snap command is available

If you see no output when you run which snap, then that means your distribution doesn’t have support for this package type installed. Canonical makes the snap command available to distributions outside of Ubuntu, so if you’re using a distribution other than Ubuntu, it’s usually just a matter of installing the required package.

Canonical has additional information available on the following site, which shows the process of enabling snap for several distributions: https://snapcraft.io/docs/installing-snapd.

For example, the documentation on that site gives the following command to install the required package for Fedora:

sudo dnf install snapd

Essentially, as long as you follow along with the instructions for setting up snap that are specific to the distribution of Linux you’re running, the process should be simple enough. If for some reason your distribution isn’t supported, you can simply use the same Ubuntu installation as you’ve been using during this book, which will have snap support built in.

Once you either confirm you already have support for snap on your computer, or you successfully enable the feature, you can install MicroK8s with the following command:

sudo snap install microk8s --classic

If successful, you should see a confirmation in your terminal that the process was successful:

Figure 18.2: Setting up MicroK8s on a Linux installation

Now that we have MicroK8s installed on your Linux computer, we can proceed through the chapter. Or, you can check out the next section to see the installation process for macOS.

Installing MicroK8s on macOS

On macOS, the installation process for MicroK8s is similar. The process for Mac will utilize Homebrew, which is an addon for macOS. Homebrew isn’t specific to Kubernetes or MicroK8s; it gives you the ability to install additional packages on your Mac that aren’t normally available, with MicroK8s being one of many. Homebrew is essentially a command-line utility with syntax similar to Ubuntu’s apt command.

To install Homebrew, visit https://brew.sh in your browser, and the command required to set it up will be right there on the site. I could insert the command to install Homebrew right here on this page, but the process may change someday, so it’s better to get the command right from the official website for the utility. At the time of writing, the page looks like this:

Figure 18.3: The Homebrew website

In the screenshot, you’ll notice the command with white text on a black highlight. It’s cut off a bit, due to the command being wider than this page. You should see the entire command on the site, so you can copy it from there and paste it into your Mac’s terminal.

It’s becoming increasingly popular for application developers to provide a command on their website to install their application that you paste directly into your terminal. This is very convenient and allows you to set up an app quickly. But you should always research and inspect such commands before pasting them into your terminal (regardless of your operating system). It’s possible that an outside actor could hijack the command on a website to trick you into running something nefarious. You can use the wget command to download the script their command will end up running, so you can check it to ensure it’s not doing something evil. Since this method of deploying software is getting more and more common, I recommend getting into the habit of checking commands before running them (especially if they use sudo).

Once you have Homebrew installed on your Mac, you can proceed to install MicroK8s with Homebrew with the following command:

brew install ubuntu/microk8s/microk8s

Once that command finishes, you should have MicroK8s installed on your Mac. In the next sub-section, we’ll see the same process in Windows.

Installing MicroK8s on Windows

If you’d like to try out MicroK8s on a laptop or desktop computer running Windows 10, there’s a specific installer available that will allow you to get it up and running. On the MicroK8s website, at https://microk8s.io, you should see a large green button labeled Download MicroK8s for Windows, and if you click on it, you’ll be able to download the installer:

Figure 18.4: The MicroK8s website, with a button to download the installer for Windows

Once the download finishes, you can then launch the installer, which will have several different sections, all of which you can accept the defaults for. Click Next and then the Install button to begin the actual installation:

Figure 18.5: The MicroK8s installer for Windows

After the installation begins, another installation will also launch in parallel that will ask you to select a hypervisor:

Figure 18.6: The MicroK8s installer for Windows, selecting a hypervisor

For this, you can leave the default selection on Microsoft Hyper-V, and click Next. You can keep the remaining options at their defaults and click Next through any remaining prompt that comes up. Depending on the setup of your computer, it may require you to reboot in order to finalize the required components before the installation can be completely finished. If you do see a prompt to reboot, do so, and then launch the installer again.

You’ll see the following window appear, asking you to set up the parameters for MicroK8s, specifically how many virtual CPUs to utilize, how much RAM to provide, disk space, and so on:

Figure 18.7: The MicroK8s installer for Windows, allocating resources

You can simply choose the defaults for this screen as well and click Next. Once the process is complete, you can click the Finish button to exit the installer.

Interacting with MicroK8s

At this point, if you’ve decided to utilize MicroK8s, you should have it installed on your computer. How you actually interact with it depends on your operating system. In each case, the microk8s command (which we’ll cover shortly) is used to control MicroK8s. Which app you use in order to enter microk8s commands differs from one platform to another.

On Windows, you can use the Command Prompt app, which is built into the operating system. The microk8s command is recognized and available for use after you install MicroK8s. When you enter the microk8s command by itself, it should display basic usage information:

Figure 18.8: Executing the microk8s command with no options in a Windows Command Prompt

With macOS, you can use the built-in terminal app that’s provided with the operating system. You may have another step to complete though before you can actually use MicroK8s. If you enter the microk8s command, and you receive an error informing you that support for multipass needs to be set-up, then you can run the following command to fix it:

microk8s install

If you’re curious, Multipass is a technology that’s also created by Canonical that allows you to quickly set up an Ubuntu instance for testing. It’s not specific to MicroK8s or even Kubernetes, but in our case it’s used in the background to facilitate MicroK8s. Multipass is not covered in this book, but it’s worth taking a look at if you think you’ll benefit from the ability to quickly set up Ubuntu instances to test applications and configurations on. It’s available for all of the leading operating systems.

When it comes to Linux, if you’ve already gone through the process of setting up MicroK8s, you should be ready to use it immediately. Open up your distribution’s terminal app and try the microk8s command to see if it’s recognized. If it is, you’re ready to move on.

To check the status of MicroK8s, the following command can be used to give you an overview of the various components. Before you run it, note the inclusion of sudo at the beginning. By default, this is required if your underlying operating system is Linux.

If you’re running Windows 11 or macOS, you shouldn’t need sudo. This difference has to do with how the underlying operating system handles permissions, which is different from operating system to operating system. So, if you’re not using Linux on your PC, feel free to omit sudo from microk8s commands:

sudo microk8s kubectl get all --all-namespaces

Don’t worry about the details regarding the individual components of Kubernetes at the moment, we’ll cover what you need to know as we go through the rest of the chapter. For now, so long as you don’t see errors when you run the previous command and check the status, you should be in good shape to continue.

By default, our MicroK8s installation comes with only the addons that are required for it to run. There are other addons we can enable, such as storage, which gives us the ability to expose a path on the host (the underlying operating system) to Kubernetes; gpu, which allows us to utilize NVIDIA GPUs within the containers; and others. We don’t need to worry about these for now, but we should at least enable the dns addon, which sets up DNS within our cluster. It’s not necessarily required, but not having it can create issues down the road when it comes to name resolution, so we may as well enable it now:

sudo microk8s enable dns

You should see output similar to the following:

Figure 18.9: Enabling the DNS addon in MicroK8s on a Linux installation

In my tests, I find that the command to enable the addon seems to stop responding for a while, with no output. Don’t close the window; it should catch up and then display information about the process of installing the addon eventually. If this happens, just wait it out a bit.

With the previous command, we’ve enabled the dns addon. As I’ve mentioned, there are other addons available, but that’s enough for now. At this point, just keep in mind that you can extend MicroK8s with addons, so it may be worth exploring in more detail later on if you wish. A list of addons is included in a link at the end of this chapter.

As mentioned earlier, if you’re using Linux to run MicroK8s, then you would’ve used sudo with the microk8s command we used earlier. If you’d like to remove the requirement of using sudo on Linux, you can do so by adding your user to the microk8s group. That can be done with the following command:

sudo usermod -a -G microk8s <yourusername>

After you log out and then log in again, you should be able to run microk8s commands without sudo.

As we’ll discover as we proceed through the rest of the chapter, the kubectl command is generally used to interact with a Kubernetes cluster and manage it. kubectl, short for Kube Control, is the standard utility you use to perform many tasks against your cluster. We’ll explore this more later on. But specific to MicroK8s, it’s important to understand that it uses its own version of kubectl that’s specific to it. So with MicroK8s, you would run microk8s kubectl instead of simply kubectl to interact with the cluster. Having a separate implementation of kubectl with MicroK8s allows you to target an actual cluster (the kubectl command by itself) or specifically your installation of MicroK8s (prefix kubectl commands with microk8s), so one won’t conflict with the other. As we work through the chapter, I’ll call out kubectl by itself when we get to the actual examples, so it will be up to you to remember to use microk8s in front of such commands as needed.

Now that we have our very own installation of MicroK8s on our laptop or desktop, we can proceed through the examples in this book. You can skip ahead to the Deploying containers via Kubernetes section later in this chapter to begin working with Kubernetes. In the next section though, we’ll take a look at setting up a Kubernetes cluster manually without MicroK8s, which is closer to the actual process you’d use in an actual production implementation.

Setting up a Kubernetes cluster

In the previous section, we set up MicroK8s, which provides us with a Kubernetes cluster on a single machine, which is great for testing purposes. That might even be all you need in order to learn Kubernetes and see how it works. If you can, I still recommend setting up a cluster manually, which will give you even more insight into how the individual components work together. That’s exactly what we’re going to do in this section.

Before we do get started, it’s important to synchronize our mindset a bit. Of all of the activities we’ve worked through so far, setting up a Kubernetes cluster manually is easily the most complex. Kubernetes itself is made up of many components, as well as settings. If any one component is incorrect or a setting is misconfigured, the entire process can fail. In this section, a great deal of care and attention was spent to ensure (as much as possible) that the process works to the point where it’s completely reproducible and has been broken down to only the required components to simplify everything. However, if the process doesn’t work out well when you go to try it, don’t worry - it’s perfectly fine if we encounter an issue. We’re still learning, and fixing things is the best way to learn. So the bottom line is to have fun, and be patient.

What does a typical Kubernetes cluster look like? When it comes to a production installation in an actual data center, having Kubernetes installed on multiple servers is commonplace. Typically, one of them will act as the controller node, and then you can add as many worker nodes as you need.

As your needs expand, you can add additional servers to provide more worker nodes to your cluster. Setting up a controller Kubernetes node and then individual workers is a great way to see the actual relationship in action. And that’s exactly what we’re going to do in this section.

As I walk you through the process, I’m going to do so utilizing three VMs. The first will be the controller, and the remaining two will be workers. On your end, it doesn’t really matter how many workers you decide to go with. You can have a single worker, or a dozen—however many you’d like to set up is fine. If you do set up more nodes than I do, then you would simply repeat the commands in this section for the additional nodes. For the nodes on your end, feel free to use whatever combination of physical or VMs makes sense and fits within the resources of the equipment you have available.

Before we get started, you’ll want to have already installed Ubuntu Server on each machine and install all of the updates. It’s also a good idea for you to create a user for yourself if you haven’t already done so. The remainder of this section will assume you’ve already set up Ubuntu on each. After you’ve installed Ubuntu on everything, it’s a good idea to also configure the hostnames on each node as well to make it easier to tell them apart. For example, on the node that I’m intending to use as the controller, I’m going to set the hostname as controller. For the workers, I’ll name them node-01 and node-02 respectively. You can name yours in whatever theme makes sense, but the reason I specifically mention it is to make sure that you name them something, as by default they’ll each show the same hostname in the command prompt, and that may be confusing.

Now that you have everything you need, let’s set up Kubernetes!

Preliminary setup

In this section, we’re going to complete some miscellaneous setup tasks before we actually start building our cluster. These are basically the prerequisites for our cluster to function.

We should first ensure that each of the servers we intend on using for the cluster has either a static lease, or a static IP that will not change. You can change your server’s IP address at any time, but with Kubernetes, there’s some additional steps involved if you do change the IP later. For this reason, I suggest setting your static lease or static IP right now, before we build the cluster. That way, you won’t have to worry about that later. The process was covered in Chapter 11, Setting Up Network Services, so I won’t repeat that here. On your side, be sure to set up a static lease or static IP, and then continue further with our setup here.

Next, if the devices you’ve selected for this exercise includes servers that are being built using the Raspberry Pi, then there are some special requirements that are specific to the Pi. If you are using one or more Raspberry Pi units, be sure to complete this step (otherwise, skip this if you’re NOT using the Raspberry Pi).

To set boot parameters specific to the Raspberry Pi, open up the following file in your editor of choice:

sudo nano /boot/firmware/cmdline.txt

What you’re about to do in this file, is add some cgroup options to control how Kubernetes is able to interact with the hardware of your device. You’ll want to add the following options to the existing line within the file. Don’t create a new line. Instead, add these options to the very end of the first (and only) line within that file:

cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1 swapaccount=1

Save and exit the file, and that should be the only change we’ll be making that’s specific to the Raspberry Pi.

In order to build a Kubernetes cluster, we’ll need to set up something called a Container Runtime. A Container Runtime is the means by which Kubernetes will run containers within the cluster. Without a runtime, there’s no ability for Kubernetes to run even a single container. While Kubernetes is a container orchestration solution, it doesn’t mandate which container runtime you use. The suggested runtime known as containerd, so that’s what we’ll use in our cluster. Other container runtimes include (but aren’t limited to) Docker, CRI-O, and there’s others. We’ve already worked with Docker in this book, but for our cluster, we’re going to deploy it with the suggested containerd runtime.

When it comes to the containers we can run, there’s no difference - containerd for example can run Docker containers just like we’ve been doing. So even though we’re going to go with a different container runtime, it shouldn’t really make a difference at all when it comes to the end result.

In this section, the commands that I’m going to have you run should be executed on each of the instances you plan on using with Kubernetes, regardless of whether or not you’re working with the intended controller or a node; these commands need to be run on each. There will be commands specific to the controller and the nodes later on, but I’ll mention the intended target if it’s something I’d like you to do on specifically one or the other.

The first thing we’re going to do is install the containerd package:

sudo apt install containerd

After the package is installed, we’ll check the status of it and ensure that it’s running:

systemctl status containerd

We should see that the unit is running:

Figure 18.10: Checking the status of the containerd runtime

We’re not quite done with containerd, though. It’s going to need some default configuration in order to function properly, but by default, there’s no sample config provided automatically. Let’s create that config, starting with adding a directory to our server to house the configuration file:

sudo mkdir /etc/containerd

Next, we’ll create the config file for containerd with the following command:

containerd config default | sudo tee /etc/containerd/config.toml

That command will not only create the default configuration file for containerd, it also prints the configuration information to the screen as well so we can see what the default values are. However, while most of the default settings are fine, we’ll need to make some adjustments to this file. Open up the /etc/containerd/config.toml file in your editor of choice.

The first change to make to this file, is to set the cgroup driver to systemd. To do so, look for the following line:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]

Several lines below that, you’ll see the following line:

SystemdCgroup = false

Change that line to true instead:

SystemdCgroup = true

At this point, we’re done with containerd for now. Let’s move on and configure some important system tweaks. The first of these is disabling swap. Although it’s usually not a good idea to run a server without swap, setting up a Kubernetes cluster is the exception to this. In fact, when we go to initialize the cluster later, the process will actually abort if swap is enabled. So let’s take care of disabling it now.

First, we’ll run the following command to disable swap:

sudo swapoff -a

After doing this, if we run free -m we should see all zero’s for swap:

Figure 18.11: Running the free -m command to check that swap is disabled

However, the command that we’ve just run to disable swap was not a permanent change. When we reboot our server, swap will be automatically re-enabled. In order to disable swap for good, we’ll need to edit the /etc/fstab file and comment out the line that activates swap during boot.

The following screenshot shows an example configuration line for swap, but it’s been commented out by inserting the # character at the beginning of that line:

Figure 18.12: The /etc/fstab file, with the swap line commented out

At this point, swap should be turned off, and considering we’ve commented out the swap line in the /etc/fstab file, swap won’t be re-enabled the next time we start our server.

Next, we’ll need to make a change to the /etc/sysctl.conf file to enable bridging. This is similar to what we worked through as we set up an internet gateway back in Chapter 11, Setting Up Network Services. Once we’ve opened up the /etc/sysctl.conf file in an editor, we’ll need to uncomment the following line:

#net.ipv4.ip_forward=1 

It should now look like this:

net.ipv4.ip forward=1

With that line uncommented, bridging should be enabled the next time we start our server.

Next, we’ll create the following file, which we’ll use to ensure a required kernel module is loaded when the server starts:

sudo nano /etc/modules-load.d/k8s.conf

Inside that file, we’ll add the following:

br_netfilter

That’s actually it for that file, so save and exit the editor. The br_netfilter module assists with networking for our eventual Kubernetes cluster, and will need to be enabled in order for the cluster to function. By creating the /etc/modules-load.d/k8s.conf file, we’re ensuring this kernel module is loaded automatically when we start our server.

Before we continue, further, let’s take a moment and ensure we’ve done everything we need to do up to now. At this point, on each of the servers you intend to use with Kubernetes (the controller as well as the nodes) you should’ve accomplished the following:

  • Installed all updates
  • Set a hostname
  • Set up a static IP or lease on each node
  • Set Raspberry Pi-specific boot options (if you’re using a Raspberry Pi)
  • Installed containerd
  • Created the /etc/containerd/config.toml file for containerd and also set the cgroup driver
  • Disabled swap
  • Added the /etc/modules-load.d/k8s.conf file with the br_netfilter module listed inside
  • Edited the /etc/sysctl.conf file to enable bridging

All of the above should’ve been completed on every server you plan to use with your cluster. Once you’re sure that you’ve performed each of those tasks, we should reboot each of our nodes:

sudo reboot

Once your nodes finish starting back up, we can proceed to actually install Kubernetes.

Installing Kubernetes

Now it’s finally time to start building our cluster, and we’ll start by installing the required packages. To do so, we’ll need to add the appropriate repository so we can have access to the required packages for installing Kubernetes. To add the repository, we’ll first add the key for the repository so our server will accept it as a trusted resource:

sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg

To add the actual repository itself, we will run the following command:

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

After adding the repository, we’ll update our local index:

sudo apt update

What you may notice is that the repository URL references xenial instead of the actual codename for Ubuntu 22.04, which is jammy (short for Jammy Jellyfish). At the time of writing, there is no dedicated Kubernetes repository for Ubuntu 22.04 yet, but the process will still work just fine. It’s possible that by the time you’re reading this, there will be a dedicated repository for 22.04. But for now, we can use the line mentioned above for our repository file.

Next, we can install the packages required for Kubernetes:

sudo apt install kubeadm kubectl kubelet

We’re installing three packages, kubeadm, kubectl, and kubelet:

  • kubeadm: The kubeadm package gives us tools we can use to “bootstrap” our cluster. We can use this tool to initialize a new cluster, join a node to an existing clusterand upgrade the cluster to a newer version.
  • kubectl: This package provides us with the Kubernetes command-line tool, kubectl. We will use this tool to interact with our cluster and manage it.
  • kubelet: The Kubernetes kubelet acts as an agent that facilitates communication between nodes. It also exposes API endpoints that can be used to enable additional communication and features.

Unlike all of the previous commands we’ve run through so far while setting up our cluster, the following will be entered and ran only on the node that you’ve designated as the controller (before you run it, read the paragraph that follows to know how to customize the command):

sudo kubeadm init --control-plane-endpoint=172.16.250.216 --node-name controller --pod-network-cidr=10.244.0.0/16

The previous kubeadm init command should be customized a bit, before you run it. There’s a few things you should change to ensure they match your environment. In fact, I bolded the individual settings you should customize. I’ll also describe some additional details about those particular settings now.

--control-plane-endpoint=172.16.250.216: For this option, the IP address that is mentioned here should be the same IP address that’s assigned to your server. I filled in the value I used for my test server, but you should make sure this matches the actual IP on your side. Since this is a very specific setting, this is one of the reasons why I recommended you finalize your IP address for your nodes before you set them up in the cluster.

--node-name controller: When I set up my test server, I simplified its hostname down to simply controller. On your side, you can set this to match your controller’s hostname as well.

Note that I didn’t bold the second IP address in the command, 10.244.0.0/16. The reason for this, is that you should not change that particular IP declaration. That’s actually the IP scheme for the Pod Network, which is an internal network to Kubernetes and is not accessible from the outside. You can actually customize this to your own scheme, but then you’d have to change other settings as well. Since this is a dedicated internal network, there shouldn’t be any need to change this so I recommend to leave it as-is.

The kubeadm init command that I mentioned earlier, after you customize it, will initialize the cluster and assign it a Pod Network. If the initialization process is successful, you’ll see a join command printed to the terminal at the end. If you do see it, then congratulations! You’ve successfully set up a Kubernetes cluster. It’s not a very usable cluster yet, because we haven’t added any worker nodes to it at this point. But you do, by definition, have a Kubernetes cluster in existence at this point. On my controller node, I see the following output:

Figure 18.13: Successful initialization of a Kubernetes cluster, showing a join command

The join command that’s shown to you after the process is complete is very special—you can copy that command and paste it into the command prompt of a worker node to join it to the cluster, but don’t do that just yet. For now, copy that command and store it somewhere safe. You don’t want to allow others to see it, because it contains a hash value that’s specific to your cluster and allows someone to join nodes to it. Technically, showing my join command with the hash value is the last thing I should do in a book that will be seen and read by many people, but since this is just a test cluster with no actual value, I don’t mind you seeing it.

Also, in the same output as the join command, are three additional commands we should run on the controller node. If you scroll up in your terminal window to before the join command, you should see output similar to the following:

Figure 18.14: Output of the initialization process for Kubernetes showing additional commands to run

For those three commands, you can copy and paste them as-is right back into the terminal, one by one. Go ahead and do that. Essentially what you’re doing is creating a local config directory for kubectl right in your user’s home directory. Then, you’re copying the admin.conf file from /etc/kubernetes and storing it in the newly created .kube folder in your home directory with a new name of config. Finally, you’re changing ownership of the .kube directory to be owned by your user. This will allow your user account to manage Kubernetes with the kubectl command, without needing to be logged in as root or use sudo.

Kubernetes itself consists of multiple Pods, within which your containers will run. We haven’t deployed any containers yet, we’ll do that later. But even though we didn’t deploy any containers ourselves, there are some that are actually running already. See for yourself:

kubectl get pods --all-namespaces

On my end, I see the following:

Figure 18.15: Checking the status of pods running in our Kubernetes cluster

Notice that the namespace for each of these Pods is kube-system. This is a special namespace, where Pods related to the Kubernetes cluster itself will run. We didn’t explicitly ask for these to run, they’re run as part of the cluster and provide essential functionality. Another important column is STATUS. In the screenshot, we see that most of them are Running. The first two, though are in a state of Pending, which means that they’re not quite ready yet. This is actually expected, we haven’t deployed an overlay network yet, which is required for the coredns pods to function.

Overlay networks are virtual networks that are created to serve as a network on top of another network. The concept is not specific to Kubernetes, but when used in Kubernetes, the overlay network manages communication between nodes.

Other Pods may also show a status of Pending, and those should automatically switch to Running when they finish setting themselves up. How long this takes depends on the speed of your hardware, but it should only take a few minutes.

The others will eventually change to a state of Running once their setup is finished – but they might also crash as well, and respawn. Since there’s no overlay network yet, the pods aren’t able to fully communicate yet. So if you see some errors, don’t worry about that yet.

Let’s deploy an overlay network so we can make the cluster more stable:

kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml

When we check the status again, we should see an additional Pod running (kube-flannel-ds), and the Pods that had a state of Pending previously should now be Running:

Figure 18.16: Checking the status of Pods running in our Kubernetes cluster again

So, what exactly did we do? Flannel is a networking layer that can run on Kubernetes. Networking is required for Kubernetes to function, as Pods within the cluster need to be able to communicate with one another. There are multiple different types of networking models you can implement in Kubernetes, Flannel is just one available option. If you’re using a cloud provider, such as AWS, then the networking model is typically chosen for you. Since we’re building a cluster from scratch, we have to choose a networking model—Kubernetes itself doesn’t come with one.

Setting up networking in a cluster that was manually created can be quite an involved task, Flannel is easy to set up (we simply deploy it) and its defaults meet the needs of Kubernetes and will get us up and running quickly. There are definitely some other options for the networking layer to consider for your cluster, but Flannel is good enough for us for what we need right now.

Next, it’s time to watch the magic happen and join worker nodes to our cluster. We can check the status of all of our nodes with the following command:

kubectl get nodes

Right now though, we only have the controller showing up in the output since we haven’t yet added any nodes:

Figure 18.17: Checking the status of the nodes within the cluster

To add a worker to our cluster, we can enter the join command we saw earlier on a node designated as a worker. If you recall, the join command was shown in the terminal when we first initialized our cluster. The command will look something like the following, which I’ve shortened a bit to fit on this page (I’ve added sudo to the beginning):

sudo kubeadm join 172.16.250.216:6443 --token zu5u3x.p45x0qkjl37ine6i 
    --discovery-token-ca-cert-hash sha256...1360c

You should consider the join command as private, and not show it to anyone, nor should you upload it to a Git repository or a documentation server. Reason being, it could be used by an outside actor to join something to your cluster. In my case, I truncated the hash so it’s impossible to reproduce, but keep this in mind going forward.

For you, the command will be very different. The IP address in the command is for the controller, which will no doubt be different on your end. The hash value will be different as well. Basically, just copy the join command you were provided while initializing the cluster on the controller and paste it into each of your worker nodes. You should see a message in the output that the node was successfully added to the cluster:

Figure 18.18: Successfully adding a worker node to the Kubernetes cluster

After you’ve run the join command on each of your worker nodes (on however many you decided to create), you can run the kubectl get nodes command again on the controller and verify the new nodes appear on the list. I added two nodes, so I see the following output on my end:

Figure 18.19: Output of kubectl showing worker nodes now added to the cluster

Once all of the nodes you plan on deploying show a STATUS of Ready, then your cluster setup is complete!

If you run into any trouble joining a node to the cluster, you can try to regenerate the join token. After a while, it’s possible the original could’ve expired and the certificates won’t be valid. To regenerate the join command, run the following from the controller node:

kubeadm token create --print-join-command

This will print a brand new join command you can use in place of the original.

At this point, our cluster exists and has one or more worker nodes ready to do our bidding. The next step is to actually use the cluster and deploy a container. That’s exactly what we’ll do in the next section.

Deploying containers via Kubernetes

Now it’s time to see our work pay off, and we can successfully use the cluster we’ve created. At this point, you should have either set up MicroK8s, or manually created a cluster as we’ve done in the previous section. In either case, the result is the same: we have a cluster available that we can use to deploy containers.

Keep in mind that if you’re using MicroK8s, you might need to prepend microk8s in front of kubectl commands, depending on how you set up MicroK8s. I’ll leave it up to you to add microk8s to the front of such commands as you go along, if you’re using MicroK8s and you don’t have it set up to simplify microk8s kubectl to kubectl.

Kubernetes utilizes files created in the YAML format to receive instructions. Does that sound familiar? In Chapter 15, Automating Server Configuration with Ansible, we worked with YAML files as that’s the format that Ansible playbooks are written in. YAML isn’t specific to Ansible; it’s used with many different applications and services, and Kubernetes will also recognize the YAML format to contain instructions for how to deploy something. Here’s an example YAML file to get us started. This one in particular is intended to launch an NGINX container within our cluster:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-example
  labels:
    app: nginx
spec:
  containers:
    - name: nginx
      image: linuxserver/nginx
      ports:
        - containerPort: 80
          name: "nginx-http"

Before I show you how to run it, let’s walk through the file and understand what’s going on.

apiVersion: v1
kind: Pod

First, we’re identifying the API version we intend to use, and then we’re setting the kind of deployment we intend to set up. In this case, we’re deploying a pod, which is what our containers run inside of in Kubernetes. One or more containers can run in a Pod, and a worker node can run one or more Pods at the same time.

metadata:
  name: nginx-example
  labels:
    app: nginx

Next, we add some metadata. Metadata allows us to set special parameters specific to our deployment. The first item of metadata we’re customizing is the name, we’re naming the Pod nginx-example in this case. We’re also able to set up labels with metadata, which is a “name: value” key pair that allows us to add additional values to our Pod that we can refer to later. In this case, we’re creating a label called app and setting it to nginx. This name is arbitrary; we could call it potato if we wanted to. Setting this to nginx is a descriptive label that makes it obvious to someone else what we are intending to run here.

spec:
  containers:
    - name: nginx       image: linuxserver/nginx

Moving on, the spec section allows us to specify what exactly we want to run in our Pod, and how we want it to run. We want to run a container in our Pod, and specifically a container that we’ll name nginx, which we’ll retrieve from a registry called linuxserver, and we’re requesting a container from that registry by the name of nginx.

The registry we’re fetching the container image from deserves some extra explanation. This registry in particular is located at https://linuxserver.io, which is a special service that makes container images available for us to download and use. The site has a documentation section that gives us information about each of the container images they offer. Why use the linuxserver.io registry? The reason is that their service makes available various container images that support a variety of architectures, including x86 as well as ARM. The latter is especially important, because if you’re using Raspberry Pi units for your cluster, they won’t be able to utilize container images created for x86. If you do attempt to run a container image that does not support ARM, then the container will fail to launch on a Pi. Since linuxserver.io makes container images available for multiple architectures, they should work fine regardless of the type of device you decided to use for your cluster. Whether you’re using an x86 physical server or VM for your worker node, the nginx container we’re retrieving from that registry should function just fine.

      ports:
        - containerPort: 80
          name: "nginx-http"

In the last few lines, we’re setting up a container port of 80, which is the standard for a web server. This is the default port that NGINX listens on when it runs, and NGINX is what we’re intending to run inside our container. We’re applying a name to this port declaration, and calling it nginx-http. We can refer to that name in a subsequent YAML file (if we have more than one) and that works better than having to type the same port in each file. Referring to a port by name is not all that different from variables in scripting or programming languages.

Before we continue, there’s a bit of difference regarding how you deploy resources to a cluster within MicroK8s compared to a cluster that was set up manually on actual servers. The commands going forward will assume you’ve set up a cluster manually. If you’re using MicroK8s on Windows or macOS, you’ll need to copy any deployment files you create into the MicroK8s VM that’s created as part of the installation of MicroK8s. If you try to save a file locally and deploy it as we’re about to do in the next paragraph, it will fail, because the file will not be found on the VM. To copy a deployment file to the MicroK8s VM to enable you to deploy it, you can use the following command to copy the file into the VM first:

multipass transfer <filename.yml> microk8s-vm:

As we go along, keep in mind that whenever we’re deploying a file, you’ll have to make sure to transfer it first. Also, a manually created cluster uses the kubectl command, while MicroK8s requires you to prefix kubectl with microk8s, so such commands become microk8s kubectl instead of just kubectl.

Let’s go ahead and deploy the container to our cluster. Assuming you named the YAML file as pod.yml, you can deploy it with the following command on the controller node:

kubectl apply -f pod.yml

As mentioned previously, the kubectl command allows us to control our cluster. The -f option accepts a file as input, and we’re pointing it to the YAML file that we’ve created.

Once you’ve run the command, you’ll be able to check the status of the deployment with the following command:

kubectl get pods

For me, I see the following when I run it:

Figure 18.20: Checking the status of a container deployment

From the output in my case, I can see that the process was successful. The STATUS is Running. What we don’t see is what worker node the Pod is running on. It’s nice to know that it’s Running, but where? We can get more information by adding the -o wide option to the end of the command:

 kubectl get pods -o wide

The output contains much more information, so much that a screenshot of it won’t fit on this page. The focus though, is that among the extra fields we have with this version of the command is the node field, which shows us which worker node is handling this deployment. We’ll even see an IP address assigned to the Pod as well.

We can use the IP address shown for the Pod to access the application running inside the container. In my case, my Pod was given an IP address of 10.244.1.2, so I can use the curl command to access the default NGINX web page:

curl 10.244.1.2

The output that’s shown in the terminal after running that command should be the HTML for the default NGINX web page. Note that the curl command may not be available in your Ubuntu installation, so you may need to install the required package first:

sudo apt install curl

We have one potential issue though: if you want to be able to access an application running inside the cluster on a machine other than the controller or worker nodes, it won’t work. By default, there’s nothing in place to route traffic from your LAN to your cluster. This means that in my case, the IP address of 10.244.1.2 that was given to my Pod was provided to it by the Pod Network, the router on my network doesn’t understand that IP address scheme so trying to access it from another machine on the LAN will fail. What’s interesting is that you can access the application from any other node within the cluster. In my case, the NGINX Pod is running on the first worker node. However, I can actually run the previous curl command from worker #2 even though the Pod isn’t running there, and I’ll get the same exact output. This works because the Pod Network is cluster-wide; it’s not specific to any one node.

The IP address of 10.244.1.2 is unique to the entire cluster, so if I run another container it won’t receive that same IP address, and every node within the cluster knows how to internally route to IPs within that network.

Not allowing outside devices to access applications within the cluster is great for security purposes. After all, a hacker can’t break into a container that they can’t even route to. But the entire point of running a Kubernetes cluster is to make applications available to our network, so how do we do that? This can be a very confusing topic for newcomers, especially when we’re setting up a cluster manually. If we’re using a cloud service such as AWS or Google Cloud, they add an additional layer on top of their Kubernetes implementation that facilitates routing traffic in and out of the cluster. Since we set up our cluster manually, we don’t have anything like that in place.

When it comes to building services and networking components designed to handle networking to bridge our LAN and Kubernetes cluster, that’s an expansive topic that can span a few chapters in and of itself. But a simple solution for us is to create a NodePort Service. These are two new concepts here, a Service as well as NodePort. When it comes to a Service, a Pod isn’t the only thing we can deploy within our cluster. There are several different things we can deploy, and a Service is a method of exposing a Kubernetes Pod, and a NodePort is a specific type of service that gives us a specific method for facilitating a way of accessing it. What NodePort itself does is expose a port running inside a Pod to a port on each cluster node.

Here’s a file we can deploy to create a NodePort Service for our Pod:

apiVersion: v1
kind: Service
metadata:
  name: nginx-example
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      nodePort: 30080
      targetPort: nginx-http
  selector:
    app: nginx

As you can see, the kind is Service; as this time, we’re deploying a service to our cluster to complement the Pod we’ve deployed previously. The type of service is NodePort, and we are mapping port 80 within the Pod to port 30080 in our cluster. I chose port 30080 arbitrarily. With NodePort, we can utilize ports 30000 through 32767. The selector in this file is set to nginx, which is the same selector we used while creating the Pod. Selectors allow us to “select” Kubernetes resources via a name we assign them, to make it easier to refer to them later.

Let’s deploy our service:

kubectl apply -f service-nodeport.yml

Assuming we’ve typed everything in the YAML file for the service properly, we can retrieve the status of services running within our cluster with the following command:

kubectl get service

If everything has gone well, you should see output similar to the following:

Figure 18.21: Checking the status of a service deployment

In the output, we should see the status of our service deployment, which is the second line in the screenshot. We can also see the port mapping for it, showing that port 30080 should be exposed to the outside. To test that it’s actually working, we can open a web browser on a machine that is within our LAN, and it should be able to access the controller’s IP address at port 30080:

Figure 18.22: Accessing a web page served by an NGINX container in a cluster from within a web browser

Another interesting benefit is that we didn’t actually have to use the IP address of our controller node, we can use the IP address of a worker node as well and see the same default page. The reason this works is that the mapping of port 30080 to port 80 inside the Pod is cluster-wide, just as the internal Pod Network is also cluster-wide. Accessing a resource on one is the same as accessing the same resource on any other, as the request will be directed toward whichever node is running the Pod that matches the request.

When it comes to removing a Pod or a Service, assuming you want to decommission something running in your cluster, the syntax for that is fairly straightforward. To remove our nginx-example Pod, for example, you can run this:

kubectl delete pod nginx-example

Similarly, to delete our service, we can run this:

kubectl delete service nginx-example

At this point, you not only have a working cluster, but you also have the ability to deploy containers to it and set them up to be accessible from outside the Pod Network. From here, I recommend you practice with this a bit and attempt to run additional containers and apps to have a bit of fun with it.

Summary

In this chapter, we took containerization to the next level and implemented Kubernetes. Kubernetes provides us with orchestration for our containers, enabling us to more intelligently manage our running containers and implement services. Kubernetes itself is a very expansive topic, and there are many additional features and benefits we can explore. But for the goal of getting set up on Kubernetes and running containers with it, we did what we needed to do. I recommend that you continue studying Kubernetes and expand your knowledge, as it’s a very worthwhile subject to dig deeper into.

Speaking of subjects that are worthwhile to learn, in the next chapter, we’re going to learn how to deploy Ubuntu in the cloud! Specifically, we’ll get started with Amazon Web Services, which is a very popular cloud platform.

Relevant videos

Further reading

Join our community on Discord

Join our community’s Discord space for discussions with the author and other readers:

https://packt.link/LWaZ0

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.250.247