DEV Community

Arsh Sharma
Arsh Sharma

Posted on

Learning To Use Kubernetes

In this post, we are going to get our hands dirty and actually use Kubernetes. We'll take a sample app and I'll show you how to run that on a Kubernetes Cluster. For this, I recommend you read the previous post where I talked a bit about the architecture of Kubernetes.

Understanding Kubernetes can get confusing because of the sheer amount of options you have. To avoid that I've stuck to a very simple app and tried to keep the language of the article extremely simple so that you can understand all the basic concepts. After that to get a deeper understanding it is recommended that you do go and check out the official docs.

The Basics

Let us take a simple Node.js app (you don't need to know Node.js to follow through):

const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.send(`
    <h1>Hello World!</h1>
  `);
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

The Dockerfile to create the image for this is also fairly simple and would look like something like this:

FROM node

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "node", "app.js" ]
Enter fullscreen mode Exit fullscreen mode

Now I'll show you how you can run this app on a Kubernetes cluster and access it. To do that we'll need the following two things:

  1. minikube: Traditionally our cluster would have nodes running on multiple machines but for experimenting, we would run everything on our local machine. Minikube will basically act as our cluster. And this cluster will have only one node. You can understand this better by thinking of both the master and the worker node running on a single machine (your local machine).

  2. kubectl: This is a command-line tool we use to send instructions to our cluster. With this tool we can send instructions to the master node which will then do whatever is necessary, to execute our instructions, with the worker nodes.

Okay, so we have our cluster on which we want our Node.js app to run. We don't have to worry much about how the cluster is internally working because k8s will take care of that for us. And we also don't have to worry about setting up this cluster properly since we are using minikube and that will do everything for us.

  1. minikube start will start our cluster.
  2. minkube stop will stop it.

We are going to focus on understanding the commands which we will send to the cluster (using kubectl). Now there are two ways here. This is kind of like the case with docker and docker-compose. There is the imperative approach where we manually type the commands each time (like we did with docker) and there is the declarative approach in which we list down everything we want in a yaml file and everything is taken care of automatically (like we did with docker-compose).

Once you understand the imperative approach, you'll understand the declarative one intuitively. So that's why let's first look at the imperative one.

But before we begin with that there are a few concepts I feel I should explain earlier so that understanding both the approaches is easier.

Introduction To Objects

Kubernetes doesn't work with containers directly. Instead, it works with things called "objects". If you do a simple google search for what Kubernetes objects are you'll most likely get this definition:

"Kubernetes Objects are persistent entities in the cluster."

Doesn't make much sense, does it? :P
Don't worry it didn't make sense to me too at first. I'm going to have to ask you to bear with me for some time and let go of that itch of understanding what exactly an object is. Everything will become a lot more clear by the time you finish reading this and see objects in action but for now, let's just remember that K8s works with objects.

Three objects we will be looking at in this article are:

  1. The Pod object.
  2. The Deployment object.
  3. The Service object.

We create objects using commands. These commands are either given in the imperative way or in the declarative way. Once we create the object k8s will take this create object and then will do stuff based on the instructions encoded in that object. And that is all you need to understand for now.

We Create An Object -> K8s Does "Something" Based On The Instructions In That Object.

Another thing I want to clear out beforehand is that you might think that we will be creating pod objects. That is the logical assumption since in the last post I told you that pods are basically wrappers for our containers and since the app runs inside the container it only makes sense that we create pod objects to run the app, no?

mp468phxmpt51

We typically don't create the pod objects on our own and manually move them to some worker node. Instead, we create a deployment object to which we then give instructions about the number of pods and containers it should create and manage for us.

TLDR

All of this might seem a bit confusing right now but just keep in mind these basic things and stuff will become a lot more clear once you see it in action:

  1. minikube will take care of setting up our cluster.
  2. We'll use kubectl to talk to our cluster.
  3. K8s works with objects.
  4. There are two way of creating objects: Imperative (like using docker from the command line) and Declarative (like using docker-compose)
  5. We'll create deployment objects and these deployment objects will then take care of the pod objects that need to be created in order to run our app.

With all this out of the way, let's finally begin :)

The Imperative way

First things first let's run out cluster using

minikube start

Now since we are dealing with containers at the end of the day, we need to build an image from our dockerfile. This can be done simply using this command:

docker build -t node-image .
Enter fullscreen mode Exit fullscreen mode

Make sure you are in the directory containing the dockerfile. node-image is simply the tag I've chosen to give to this image.

Now you must understand that our cluster is independent from the local machine. We issue instructions to the cluster from the local machine using kubectl. So we won't be able to send our image via the local machine. That is why we upload the image we just created to docker hub. Once we've done that we're good to go.

Now what we are going to do is create a deployment object. To do this use the following command:

kubectl create deployment node-app --image=YourDockerHubName/node-image
Enter fullscreen mode Exit fullscreen mode

Here node-app is the name I chose to give to this deployment. The image specified is the one that should be used for the container of the pod created by this deployment. The created object is automatically sent to the Kubernetes cluster and then the cluster looks for the image on Docker Hub.

Wait for a few seconds and then run

kubectl get deployments
Enter fullscreen mode Exit fullscreen mode

You'll see that we've been successful. We can also see that the pod created by this deployment is up and running using this command:

kubectl get pods
Enter fullscreen mode Exit fullscreen mode

Also if you wish to delete this deployment the command for that would be:

kubectl delete deployment node-app
Enter fullscreen mode Exit fullscreen mode

Now that we have a pod up and running the obvious question is how do we see our app in action?

Pods do have an internal IP address by default but we can't use it to access the pod from outside the cluster. Also, this IP address changes whenever a pod is replaced, which happens more frequently than you'd imagine since k8s manages the pods for us based on various factors.

So to reach a pod (and hence the container running in the pod) we need a service object. This service object is responsible for exposing pods to other pods in the cluster and to users outside of the cluster. The latter fits our current use case perfectly.

Although a little technical I would like to briefly tell about how a service object actually works. Service groups pods together and gives them a shared IP address which doesn't change. We can then also tell the service to expose this address outside the cluster.

So let's create a service!

You'd think we would do something like kubectl create service .. but no. There is in fact a much simpler command to expose the pod created by a deployment, that being kubectl expose ... This command exposes pods created by a deployment by creating such a service. Simply run the following to do so:

kubectl expose deploytment node-app --type=LoadBalancer --port=3000
Enter fullscreen mode Exit fullscreen mode

Here two things need to be explained:

  1. --type=LoadBalancer: --type is used to specify the type of service we want to create. LoadBalancer type utilizes a Load Balancer and then this Load Balancer will not only generate a unique address for this service (which we can use to finally interact with our app) but will also evenly distribute incoming traffic across all pods which are part of this service (though that isn't of much relevance here since we're experimenting locally).

  2. --port=3000: Here we simply mention the port which we chose in the code of our Node.js app.

With this our service is now created. You can run

kubectl get services
Enter fullscreen mode Exit fullscreen mode

to see the service we just created listed there.

There would be an additional service which we didn't create already present. No need to be alarmed by that :)

But again you might ask: HOW DO WE SEE OUR APP IN ACTION?!

Well, if we had created this service on a cloud provider we would see an external IP (after running kubectl get services) that we could use. But since we didn't do that we use a special minikube command:

minikube service node-app
Enter fullscreen mode Exit fullscreen mode

Run this and it should automatically take you to a browser window where you would see "Hello World!" from the Node.js server written.

And WOOHOO! This was it. You've successfully gotten your hands dirty with K8s now! ;)

This was it for the imperative approach. Let us now have a look at the declarative approach. Before doing that make sure to delete the service and deployment we just created using:

kubectl delete service node-app
kubectl delete deployment node-app
Enter fullscreen mode Exit fullscreen mode

The Declarative Way

Now that we have the hard part of understanding what is happening covered, life is much easier from here. In the declarative way we'll simply be creating two files:

  1. deployment.yaml - for our deployment object
  2. service.yaml - for our service object.

After that we'll be "applying" these files using:

kubectl apply -f=deployment.yaml -f=service.yaml
Enter fullscreen mode Exit fullscreen mode

Make sure to run this command in the directory where these files are present.

After this simply running:

minikube service node-app-service
Enter fullscreen mode Exit fullscreen mode

will take us to the browser where we would see the app in action.

Now that you know the flow what simply remains is having a look at the two yaml files. Most of what is written in these files should feel intuitive after our discussion of the imperative method above. I'll show you the files and explain certain points but the majority of it just has to do with the syntax which you can always look up in the official documentation. So let's now have a look at the files one by one:

deployment.yaml:

apiVersion: apps/v1 
kind: Deployment 
metadata:
  name: node-app-deployment
spec:
  replicas: 1 
  selector: 
    matchLabels: 
      anything: node-app 
  template: 
    metadata:
      labels:
        anything: node-app
    spec: 
      containers:
        - name: node-app-container 
          image: YourDockerHubName/node-image
Enter fullscreen mode Exit fullscreen mode
  1. First we specify which version of the Kubernetes API we're using to create this object: apps/v1. You can always see which one to pick from the docs.
  2. Then we tell what kind of object this file is for.
  3. We also provide the name under metadata.
  4. spec will have the core chunk of what we want to describe. Here we describe the specification of the deployment.
  5. Replicas mentions how many default pods we want our deployment to create. If not mentioned then the default value is 1.
  6. Under selectors we use matchLabels and then specify the labels which we would be mentioning below when we define the metadata for our pods. Here the key and value are both arbitrary values. In the example above, I have chosen the key to be anything and the value to be node-app. You could choose something else too.
  7. After that we mention the template. Here we define the pods that should be created. This is similar to when we used --image in the imperative approach. Do note that the template of a deployment will always describe a pod so we don't need to mention explicitly anywhere that we're going to be describing a pod.
  8. The pod we specify will have metadata where we mention the labels. It is these labels that we specified above in the selector so that the deployment knows which pods it is to manage later on.
  9. We also mention the specification of the pod under spec and give details about the containers where we mention the image the container is to be based off on.

This was it for out deployment.yaml file. Now let's look at our service.yaml file:

apiVersion: v1 
kind: Service
metadata:
  name: node-app-service
spec:
  selector: 
    anything: node-app 
  ports:
    - protocol: "TCP"
      port: 80 
      targetPort: 3000
  type: LoadBalancer
Enter fullscreen mode Exit fullscreen mode
  1. Again we specify the API version(though a bit differently here). You can look at something like this to see which API version to choose or check the examples in the official docs.
  2. Then we specify the selector like we did before. Here we don't need to use matchLabels since it uses matchLabels by default.
  3. After that we simply mention the ports and type. The targetPort is the one we specified for our app in the code and the port is the outside port to which we want to expose. We could've picked a different protocol too but here I went with TCP.

And voila! Execute the commands I mentioned above and you should be good to go with the declarative way of creating objects!

Conclusion

This has probably been the longest post I've ever written simply because there was just so much to cover. If you feel overwhelmed try reading it in parts and understanding. I tried to aim for simplicity more than technical accuracy so that you would be able to make a better sense of what is happening. I again recommend you to read the official docs so that you have a more in depth knowledge about the working of K8s.

With that, I hope you were able to learn something out of this.
Thanks a ton for reading :)

If you have any feedback for me or just want to talk feel free to connect with me on Twitter. I'll be more than happy to hear from you! :D

Top comments (0)