Saturday, 18 December 2021

Introduction to Kubernetes

This blog is a brief introduction into what Kubernetes is, and how it works. Not on the low level, but more of the part that's interesting to you and me as a software developer. In the end, I also want to give you all the information that you need in order to deploy your Django project in the Kubernetes cluster.

So, What is Kubernetes?

First and foremost, Kubernetes is a distributed system to run containers. It can not only run Docker containers, but also others. Another term would be orchestrator. However, you can't just run containers. The smallest deployable object in Kubernetes is a pod. A pod runs at least one container and pods ensure that their containers are restarted if needed and desired. Or they vanish when all the containers are terminated. I'll go into a bit more detail on pods in a moment.

For now let's look at how you'd communicate with Kubernetes. You generally talk to a REST API that's run on manager nodes. The counterpart to those nodes are worker nodes. Those nodes actually run the pods and thus the containers. You interact with Kubernetes using a command line tool called kubectl.

Another property of Kubernetes is it's eventual consistency. That means, just because you told Kubernetes to do something and just because the API responded to you with, "Okay," it doesn't mean it's done. Some operations may take a while. It's also eventually consistent because it can deal with fault, to a degree at least. If a container dies, Kubernetes can restart it. Or when a worker node is offline, Kubernetes will automatically start the containers on other worker nodes, and maybe even start a new worker node. Let's look at the diagram, what Kubernetes could look like on a high level.

We'll start with the overall cluster. A cluster is typically standalone. However, you could run multiple clusters that interact with each other through typical network interfacing Within a cluster you have nodes. In the chart here, I'm using three. When you run Kubernetes locally on your computer, you typically only have one node, but any somewhat serious cluster would have at least two nodes, or even better, three. This is mostly or actually solely for reliability and redundancy. Nodes can either be physical, bare metal servers or VMs in your cloud provider, such as EC2 instances. In fact, if you're using Kubernetes on any cloud provider, like AKS, EKS, GKS et cetera, they use the underlying VMs. And if you want your cluster to survive a data center fire, you would make sure to spread the nodes across different data centers, and set up proper network communication between them. Again, the typical cloud provider, they do that for you automatically, when you spin up any of their hosted Kubernetes services.

As mentioned earlier, on the nodes, we have pods. they are spun up on the nodes that have the required resources available. There either exists just a single instance of a pod, like the one with the purple background here. All pods exist multiple times. This is for example, as part of something called deployment, which starts the same pod multiple times to provide redundancy. For example, the blue and green pods exist two times each and the yellow and red ones, three times. For the yellow one, something called a pod anti-affinity is defined, in order to prevent the same pod to be run more than once per node. This is a good idea to ensure the downtime of one node doesn't cause a disruption to the service by taking a lot of pods down. In contrast, the red pods do not define a pod anti-affinity. since the same pod is run twice on node three. Similarly, there's something called pod affinity. It could be set for the green and blue pods to ensure they are run on the same node. A typical use case for this scenario is a lot of network communication between those two pods. And in doing so, you can reduce inter-node traffic, and thus reduce the latency in these network communications. There's also a concept called node affinity, which lets you pin a pod to a specific type of node. For example, if you have some software that uses GPUs for computation, you could have a few expensive nodes with GPU's and use node affinity to ensure only software is run there that needs to run on this GPU nodes. And lastly, let's look into the pods. They can run containers. As mentioned earlier, they run at least one. It's important to remember that all containers within a pod run on the same node.


Now that that's settled,

what are the key concepts that you as a developer, engineer or DevOps person are exposed to?

Generally speaking, in Kubernetes everything is a resource. You've seen one of them already; pods. A resource consists of a kind, which defines what type of resource you're talking about, and each kind also exists in at least one version. And each resource has some metadata attached, such as its name, a unique identifier, creation timestamp, labels, annotations, and a lot more. Resources can exist, either cluster wise or they exist within the namespace. Namespaces are cluster wise resources as well. They are cluster-scoped, which means each namespace can only exist once in a cluster. But pods are namespace-scoped, so each pod exists once in a namespace. But within different namespaces, the pod can exist more than once. You could, for example, create namespaces for development, staging, and production, and then have other resources within those namespaces.

After all this theory, let's look at some basic resources and their code and how to use them. I won't be able to give a finite list of these building blocks because I'd never finished writing that list, it's endless. I'll limit myself to some central ones, but rest assured there's a ton more.

1. Namespace



I've already mentioned namespaces. They are a key resource because other resources exist on either a cluster level or a namespace level. And what you can see here on the left is a YAML definition of what a namespace resource could look like. The schema is very much the same for all the other resources. There's an API version, there's the kind and some metadata, such as the name.

You can also create new or update existing resources, The Apply command, for example, updates in existing resource, or creates a new one if it doesn't exist yet. And you provide it with a file. So if you place the content on the left, in the file called K8s-namespace.yaml, and use kubectl apply, Kubernetes will create the namespace, which we can see in the list of namespaces once we run kubectl get namespaces again.



2. Pods

As already mentioned, there are pods. In our example, we use a container image, traefik/whoami and named the container, whoami. Kubernetes uses the container name to uniquely identify the container within a pod. You also define the pod that the container exposes and give it name, http. And lastly, we put some labels on the pod. We'll use them later to tell Kubernetes where to run network traffic to. Similarly to the namespace, you can use kubectl get pods, to get a list of all pods. However, unless you specify the namespace using -n, Kubernetes is going to use the default namespace. Also, did you notice that we defined the namespace in the resource? Well, because of that, we don't need to provide it when we apply the YAML file using kubectl. However, it's still good practice.



3. Services

Now that our pod is running, how do we access it? Well, for that, we need to do some network routing. No worries. You don't need to fiddle around with IP addresses, net masks and such. The network routing on the level that we are concerned with here happens through a resource type called services. Services bundle traffic on an incoming IP and pod and route it to one or more pods, which in turn route it to the corresponding containers. Using the selector, Kubernetes looks up all pods in the same namespace that have the given label. If they define a pod, http, the service is going to include them in its routing, which means you can scale out your service by deploying more pods, we just saw. As long as the label and pod match, they are automatically picked up, which makes scaling out or scaling horizontally very, very easy.



4. Ingress

Now, since they're using http here, you also probably want to use something called an Ingress definition. Compared to two/three-pod deployments, this ingress definition is a reverse proxy. In the example here, we'll be using Nginx, that's the de facto standard in the Kubernetes world, but there's heaps of others as well, for example, Traefik.

In recent Kubernetes versions, the Ingress resource has become a bit more complex, but you'll get used to them eventually. When the http requests host is example.com, we'll redirect everything underneath a single forward slash to our whoami service on port http, so port 8080. In the case of a two layered application where your front end is running in a different container than your backend, you could, for example, send each request that starts /API, to your backend servers and everything else to your front end servers.



5. Configmaps and secrets

A concept that has been around for a very long time is the Twelve-Factor App. This means among 11 other factors, that applications should be configured using environment variables. And we can do that in Kubernetes as well. We can either hard coat configuration options in pod definitions or using ConfigMaps or Secrets, which we can then reuse in different pods. There are a few differences between ConfigMaps and Secrets, but I'm not going into the difference here. That's beyond the scope of this talk.

Let's just say the latter is where you store secrets, like the secret key and the database URL. ConfigMaps contain a value in its clear text. The secrets in the secrets file are typically Base64 encoded. So they are not encrypted, just encoded, so they're still readable.



6. Deployments

The last Kubernetes resource we are going to look at is the Deployment. It's a way to tell Kubernetes to run pods using a given template, a certain number of times. What you find here on the right hand side is the replicas which is sector three in the example. Kubernetes will run three identical pods. We also see a template that defines how a pod should look like. Some exceptions aside, you can put everything within the template that's available to the pod resource. What you can also see here are two ways to define environment variables in a pod. We define an environment variable, some secret, with a value that is spread from a Kubernetes secret and we load the entire ConfigMap as environment variables. Every entry, every key in the ConfigMap, will be the name of an environment variable and the corresponding value, the corresponding value and the environment variable. Once applied, you'll see how Kubernetes spins up three additional pods. Their names are somewhat randomly generated. And when you change something in the deployment resource, Kubernetes is going to start new pods. By default one pod at a time, Kubernetes is starting a new one, waiting for it to be available, and then terminating an old one, until all pods are gone.



Kubernetes and Django

Well, now that the basics are clear, what do we need to do for Django to work with Kubernetes? Turns out it's only a few things and only one of them is specific to Kubernetes. Django requires you to set a list of allowed hosts. We do that using a ConfigMap.



The first host in this list is probably clear. It's our domain that we want our app to be available on. The second value in this allowed hosts is a Kubernetes' internal domain name. Using the service name dot namespace name allows you to talk to any service within the cluster, which is pretty neat.



Looking at the settings. I've included the ones that are relevant for the deployment in our case. Django will fail to start when no secret key is provided, which is a good thing, because then you require and can ensure that you have actually set a secret key and not accidentally used a secret key that you used for testing. Turning off debug by default is the same good idea. Turn it on when you need it, but have it set to false by default. So by setting an environment variable called 'debug' to the value, true, the lowercase string, true, we turn on Debug. The allowed hosts are split at a comma as mentioned earlier. And the database settings are retrieved using the wonderful library, DJ-Database-URL.



In our deployment, we're gonna refer to the ConfigMaps and Secrets in the entirety, and not only key by key. In doing so, we can far more easily reuse the ConfigMaps and Secrets in other deployments or in other container definitions. Such as when we want to run Celery, we need another pod or at least another container, and maybe probably we need the same settings.



And then there's a bit of a Docker file. I use an entry point script, which will decode and define the startup process of the container. The default command is gunicorn though. The entry point on the other hand does four things. Firstly, it checks if it can connect to the database. In the Postgres world, there's a PG ready command, but that requires specific environment variables to be set. Using Django's DB shell is much easier because you can just rely on Django's settings. Secondly, it applies to all migrations. This can be quite tricky, but proper testing and carefully considering the implications of migrations between two releases make it possible. Thirdly, we are collecting static files. And lastly, we run the command passed into the container, so by default gunicorn. But if you want to jump into a Django management command or Django shell, you can pass in Django admin shell, for example. And that's it.

We started with the question, what is Kubernetes? And what can I say? This is Kubernetes.
 
Thank you for taking your time and making it to the end.

Leave a feedback below.

Post a Comment

Let's make it better!
Comment your thoughts...

Whatsapp Button works on Mobile Device only

Start typing and press Enter to search