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