Containerizing a Node.js REST API for Kubernetes

Overview

In this tutorial, you will learn how to build a Node.js Docker image for an Express REST API, and deploy it to Kubernetes. You will also learn how to store sensitive backend credentials securely, and away from your applications source code.

1 – Building an Express REST API

The following will show you how to build an Express REST API, based on Building a RESTful API Using Node and Express 4, written by Chris Sevilleja. The purpose of this app will be to demonstrate how to build and deploy an Express app Docker image in Kubernetes

Most importantly, you will learn how to handle environment settings, such as database credentials, correctly.

Inside of your project directory, create a package.json file. The following will be used in this tutorial, which will install packages for Express, Body Parser, and Mongoose.

// package.json

{
   "name": "express-api",
   "version": "1.0.0",
   "description": "An example Node.js Express API",
   "main": "index.js",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "[email protected]",
   "license": "ISC",
   "dependencies": {
     "body-parser": "^1.19.0",
     "express": "^4.17.1",
     "mongoose": "^5.5.11"
   }
 }

Use NPM to install the express-api’s dependencies

npm install

Create a file named config.js in the same directory. This file will hold all of our application’s environment configurations.

// config.js

export function = {
  app: {
    port: process.env.PORT || 3000,
  },
  db: {
    username: process.env.DB_USERNAME || '',
    password: process.env.DB_PASSWORD || '',
    host: process.env.DB_HOST || '',
    database: expressapi
  } 
}

Notice that most of the values are set via environment variables. This is done to completely decouple the application from every environment, which allows us to deploy the app any where.

The values will then be set by either Kubernetes Secrets or Kubernetes ConfigMaps, for sensitive and non-sensitive settings, respectfully. For example, a database password should be stored as a secret, whereas a database host could be stored in a configMap.

Creating Routes

const express = require('express');
const book   = require('./bookController');
const router = express.Router();

router.use('/book', book);

module.export = router;

Create a model

// bookModel.js

const mongoose = require('mongoose');

var bookSchema = mongoose.Schema({
  title: {
    type: String,
    required: true
  },
  published: {
    type: Date
  }
});

var Book = module.exports = mongoose.model('book', bookSchema);

Creat a controller

// bookController.js

Book = require('./bookModel');

exports.index = function (req, res) {
  Book.get(function (err, books) {
    if (err) {
      res.json({
        status: "error",
        message: err
      });
    }
    res.json({
      status: "Success"
      data: books
    });
  });
};

Now create the index.js file.

// index.js

const express    = require('express');
const bodyParser = require('body-parser');
const routes     = require('./routes');
const config     = require('./config');

const app        = express();

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

router.get('/', function(req, res) {
     res.json({ message: 'hooray! welcome to our api!' });   
});
 
app.use('/api', routes);

app.listen(config.app.port);
console.log('Express API running on port ' + config.app.port);

2 – Building the Docker Image

The image built in this tutorial will be based Node’s official documentation on dockerizing node.js applications.

Inside of your project directory, create a new file named Dockerfile. The Docker image we build will be based on the official Node image.

FROM node:12

RUN apt update \
    && apt update upgrade -y
RUN apt install -y \
    nginx

WORKDIR /app

COPY app/package.json /app/package.json
COPY app/index.js /app/index.js
COPY app/config.js /app/config.js

COPY nginx/nginx.conf /etc/nginx.conf

RUN npm install
CMD ['startup.sh']

3 – Storing Configurations in Kubernetes ConfigMaps

ConfigMaps are resource types in Kubernetes that allow you to store configurations for containers. The configs can then be mounted as environment variables or as files, which ever is best suited for your application.

For example, the configuration can be presented as a properties file for a Java Springboot application, and as an environment variable for a Node.js application.

Create a new manifest for the Node.js API named expressapi-configmap.yml.

touch expressapi-configmap.yml

Add the following configurations to it. These will be used for the database host and database name within our API.

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: expressapi-config
data:
  mongodb_host: db.prod.serverlab.ca
  mongodb_database: expressapi_prod

Now create the ConfigMap resource in your Kubernetes cluster by using the kubectl apply command. If the resource already exists, it will be updated.

kubectl apply -f expressapi-configmap.yml

3 – Storing Secrets in Kubernetes Secrets

Unlike ConfigMaps, secrets are for storing sensitive information. This is the resource type you will use to store credentials, ssh keys, TLS keys and certificates, and any other sensitive data used by your applications.

Secrets are stored as Base64 encoded strings. In order for us to store our database username and password, we will need to convert it to base64. Use the following commands to perform this action.

echo -n 'db-user' | base64
ZGItdXNlcg==
echo -n 'super-secret-password' | base64
c3VwZXItc2VjcmV0LXBhc3N3b3Jk

Keep note of the base64 encoded strings outputted by both commands. These values will be inserted into our Kubernetes secrets for our Express API.

Create a new file named expressapi-secrets.yml

touch expressapi-secrets.yml

Add the following contents to the file. Remember to use your own base64 encoded string for the db_username and db_password keys.

---
apiVersion: v1
kind: Secret
metadata:
  name: expressapi-secret
data:
  mongodb_username: ZGItdXNlcg==
  mongodb_password: c3VwZXItc2VjcmV0LXBhc3N3b3Jk

Create the secret resource in Kubernetes using the kubectl apply command.

kubectl apply -f expressapi-secrets.yml

Base64 is not encryption, so you should store this file in a safe place. Or better yet, remove the key values all together. This allows you to retain the key names and provides a mechanism to easily update the values later on.

4 – Creating a Deployment for the Node.js API

A deployment is a mechanism to scale and update stateless container Pods. Since our Express API does not maintain state, we create a deployment resource for it, which will in turn generate a replicaSet resource.

A replicaSet is what controllers the pods replicas, allowing it to replace failed pods, scale pods up or scale them down.

Create a new file named expressapi-deployment.js

touch expressapi-deployment.js

Add the following contents to it. This deployment manifest will create 3 replicas of the Express API. The replicas will be based on the serverlab/expressapi:1.0.0 Docker image created earlier in this tutorial.

apiVersion: v1
kind: Deployment
metadata:
  name: expressapi
  labels:
    app: expressapi
spec:
  replicas: 3
  selector:
    matchLabels:
      app: expressapi
  template:
    metadata:
      labels:
        app: expressapi
    spec:
      containers:
        - name: expressapi
          image: serverlab/expressapi:1.0.0
          env:
            - name: MONGODB_HOST
              valueFrom:
                configMapKeyRef:
                  name: expressapi-configmap
                  key: mongodb_host
            - name: MONGODB_DATABASE
              valueFrom:
                configMapKeyRef:
                  name: expressapi-configmap
                  key: mongodb_database
            - name: MONGODB_USER
              valueFrom:
                secretKeyRef:
                  name: expressapi-secret
                  key: mongodb_username
            - name: MONGODB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: expressapi-secret
                  key: mongodb_password
 

Notice the values under then env key. Values fetch from our ConfigMap are referenced by the configMapKeyRef key, and secrets by the secretKeyRef key.

We fetch our database connection information from the ConfigMap, and the database credentials from the Secret. This allows any newly created Express API pod to connect to the Mongo database.

5 – Exposing your Node.js Deployment

To expose your Express API to the public, you need to create a service resource for it. We create service resources because pods are ephemeral, which there entire state is temporary. As soon as a pod is destroyed, its IP address is revoked, and a new pod will then acquire a different address.

Services are static and not ephemeral. For this reason they are ideal for using as an endpoint. Pods are attached to a service resource by a label, which is set using a selector on the service, and a metadata name on the pod manifest.

Create a new file named expressapi-service.yml.

touch expressapi-service.yml

Add the following contents to it.

apiVersion: v1
kind: Service
metadata:
  name: expressapi-service
spec:
  type: LoadBalancer
  selector:
    app: expressapi
  ports:
    - protocol: TCP
      port: 3000
      targetPort: 3000

We are assigned the LoadBalancer type to our service, which will cause the cloud platform Kubernetes is running on to provision a compute load balancer. The balancer will then expose the port on the protocol set in the manifest.

The selector key is assigned expressapi, which maps to the Deployment metadata name value. Only pods matching this label are attached to the service.

Create the new service resource using the kubetcl apply command.

kubectl apply -f expressapi-service.yml

6 – Scaling your Node.js API