In the past two months, I’ve been working quite extensively with Kubernetes to deploy and run the prototype of a SaaS product that I’ve been building while in lock-down. In this time I’ve gone from an almost complete neophyte, to, well let’s just say, having a passing familiarity with the software. It’s also something that I think with which every programmer should come to grips. Forget about deploying servers or shoving jar files or zip bundles into runtime platforms, even on so-called ‘serverless’ architecture. Deploy your server side software on Kubernetes.
In this article I’m going to cover the core concepts inside Kubernetes. I found, for me at least, that during that first week or so of trying to piece things together, in order to work out how to deliver my ‘hello world’ service into an actual running component, I had to fit together these concepts to get a picture of what was going on, and what could go wrong, and what I needed to do next. The Kubernetes documentation does a good job of walking you through all the steps necessary to create, update, and delete the various components, so I won’t cover that in detail. Instead, I’m going to talk about the core conceptual components of the system that the programmer needs to understand, so that they can deploy their software components in Kubernetes.
So, for anyone unfamiliar with it, what is Kubernetes (also abbreviated to k8s)? The Kubernetes website has the best description:
Kubernetes (K8s) is an open-source system for automating deployment, scaling, and management of containerized applications.
It groups containers that make up an application into logical units for easy management and discovery. Kubernetes builds upon 15 years of experience of running production workloads at Google, combined with best-of-breed ideas and practices from the community.
In effect, it’s how you deliver a service architecture for the internet. When you hear the word containerization, in terms of running software for the internet, that nearly always means Kubernetes, at least in practice (although not exclusively). You could do some basic containerization with just Docker, and little else. However, while Docker will take an individual application, or some code that you wrote, and run it as a container in the Docker runtime, it lacks, on its own, the means to understand, configure, and control how that component interacts with other components in your system. This is the essential part Kubernetes adds to this mix.
First, to understand the evolution of Kubernetes, you should read this page in the documentation. This shows the evolution of software deployment models, from deploying multiple applications directly onto an operating system running on hardware, to virtualization of operating system instances, to containerization of application instances.
At the high level, Kubernetes consists of a control plane, and some nodes that run the workloads. The control plane is really just another node or nodes in the cluster, one which just happens to contain the components that control the global state of the cluster. While this job can be spread out among the ‘worker nodes’, normally there are one or more dedicated nodes in the cluster which run only control plane components. Your cloud provider may even completely abstract control plane nodes away from you, so you don’t see the control plane as a node at all, or even have any idea how many nodes your control plane runs on.
Nevertheless, it’s very helpful to know what the control plane does. The control plane components consist of the following:
kube-api-server- the main API service you interact with to change the state of the cluster. The nodes also get and take their information from this service.
etcd- (pronounce it et cetera dee or just e.t.c.d) the key-value store which is the database containing the information about the cluster’s state.
kube-scheduler- this component controls which pods run on which nodes (the pod is scheduled onto a node).
kube-controller-manager- this component is a composite of all the
controllercomponents, which include the
endpoint-controller, as well as controllers for service accounts and token access to the API.
cloud-controller-manager- this is a cloud provider specific component which interfaces to that cloud provider’s layers in order that Kubernetes can do things like control nodes (e.g. on AWS EC2 or GCP cloud-compute instances) and route network traffic.
Typically, you don’t interact with these components directly. You’re either using a console, or the Kubernetes command-line component
kubectl (pronounce it kube-control), to interact with
kube-api-server. Other configuration components that you might use to drive automated deployment and configuration processes, like Helm, also interact with the API on your behalf.
On the node, it’s a little simpler. There are three main components:
kubelet- It ensures the pods specified to be scheduled on the node are in fact scheduled and actually running on the node.
kube-proxy- It provides a network proxy and other networking services for the node.
kube-schedulerand controlled by the
kubelet. This is nearly always Docker, but other container instances are possible.
There are other components, which Kubernetes calls addons. You will generally see these running inside your node using a special namespace called
kube-system. These almost always include a DNS implementation, as well as logging, and sometimes components like the Kubernetes Dashboard, and resource monitoring components.
From a programmer’s point of view, these components, on the control plane and the node, are typically run and defined by the ops parts of a DevOps organisation (for example, system reliability engineers).
Ok, so there’s plenty of information in general about each of the different Kubernetes ‘objects’. The best source of information I think is the Kubernetes documentation itself. Unlike a lot of other open source projects that have documentation of highly variable quality (*cough*Apachecough), the Kubernetes site is pretty well written and structured. Despite that, I found the first challenge was fitting all these individual pieces together into a coherent whole. Once I could conceptually position each of the various configuration objects into its correct slot, I found my understanding of what I needed to do next gained greatly. That was the point at which I no longer needed to blindly copy what the documentation told me, but I could then really start designing what I needed to do, anticipating what knowledge gaps I had to fill, and constructing useful questions to ask, or search terms to use, in order to get the most benefit in the shortest possible time.
This is the knowledge I’m aiming to record in this article. Hopefully you will find it helpful. Let’s start at the very bottom.
Pod is the smallest independent Kubernetes object. A Pod will run one or more
Container objects. The Container objects are exactly what you’d expect them to be, if you’re familiar with Docker already: a docker image.
Notice I said one or more containers are in a Pod. There’s an important caveat to this which is critical to understand. In Kubernetes, it’s the Pod which is assigned the IP address and hostname in the k8s DNS. All containers which run in that pod have the same IP address, share the same set of TCP ports, and can see each other on
You don’t put two unrelated containers into the same pod (e.g. a service, and a database, do not belong inside the one Pod). If you are doing the simplest of architectures, it is almost certain that you are putting only one container inside each Pod! However, containers may be co-located in the same Pod, if you’ve got a good architectural reason. These are typically reasons such as:
You need one of more init containers which run before the Pod’s main container. For example, the init container may ensure certain essential services are available to the Pod, blocking its execution until they are, or perform some initial setup (e.g. create the database table structures).
The two containers co-operate closely in a way that doesn’t make sense to separate them. For example, one scenario I encountered recently was dynamic cluster discovery for ActiveMQ instances. ActiveMQ can take a static configuration with a complete list of hosts in the cluster (more or less impossible in a dynamic environment like Kubernetes!), or uses UDP Multicast for broadcast discovery (not impossible, but hard and fiddly to make work in a Kubernetes cluster without additional network layer software). So one way to achieve this would be to create a second container alongside the ActiveMQ container inside the pod. This container would query the Kubernetes API looking for other ActiveMQ instances, and dynamically update the ActiveMQ containers configuration.
One of the containers is a ‘side-car’ container performing some essential infrastructural duty for the other container, such as authentication and authorisation of all service calls in and out of the Pod.
It’s also very important to know that while you can technically create a
mypod.yaml with a bare Pod spec inside it, normally you do not (I never have). Running such a Pod spec is much like launching a container with
docker run. You will get exactly one instance of the desired object running and when it exits (either because you stop it, it completes its task, or it fails), it will not restart. To get that you will need a Controller.
Controllers are the objects that you work with the most in Kubernetes, if you are a programmer. Their
spec (specification) will include the details of the Pod they control (in
spec.template, which in turn contains the
spec of the Pod as well). The way to think about controllers is that the objects you specify (e.g. a
Deployment) specifies the desired state of some objects in your cluster, and a corresponding
Controller service (in this case the
DeploymentController) works with the Kubernetes API to ensure that the cluster is in the state specified.
There are different types of controllers, each gives a different type of control over the pod. One way to think about it is that each controller creates a different lifecycle for their pods.
The following sections cover the most common types that you may need.
ReplicaSet creates and maintains a specified number of (identical) Pods in the cluster. Normally, like Pods themselves, you will not create this object directly, and instead use a Deployment to create and manage the ReplicaSet.
Deployment creates and maintains a ReplicaSet, along with the specification of the Pod the ReplicaSet controls. As well as changing the number of Pods in the ReplicaSet, a Deployment controls the revision and other configuration of the Pod. Deployments have many other features, such as the ability to be scaled by a HorizontalPodScaling controller. This controller creates and destroys instances depending on the CPU load and memory usage measured in the pods.
Deployment is the single most important k8s object specification that a programmer normally creates and maintains. For that reason, makes sure you are intimately familiar with the Deployment API (i.e. the
.yaml format of the object specification).
StatefulSet creates and maintains a set of (identically) specified Pods. However, unlike a Deployment, which will terminate failed Pods and simply create new instances from new to replace the old instances, a StatefulSet creates and maintains a stable number of Pods in a guaranteed order and identity.
For example, if your StatefulSet is called
server, and you specify three instances, you will get instances named
server-2. They are also created in that order. If you then change your spec to only have two instances, it is
server-2 that will be terminated. If you then change the spec to have four instances,
server-2 will be recreated (using the same persistent storage as before), and a new instance,
server-3, is created after
server-2 has started.
One important current caveat is that you must also create a Headless Service to go along with the StatefulSet.
I’m using StatefulSets to create and run Apache Cassandra databases which are used for Akka Persistence. Using the StatefulSet to do this allows me to keep the persistent storage for each instance intact over multiple create/destroy cycles of the Pods, and give both Cassandra and the Akka clients known contact points for cluster topology and discovery, regardless of the total number of nodes in the Cassandra cluster.
DaemonSet makes sure all the specified Nodes run a copy of the Pod. You might use this controller for something like a storage object, a common cache service shared by most or all of your pods, or a logging or monitoring service. It will ensure that a Pod always running on every Node, as close to the other services that use it as possible. Lots of system monitoring and security processes use this type of Controller.
Job creates one (or more) Pods which are then run to completion. When the Pod completes, it is not restarted (if the Pod fails however, a new Pod will be started to replace it). You might use this object to run a reporting process, or a database or other system maintenance process.
Extending this concept, a
CronJob creates a Job on a repeating schedule. If you know what a Unix
cronjob is you already get the idea.
That’s it for the moment. In my next article I’ll touch on the networking objects like
Ingress and how you use these to create and expose your services to other Pods in your cluster, or to the world in general.