Kubernetes beginners series - Part 2 - deploy a simple app

If you followed by previous post you should be in the position where you have a Kubernetes cluster up and running and can access it via the kubectl which is installed on your local machine. If not please follow this link.

In this post we will revisit the basics of docker and then cover the basics of Kubernetes by completing the following tasks:

  • Create a simple Node Js app, using Redis as a persistence store.
  • Dockerize the app.
  • Deploy the app to Kubernetes and explore two kubernetes resource types - pods and services.

In the next post of this series we will cover some more advanced topics.

The Node Js app

First lets setup our env.

npm init
npm install express --save-dev
npm install redis --save-dev
npm install body-parser --save-dev

If you're new to Node --save-dev will add the dependency to our package.json. Later our docker container will be bootstrapped by running npm install thus installing required dependencies into the container

Note - Please don't worry if this app won't run locally - you probably won't have redis installed - but thats expected.. this is what containers are for.

Create an app.js

const express = require('express'); 
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json()); 
app.use(bodyParser.urlencoded({ extended: true }));

const redis = require('redis');
const client = redis.createClient('6379', 'redis');

app.get('/', (req, res) =>
{
    client.hkeys('items', function (err, reply)
    {
        res.send(JSON.stringify(reply));
    });
});

app.post('/api/additem', function(req, res) 
{
    client.hmset('items', req.body.name, req.body.data, function (err, reply)
    {
        res.send(`Added ${req.body.data}.`);
    });  
});

const port = 3030;

app.listen(port, () =>
{
    console.log(`server listen port: ${port}`);
});

This is a trivial app which does three things:

  1. Create an express webserver which listens for incoming requests on port 3030.
  2. If we receive a get request to the root we return a response with all the item keys in the redis store.
  3. If we receive a post request to /api/additem we add to the store.

Dockerize the app

Before we jump into Kubernetes we need to create a container for our app. Containers are based on images, and to create an image we need a dockerfile.

# The base image
FROM node:latest
# Create a directory on the container
RUN mkdir -p /my/app    
# Set the working directory
WORKDIR /my/app  
# Copy package.json to the working directory
COPY package.json . 
# Install node dependencies
RUN npm install
# Copy app.js into the working directory 
COPY app.js .       
# Expose the port 3030 on the container
EXPOSE 3030      
# Run the app
CMD [ "node", "app.js" ]    

To build an image from this dockerfile:

docker build -t daveleigh/demo .

Note: Your local environment needs docker installed - I am on ubuntu. https://www.docker.com/community-edition

We then need to make this image availble to our Kubernetes stack for later so we push it to a container registry.

docker push daveleigh/demo

To do this with your own account sign up at https://hub.docker.com/.

You could run this container on your docker enabled host:

docker run -d --name myapp -p 3030:3030 daveleigh/demo

However it would error as redis is not defined yet. Also that's not the aim here, we are going to deploy this and redis to our Kubernetes stack.

Finally... Kubernetes

Kubernetes provisions and manages containers running on multiple hosts. It can scale pods, manage the health of the stack (spinning up new pods if existing ones crash), automate deployments, load balance... the list goes on.

Declarative yaml/json files define the stack and resources are provisioned by the master on to worker nodes on our cluster via the kubectl Api.

The first resource we will create is a pod. A pod is a unit of deployment and scale and will contain one or more containers that may expose ports. Containers within a pod can talk to each other via localhost.

In our example we will create a pod for our app, and a separate pod for redis. However in a real world scenario if you were using redis as ephemeral cache you may have both the web app and redis in the same pod so they scale together.

Lets define a pod for our web app, and to do this we create a yaml file.

web-pod.yml

apiVersion: v1
kind: Pod
metadata:
  name: web
  labels:
    name: web
    app: demo
spec:
  containers:
    - image: daveleigh/demo
      name: web
    ports:
      - containerPort: 3030

This file is pretty self explanatory. We define the type of resource to create and some meta data. Note labels are arbitrary so you can specify anything you want. The purpose of labels is for other resources should they need to target your pod, for example a load balancer might target pods where app = demo.

Within spec we define the containers the pod will host and the ports each container exposes. Simple.

Lets also define a redis pod:

redis-pod.yml

apiVersion: v1
kind: Pod
metadata:
    name: redis
    labels:
    name: redis
    app: demo
spec:
    containers:
    - image: redis:latest
        name: redis
        ports:
        - containerPort: 6379

Provisioning the pods

Let's deploy our pods onto our cluster.

kubectl create -f web-pod.yml -f redis-pod.yml

You should see the following output:

pod "web" created
pod "redis" created

To view the pods we have just asked Kubernetes to create for us:

kubectl get pods

You will notice the web pod has errored! We can see the details of the error:

kubectl get logs web

Don't panic just yet.. and it's not quite time to swear at me for wasting your time.. this is expected and brings us onto..

Services

Our pods need to be able to communicate with each other and potentially the outside world.

There are two main types of service, internal and external. Internal services allow our pods to communicate within the cluster. External services allow access to our pods from the host.

Lets create an internal service:

redis-svc.yml

apiVersion: v1
kind: Service
metadata:
    name: redis
    labels:
    name: redis
    app: demo
spec:
    selector:
    name: redis
  type: ClusterIP
  ports:
   - name: http
     targetPort: 6379
     port: 6379

This will create a new Service object named 'redis' which targets TCP port 6379 on any Pod with the "name=redis" label.

Type 'ClusterIP' exposes the Service on an internal IP in the cluster. 'targetPort' is the port the container exposes in the pod and 'port' is the port used internally by consumers (it is the port your service listens on inside the cluster.)

kubectl create -f redis-svc.yml

Then recreate the web pod:

kubectl delete pod web
kubectl create -f web-pod.svc

Now the app should be up and running, however we can only access it internally. To test the app we can hop into the shell within our web pod:

kubectl exec -it web /bin/bash

Now lets try using the api:

curl -d "name=item1&data=test" -X POST http://localhost:3030/api/additem

You should see 'Added item1'. Great!

Before we finish up, lets make the app available from outside the cluster.

External Service

Lets use our app via the public IP of one of our worker nodes. The only missing piece of the puzzle is to create an external service to allow requests to the host to access our pods.

web-svc.yaml

apiVersion: v1
kind: Service
metadata:
    name: web
spec:
    selector:
    app: demo
  type: NodePort
     ports:
    - name: http
        nodePort: 31001
      port: 80
      targetPort: 3030

Creating our service of type 'NodePort' exposes the Service on the same port of each Node in the cluster.

The 'nodePort' defines the port on the node.
Internally all the consumers of the service can talk to the service on port 80.
the 'targetPort' is 3030, i.e the port the container exposes in the pod.

kubectl create -f web-svc.yml

If you want to see details about any of the resources you have created:

kubectl describe svc web

You should now be able to access your app at: http://PublicIP:31001.

To conclude

So we've taken our tiny app and deployed it onto a Kubernetes cluster. Hopefully this gives you a glimpse of what Kubernetes has to offer. This example is of course trivial but I hope it helps show the power of Kubernetes and how it could help you run your applications in production situations.

Speaking of more production like situations we will cover persistance in pods which is a hot topic in a later post and in the next post we will create a replica set and a rolling deployment.

Dave Leigh

Web, and long time Sitecore developer based in Bristol, UK, working at Valtech - valtech.co.uk - @valtech.
I occasionally do other things too.