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:
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.
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.
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.
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.
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.
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.
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.
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.
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!
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:
containerd
/etc/containerd/config.toml
file for containerd
and also set the cgroup driver/etc/modules-load.d/k8s.conf
file with the br_netfilter
module listed inside/etc/sysctl.conf
file to enable bridgingAll 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.
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
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
. We will use this tool to interact with our cluster and manage it.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.
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.
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.
Join our community’s Discord space for discussions with the author and other readers:
18.218.250.247