Kubernetes: an Introduction and Starting Project

John MacLean
11 min readMay 29, 2023

--

Photo by Allef Vinicius on Unsplash

Introduction

Kubernetes is a highly sought-after open-source platform that specializes in the orchestration and management of containerized applications. The genius of Kubernetes lies in its ability to automate the deployment, scaling, and operational tasks of containers, thereby enhancing the efficiency and reliability of software systems. In a nutshell, Kubernetes clusters containers which constitute an application into logical units for seamless and effective management.

On the other hand, Docker is another influential player in the field of software development and deployment. Docker’s primary contribution is its platform that aids developers in creating, deploying, and running applications with the help of containerization. A Docker container packages an application with everything it needs to run, such as libraries, system tools, and code, thereby ensuring that the application works quickly and reliably from one computing environment to another.

While both Kubernetes and Docker have made significant strides in the realm of containerization, they are not directly comparable since they operate at different levels of the stack, but complement each other in creating a full-fledged container infrastructure. Docker shines in creating and managing individual containers. Kubernetes, however, takes container management a notch higher by providing a framework for running distributed systems resiliently, managing and orchestrating Docker containers in a multi-node setup.

In the context of this article, we ventured on a journey involving Kubernetes and Docker, where the task required deploying multiple services, exposing them through a single service, and making them accessible via local port forwarding. We utilized Kubernetes ConfigMaps and Deployments, established a Service to expose these Deployments, and proceeded to troubleshoot a series of connection and configuration challenges.

Here are the Kubernetes terms we encountered in this journey:

  • Kubernetes: An open-source platform for managing containerized applications across multiple hosts.
  • Docker: A platform that automates the deployment, scaling, and management of applications within containers.
  • Container: A lightweight, standalone, executable package containing everything needed to run a piece of software.
  • Pod: The smallest unit in Kubernetes that represents a single instance of a running process in a cluster.
  • Deployment: A Kubernetes object that maintains a set of identical Pods, ensuring that they have the correct config and that the right number of them exist.
  • Service: A Kubernetes abstraction which defines a logical set of Pods and a policy by which to access them.
  • ConfigMap: An object used to store non-confidential data in key-value pairs.
  • NodePort: A feature that exposes a service to external traffic by making it available on each Node’s IP at a static port.
  • LoadBalancer: A type of Kubernetes service which can automatically provision an external load balancer in supported environments. It distributes incoming application traffic across multiple Pods, improving the distribution of workloads, enhancing application availability and resilience.
  • kubectl: A command-line interface for running commands against Kubernetes clusters.

Pre-requisites

  1. Install Docker Desktop. Docker Desktop can be installed by visiting the official Docker website and downloading the relevant installer for your operating system (Windows, macOS). Follow the prompts to complete the installation.
  2. Enable Kubernetes. After Docker Desktop has been installed, you can enable Kubernetes by opening Docker Desktop, clicking the Settings button, selecting the Kubernetes menu and checking the box that says Enable Kubernetes. If you have an issue with Kubernetes not starting, check this article which helped me out.
  3. Install kubectl. kubectl is a command line tool for interacting with a Kubernetes cluster. It's included with Docker Desktop, but if you need to install it separately, you can follow the instructions on the Kubernetes website.
  4. Install a text editor if you don’t have one. You’ll need a text editor to create and modify your deployment YAML files and your custom index.html pages. This could be something as simple as Notepad, or you could use a more advanced editor like Visual Studio Code.
  5. A development environment. This time I am just using Windows PowerShell with WSL 2 — although please note I had to revert back to WSL 1 — see the Testing section for the reason why!

The Challenge!

Foundational stage:

Spin up two deployments. One deployment contains 2 pods running the nginx image.

Include a ConfigMap that points to a custom index.html page that contains the line “This is Deployment One”. The other deployment contains 2 pods running the nginx image. Include a ConfigMap that points to a custom index.html page that contains the line “This is Deployment Two”.

Create a service that points to both deployments. You should be able to access both deployments using the same IP address and port number.

Use the curl command to validate that you eventually see the index.html pages from both Deployment 1 and Deployment 2.

Example: curl service_ip_address:service_port

Note that I may follow up with the Advanced and Complex stages of this challenge in future articles, so watch out for those!

Preparation

We need 5 files to complete this challenge:

  • 2 custom .html files. I have just created a very simple .html file for each deployment that displays a nice background and a basic message indicating which deployment the file belongs to.
  • 2 Deployment configuration YAML files. Again, one for each deployment.

A Deployment YAML file serves as a set of instructions to Kubernetes. In our case, this file describes an application based on the Nginx web server and dictates how it should be set up and run. The file specifies the image to use (Nginx), the number of instances to run (called replicas), and other configurations like volumes for storage.

Importantly, it also details how to incorporate a ConfigMap, a Kubernetes feature that stores non-confidential data in key-value pairs. Here, we use the ConfigMap to supply a custom webpage to our Nginx server. By applying this Deployment file, we instruct Kubernetes to create instances of our application as defined, effectively automating the setup and deployment process.

apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-one
spec:
replicas: 2
selector:
matchLabels:
app: multi-deployment
template:
metadata:
labels:
app: multi-deployment
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html/index.html
subPath: index.html
readOnly: true
volumes:
- name: html
configMap:
name: deployment-one-config

Here is the code for our deployment-one-config.yml.

deployment-two-config.yml is exactly the same, except every occurrence of one is replaced by two.

  • 1 Service configuration YAML file. Our service should use a selector that matches the labels for both deployments. This is a bit tricky, because by default services can only route to pods with a single set of matching labels. A common workaround is to use a single label that both deployments share, or to use multiple services. Here, we are using the former option.
apiVersion: v1
kind: Service
metadata:
name: multi-deployment-service
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
selector:
multi-deployment: "true"

Our service.yml file is like a phonebook entry that directs incoming traffic to our application’s specific pods. Here’s a simplified breakdown:

  • apiVersion and kind. These are standard Kubernetes identifiers to specify we’re creating a Service object.
  • metadata. This section includes the name of our service, acting as the identifier within the Kubernetes system.
  • spec. This is where we provide specifics about the service. In this case, we’re creating a LoadBalancer type of service that can distribute network traffic to multiple pods.
  • ports. Here we define the port information. The port is the port number that will be exposed externally by the service, and targetPort is the port on the pod to which the service will route the traffic.
  • selector. This is a crucial part. We’re specifying that the service should direct traffic to any pod with the label multi-deployment set to true. This is how we “connect” the service with our application’s pods.

By structuring our service.yml this way, we’re ensuring that traffic can find and reach our application’s pods, even as they’re created and destroyed during the normal course of operation.

Setting Up Our Environment

Make sure we have all our files copied into the same directory on our local workstation…

…then change into that directory in our terminal window.

kubectl create configmap deployment-one-config --from-file=index.html=./deployment1.html
kubectl create configmap deployment-two-config --from-file=index.html=./deployment2.html

Next we create our config maps.

Note I had to structure the commands to cater for the fact I gave my .html files custom names. Yet we need to ensure they are named index.html when in our pods so that the pages will be recognised and displayed correctly by Nginx.

Then we need to apply our deployments…

kubectl apply -f deployment-one-config.yml
kubectl apply -f deployment-two-config.yml

…which was successful.

Finally, we need to apply our service…

kubectl apply -f service.yml

…which was also created successfully.

Let’s just check everything is up and running before doing our final tests.

kubectl get svc will show us the IP address and port of our service.

kubectl get deployments confirms our deployments are up and available.

Testing

Ok, we should just need to run a curl command against our service and its port and we should be directed to one of our custom webpages:

Hmm, ok. Well, it appears that since I’m running curl in PowerShell, I have to format the command slightly differently to Linux. Let’s try again…

…and oh dear. More troubleshooting required! One day, these challenges will be straightforward I’m sure.

And at this point dear reader, I disappeared down a troubleshooting rabbit hole for many, many, many hours.

Yes, the eagle eyed amongst you will have spotted that I changed port numbers between screenshots. I tried resetting and redeploying my environment, I tried curling port 80, curling the IPv6 notation for localhost before things got weird with adding firewall rules and port forwarding on my local internet security software. Even turning off my firewall didn’t resolve the issue.

Eventually I found a thread on GitHub discussing how WSL 2 cannot connect to localhost.

After trying multiple solutions, what seemed to resolve the issue for me was to revert WSL to version 1 and to set version 1 to be the default version of WSL. This just takes two commands in PowerShell which can be found at the official Microsoft WSL Learn site.

WSL 2 and WSL 1 seem to handle network traffic completely differently which is the crux of the problem.

The only oddity now was that I couldn’t access my website by curling to localhost on my service port.

Curling to localhost:80 worked however. I still can’t figure out why this is the case. If anyone has any ideas, please let me know!

Refreshing a number of times would eventually display the other site, which I gave a different charming background. So we verified the load balancer worked.

Really, that was all that was required to complete the task, but I wasn’t really satisfied that things didn’t go quite as expected, so I decided to try a different configuration.

Testing using NodePort instead of a LoadBalancer

So I checked out my fellow Level Up In Tech Gold cohort Melvin Groom’s article on the same topic and noticed he had used a NodePort instead of a LoadBalancer. As a final throw of the dice, I decided to do the same.

One tip I can offer for troubleshooting is to use the kubectl describe service <service name> command. It offers a lot of good information about your service.

Here is my service before I changed anything. As we can see, Type is set to LoadBalancer.

apiVersion: v1
kind: Service
metadata:
name: multi-deployment-service
spec:
type: NodePort
selector:
app: multi-deployment
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080

To change to NodePort, I needed a different service.yml file. As we can see, the type is set to NodePort and we define that the nodePort will use a set port in the range 30000–32767 (because these are designated as User Ports by the Internet Assigned Numbers Authority (IANA). By using this range we shouldn’t conflict with any system ports or well-known ports used by system processes).

I set it to 30080 for no reason in particular!

The great thing about Kubernetes is that in order to change my deployment, all I need to do is apply the new service YAML file. I don’t need to rearchitect, reconfigure or change anything else.

So doing that and re-running the kubectl describe service command again shows the Type as NodePort and confirms that we will have exposed the service on static port 30080 on each Node in our cluster.

This time when I run curl http://localhost:30080 in the terminal, it correctly returns our website information.

Our webpage is also displayed correctly using localhost:30080 in our browser.

Again, if we refresh a few times, we’ll get the other site proving our NodePort configuration is working.

Conclusion

In conclusion, Kubernetes serves as a powerful tool in the arsenal of developers and IT teams, offering an intricate system to deploy, scale, and manage containerized applications like the ones you can create with Docker. With its services and deployments, as illustrated in our task, it is possible to maintain, load balance, and route traffic to different instances of an application based on the desired configuration.

However, as powerful as it is, Kubernetes is not without its intricacies and potential roadblocks. The task we worked through highlighted some of these challenges — from the careful crafting of deployment and service YAML files, to the intricate debugging required when faced with error messages. Yet, overcoming these obstacles and understanding the nuances of Kubernetes leads to an efficient, scalable, and reliable system.

As with any sophisticated technology, there’s always more to learn about Kubernetes, and its ongoing development promises even more robust and intuitive features for managing and orchestrating containers. As we continue to explore and embrace these cloud technologies, we’re navigating toward a future of greater automation, scalability, and overall operational efficiency in software development.

As ever, thank you for reading and I welcome any feedback.

--

--