Merikanto

一簫一劍平生意,負盡狂名十五年

Deploying and Scaling Microservices in Kubernetes

Kubernetes is an open-source container orchestration tool for managing containerized applications. In this post, we will build, deploy, and manage an end-to-end microservices application in Kubernetes. The sample web application you’ll use is a “todo list” application written in Node.js that uses MongoDB as a database.

You’ll build a container image for this app from a Dockerfile, push the image to Docker Hub, and then deploy it to your cluster. Then you’ll scale the app to meet increased demand.



Build Image with Dockerfile

We will begin by containerizing the web application by packaging it into a Docker image.

Start by changing to your home directory, then use Git to clone this post’s sample web application from its official repository on GitHub.

1
2
cd ~
git clone https://github.com/janakiramm/todo-app.git

Build the container image from the Dockerfile. Use the -t switch to tag the image with the registry username, image name, and an optional tag.

1
docker build -t merikanto/todo .

Verify that the image is created by running the docker images command.

1
docker images

Next, push your image to the public registry on Docker Hub. To do this, log in to your Docker Hub account:

1
docker login

Once you provide your credentials, tag your image using your Docker Hub username:

1
docker tag merikanto/todo-app

Then push your image to Docker Hub:

1
docker push

You can verify that the new image is available by searching Docker Hub in your web browser.

With the Docker image pushed to the registry, let’s package the application for Kubernetes.



Deploy MongoDB Pod in K8S

The application uses MongoDB to store to-do lists created through the web application. To run MongoDB in Kubernetes, we need to package it as a Pod. When we launch this Pod, it will run a single instance of MongoDB.

Create a new YAML file called db-pod.yaml:

1
vim db-pod.yaml

Add the following code which defines a Pod with one container based on MongoDB. We expose port 27017, the standard port used by MongoDB. Notice that the definition contains the labels name and app. We’ll use those labels to identify and configure specific Pods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# db-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: db
labels:
name: mongo
app: todoapp

spec:
containers:
- image: mongo
name: mongo
ports:
- name: mongo
containerPort: 27017

volumeMounts:
- name: mongo-storage
mountPath: /data/db

volumes:
- name: mongo-storage
hostPath:
path: /data/db

The data is stored in the volume called mongo-storage which is mapped to the /data/db location of the node. For more information on Volumes, refer to the official Kubernetes volumes documentation.

Run the following command to create a Pod.

1
kubectl create -f db-pod.yml

You’ll see this output:

1
2
# Output
pod "db" created

Now verify the creation of the Pod.

1
kubectl get pods

Let’s make this Pod accessible to the internal consumers of the cluster.

Create a new file called db-service.yaml that contains this code which defines the Service for MongoDB:

The Service discovers all the Pods in the same Namespace that match the Label with name: db. The selector section of the YAML file explicitly defines this association.

We specify that the Service is visible within the cluster through the declaration type: ClusterIP.

Save the file and exit the editor. Then use kubectl to submit it to the cluster.

1
kubectl create -f db-service.yml

You’ll see this output indicating the Service was created successfully:

1
2
# Output
service "db" created

Let’s get the port on which the Pod is available.

1
kubectl get services

You’ll see this output:

1
2
3
4
Output
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
db ClusterIP 10.109.114.243 <none> 27017/TCP 14s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 47m

From this output, you can see that the Service is available on port 27017. The web application can reach MongoDB through this service. When it uses the hostname db, the DNS service running within Kubernetes will resolve the address to the ClusterIP associated with the Service. This mechanism allows Pods to discover and communicate with each other.

With the database Pod and Service in place, let’s create a Pod for the web application.



Deploy Web App as a Pod

Let’s package the Docker image you created in the first step of this post as a Pod and deploy it to the cluster. This will act as the front-end web application layer accessible to end users.

Create a new YAML file called web-pod.yaml:

1
vim web-pod.yaml

Add the following code which defines a Pod with one container based on the merikanto/todo-app Docker image. It is exposed on port 3000 over the TCP protocol.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# web-pod.yaml
apiVersion: v1
kind: Pod

metadata:
name: web
labels:
name: web
app: todoapp

spec:
containers:
- image: merikanto/todo-app
name: myweb
ports:
- containerPort: 3000

Notice that the definition contains the labels name and app. A Service will use these labels to route inbound traffic to the appropriate ports.

Run the following command to create the Pod:

1
kubectl create -f web-pod.yaml

Let’s verify the creation of the Pod:

1
kubectl get pods

Notice that we have both the MongoDB database and web app running as Pods.

Now we will make the web Pod accessible to the public Internet.

Services expose a set of Pods either internally or externally. Let’s define a Service that makes the web Pod publicly available. We’ll expose it through a NodePort, a scheme that makes the Pod accessible through an arbitrary port opened on each Node of the cluster.

Create a new file called web-service.yaml that contains this code which defines the Service for the app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Service
metadata:
name: web
labels:
name: web
app: todoapp

spec:
selector:
name: web
type: NodePort
ports:
- name: http
port: 3000
targetPort: 3000
protocol: TCP

The Service discovers all the Pods in the same Namespace that match the Label with the name web. The selector section of the YAML file explicitly defines this association.

We specify that the Service is of type NodePort through the type: NodePort declaration.

Use kubectl to submit this to the cluster.

1
kubectl create -f web-service.yml

You’ll see this output indicating the Service was created successfully:

1
2
# Output
service "web" created

Let’s get the port on which the Pod is available.

1
kubectl get services

From this output, we see that the Service is available on port 30770. Let’s try to connect to one of the Worker Nodes.

Obtain the public IP address for one of the Worker Nodes associated with your Kubernetes Cluster by using the DigitalOcean console.

1
DigitalOcean console showing worker nodes

Once you’ve obtained the IP address, use the curl command to make an HTTP request to one of the nodes on port 30770:

1
curl http://your_worker_ip_address:30770

You’ll see output similar to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<title>Containers Todo Example</title>
<link rel='stylesheet' href='/stylesheets/screen.css' />
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div id="layout">
<h1 id="page-title">Containers Todo Example</h1>
<div id="list">
<form action="/create" method="post" accept-charset="utf-8">
<div class="item-new">
<input class="input" type="text" name="content" />
</div>
</form>
</div>
<div id="layout-footer"></div>
</div>
<script src="/javascripts/ga.js"></script>
</body>
</html>

You’ve defined the web Pod and a Service. Now let’s look at scaling it with Replica Sets.



Scale the Web Application

A Replica Set ensures that a minimum number of Pods are running in the cluster at all times. When a Pod is packaged as a Replica Set, Kubernetes will always run the minimum number of Pods defined in the specification.

Let’s delete the current Pod and recreate two Pods through the Replica Set. If we leave the Pod running it will not be a part of the Replica Set. Thus, it’s a good idea to launch Pods through a Replica Set, even when the count is just one.

First, delete the existing Pod.

1
kubectl delete pod web

Now create a new Replica Set declaration. The definition of the Replica Set is identical to a Pod. The key difference is that it contains the replica element which defines the number of Pods that need to run. Like a Pod, it also contains Labels as metadata that help in Service discovery.

Create the file web-rs.yaml and add this code to the file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: extensions/v1beta1
kind: ReplicaSet
metadata:
name: web
labels:
name: web
app: todoapp

spec:
replicas: 2
template:
metadata:
labels:
name: web
spec:
containers:
- name: web
image: merikanto/todo-app
ports:
- containerPort: 3000

Save and close the file.

Now create the Replica Set:

1
kubectl create -f web-rs.yaml

Then check the number of Pods:

1
kubectl get pods

When we access the Service through the NodePort, the request will be sent to one of the Pods managed by the Replica Set.

Let’s test the functionality of a Replica Set by deleting one of the Pods and seeing what happens:

1
kubectl delete pod web-wh6nf

Look at the Pods again:

1
kubectl get pods

As soon as the Pod is deleted, Kubernetes has created another one to ensure the desired count is maintained.

We can scale the Replica Set to run additional web Pods.

Run the following command to scale the web application to 10 Pods.

1
kubectl scale rs/web --replicas=10

Check the Pod count:

1
kubectl get pods

You’ll see this output:

1
2
3
4
5
6
7
8
9
10
11
12
13
Output
NAME READY STATUS RESTARTS AGE
db 1/1 Running 0 22m
web-4nh4g 1/1 Running 0 21s
web-7vbb5 1/1 Running 0 21s
web-8zd55 1/1 Running 0 21s
web-f8hvq 0/1 ContainerCreating 0 21s
web-ffrt6 1/1 Running 0 21s
web-k6zv7 0/1 ContainerCreating 0 21s
web-n5l5h 1/1 Running 0 3m
web-qmdxn 1/1 Running 0 21s
web-vc45m 1/1 Running 0 21s
web-ws59m 1/1 Running 0 2m

Kubernetes has initiated the process of scaling the web Pod. When the request comes to the Service via the NodePort, it gets routed to one of the Pods in the Replica Set.

When the traffic and load subsides, we can revert to the original configuration of two Pods.

1
kubectl scale rs/web --replicas=2

This command terminates all the Pods except two.

1
kubectl get pods

To verify the availability of the Replica Set, try deleting one of the Pods and check the count.

1
2
3
kubectl delete pod web-ws59m

kubectl get pods

As soon as the Pod count changes, Kubernetes adjusts it to match the count defined in the YAML file. When one of the web Pods in the Replica Set is deleted, another Pod is immediately created to maintain the desired count. This ensures high availability of the application by ensuring that the minimum number of Pods are running all the time.

You can delete all the objects created during this post with the following command:

1
kubectl delete -f db-pod.yaml -f db-service.yaml -f web-rs.yaml -f web-service.yaml