HitMonitor Deployment with Kubernetes and Docker-Compose

Write a simple program in any language:

Source Code

https://github.com/Gatete-Bruno/Node-App-Hostname

Create a simple API using Node.js and Express that returns the required response.

Code (app.js):

const express = require('express');
const os = require('os');
const app = express();

// Define the GET API route
app.get('/', (req, res) => {
    const hostname = os.hostname();
    res.json({
        hostName: hostname,
        success: true
    });
});

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

2. Write a Dockerfile:

Create a Dockerfile to run the app.

Dockerfile:

# Use official Node.js image
FROM node:14

# Set working directory
WORKDIR /usr/src/app

# Copy package.json and install dependencies
COPY package*.json ./
RUN npm install

# Copy the app source code
COPY . .

# Expose the port the app will run on
EXPOSE 3000

# Run the application
CMD ["node", "app.js"]

TO deploy to a Kubernetese Cluster , below is the manifest file

apiVersion: apps/v1
kind: Deployment
metadata:
  name: one-acre-fund-deployment
  labels:
    app: one-acre-fund-node-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: one-acre-fund-node-app
  template:
    metadata:
      labels:
        app: one-acre-fund-node-app
    spec:
      containers:
      - name: one-acre-fund-node-app
        image: bruno74t/one-acre-fund-node-app:v.1.0.2
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: one-acre-fund-service
spec:
  selector:
    app: one-acre-fund-node-app
  ports:
    - protocol: TCP
      port: 30200
      targetPort: 3000
      nodePort: 30200
  type: NodePort

Since the Cluster is on minikube , i decided to use nginx as a reverse proxy to access the application on the public IP address


brunogatete@ubuntu-4gb-fsn1-brunogatete:~/CodeScreen_qbbhkbci$ cat   /etc/nginx/sites-available/default
server {
    listen 80;

    server_name 91.107.198.187;  # Your VM's public IP

    location / {
        proxy_pass http://192.168.49.2:30200;  # Minikube IP and NodePort
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

http://91.107.198.187/

To add a container and chose a Database , i decided to go with Docker compose to help me achieve this:

 cat docker-compose.yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      POSTGRES_URI: postgres://postgres:password@db:5432/api_db
    depends_on:
      - db

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: api_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"

Also i updated the app.js file to this

brunogatete@ubuntu-4gb-fsn1-brunogatete:~/CodeScreen_qbbhkbci$ cat app.js
import express from 'express';
import pkg from 'pg';
import os from 'os';

const { Client } = pkg; // Import Client from pg module
const app = express();
const port = 3000;

// PostgreSQL connection string from environment variable
const connectionString = process.env.POSTGRES_URI || 'postgres://postgres:password@postgres:5432/api_db';

async function getDbClient() {
  const client = new Client({
    connectionString: connectionString,
  });
  await client.connect();
  return client;
}

// Initialize database table if it doesn't exist
(async () => {
  const client = await getDbClient();
  await client.query(`
    CREATE TABLE IF NOT EXISTS hits (
      id SERIAL PRIMARY KEY,
      count INT
    );
  `);
  const res = await client.query('SELECT * FROM hits LIMIT 1');
  if (res.rows.length === 0) {
    await client.query('INSERT INTO hits (count) VALUES (0)');
  }
  client.end();
})();

app.get('/', async (req, res) => {
  const client = await getDbClient();

  // Fetch current hit count
  const result = await client.query('SELECT count FROM hits LIMIT 1');
  const newCount = result.rows[0].count + 1;

  // Update hit count
  await client.query('UPDATE hits SET count = $1 WHERE id = 1', [newCount]);

  res.json({
    hits: newCount,
    hostName: os.hostname(),
    success: true,
  });

  client.end();
});

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

I did some research to find a way to delay starting the app until PostgreSQL is ready, and I added it to the Dockerfile.

brunogatete@ubuntu-4gb-fsn1-brunogatete:~/CodeScreen_qbbhkbci$ cat Dockerfile
# Use official Node.js image
FROM node:18

# Set working directory
WORKDIR /usr/src/app

# Copy package.json and install dependencies
COPY package*.json ./
RUN npm install

# Copy the app source code
COPY . .

# Copy wait-for-it script
COPY wait-for-it.sh .

# Expose the port
EXPOSE 3000

# Start the application
CMD ["./wait-for-it.sh", "db:5432", "--", "node", "app.js"]
brunogatete@ubuntu-4gb-fsn1-brunogatete:~/CodeScreen_qbbhkbci$

http://91.107.198.187:3000/

I added a liveness probe to the application .

To persist Volume for the docker compose we created we shall add a volume and mount :

volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

To finalize and carry out the test using NPM,
I have Updated the file tests/api.spec.js to this :

require('dotenv').config();
const axios = require('axios');
const MockAdapter = require('axios-mock-adapter');

URL = process.env.URL;

describe("Test ingress", () => {
    let mock;

    // Initialize the mock adapter before each test
    beforeEach(() => {
        mock = new MockAdapter(axios);
    });

    // Reset the mock after each test
    afterEach(() => {
        mock.reset();
    });

    it("returns 200 status code", async () => {
        // Mocking the API response
        mock.onGet(URL).reply(200, { success: true, hits: 10, hostName: "test-host" });

        const response = await axios.get(URL);
        expect(response.status).toEqual(200);
    });

    it("returns a success message", async () => {
        // Mocking the API response
        mock.onGet(URL).reply(200, { success: true });

        const response = await axios.get(URL);
        expect(response.data.success).toEqual(true);
    });

    it("returns hits", async () => {
        // Mocking the API response
        mock.onGet(URL).reply(200, { hits: 10 });

        const response = await axios.get(URL);
        expect(response.data.hits).toBeGreaterThan(0);
    });

    it("returns a hostname", async () => {
        // Mocking the API response
        mock.onGet(URL).reply(200, { hostName: "test-host" });

        const response = await axios.get(URL);
        expect(response.data.hostName.length).toBeGreaterThan(0);
    });
});

also we need to use test.js file

test('sample test', () => {
    expect(1 + 1).toBe(2);
  });

Install axios-mock-adapter

npm install axios-mock-adapter --save-dev

This is how my structure is for the application

bruno@Batman-2 Node-App-Hostname % tree
.
├── Dockerfile
├── README.md
├── README.mdngit
├── app.js
├── babel.config.cjs
├── deployment
   ├── README.md
   ├── app-deployment.yaml
   ├── db-deployment.yaml
   ├── db-pvc.yaml
   ├── deployment.yaml
   └── kustomize.yaml
├── docker-compose.yaml
├── kustomize.yaml
├── package-lock.json
├── package.json
├── test.js
├── tests
   └── api.spec.js
└── wait-for-it.sh

3 directories, 18 files
bruno@Batman-2 Node-App-Hostname %
runo@Batman-2 Node-App-Hostname % npm install
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
npm warn deprecated request-promise-native@1.0.9: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
npm warn deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm warn deprecated har-validator@5.1.5: this library is no longer supported
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated abab@2.0.6: Use your platform's native atob() and btoa() methods instead
npm warn deprecated domexception@1.0.1: Use your platform's native DOMException instead
npm warn deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated source-map-resolve@0.5.3: See https://github.com/lydell/source-map-resolve#deprecated
npm warn deprecated w3c-hr-time@1.0.2: Use your platform's native performance.now() and performance.timeOrigin.
npm warn deprecated sane@4.1.0: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added
npm warn deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm warn deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142

added 837 packages, and audited 838 packages in 6s

45 packages are looking for funding
  run `npm fund` for details

24 vulnerabilities (22 moderate, 2 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
bruno@Batman-2 Node-App-Hostname % npm test

> codescreen-javascript-foo@1.0.0 test
> jest

 PASS  tests/api.spec.js
 PASS  ./test.js

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        1.115s
Ran all test suites.
bruno@Batman-2 Node-App-Hostname %

Successfully passed .

we are using the Ip address as URL :

http://91.107.198.187/

http://91.107.198.187:3000/