Introduction
In this post series we will follow what is needed for you to know How to prepare a NodeJS application for Kubernetes. On Part 1, We will use a NodeJS example application, prepare a Dockerfile for it and then prepare the image for deployment on Kubernetes. Then, on Part 2, we will use a Deployment yaml files to deploy our application into Kubernetes. This is a part of our Kubernetes articles here in k8s.co.il
Application Description
For the demonstration, we will create a small app called “WorldCup” to demonstrate how we nee to prepare it. The app will show 16 countries competing each other in a battle who for who is the best country.
Application Design
We will have a straight forward design which is very common in many applications deployed on K8s. The application will have a Backend, running NodeJS, and a Frontend running React. This, of course, means two different deployments and two different container images.
Let’s the see how we create those and prepare it for a containerized environment.
Procedure
In the following steps we will see the application, the Dockerfile to prepare the application for containerized environment for both Frontend and Backend of the application.
The application resides in k8s-worldcup
directory. with two sub-directories, backend and worldcup-frontend.
Let’s create the structure for the backend:
$ mkdir -p k8s-worldcup/backend
Backend Application
Our backend application is using javascript. The NodeJS application is basically one file one – app.js
in the backend
directory:
// Add this line at the beginning of app.js in the backend folder
const cors = require('cors');
const express = require('express');
const app = express();
// Add this line right after defining the app variable
app.use(cors());
const port = process.env.PORT || 3000;
const countries = [
'Argentina', 'Brazil', 'France', 'Germany',
'Italy', 'Netherlands', 'Spain', 'England',
'Portugal', 'Belgium', 'Croatia', 'Uruguay',
'Sweden', 'Switzerland', 'Denmark', 'Mexico'
];
const teams = countries.map(country => ({
country,
points: 0,
matchesPlayed: 0,
matchesWon: 0,
matchesLost: 0,
matchesDrawn: 0
}));
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
function getRandomMessage() {
const messages = [
"Don't bet your life savings on this one!",
'Expect the unexpected!',
'Football is full of surprises!',
'Keep your eyes on the ball!'
];
return messages[getRandomInt(messages.length)];
}
function randomOutcome() {
const teamAIndex = getRandomInt(teams.length);
let teamBIndex = getRandomInt(teams.length);
while (teamAIndex === teamBIndex) {
teamBIndex = getRandomInt(teams.length);
}
const outcome = getRandomInt(3);
const message = getRandomMessage();
teams[teamAIndex].matchesPlayed++;
teams[teamBIndex].matchesPlayed++;
if (outcome === 0) {
// Team A wins
teams[teamAIndex].points += 3;
teams[teamAIndex].matchesWon++;
teams[teamBIndex].matchesLost++;
} else if (outcome === 1) {
// Team B wins
teams[teamBIndex].points += 3;
teams[teamBIndex].matchesWon++;
teams[teamAIndex].matchesLost++;
} else {
// Draw
teams[teamAIndex].points++;
teams[teamBIndex].points++;
teams[teamAIndex].matchesDrawn++;
teams[teamBIndex].matchesDrawn++;
}
return {
teamA: teams[teamAIndex],
teamB: teams[teamBIndex],
outcome: outcome === 2 ? 'draw' : outcome === 0 ? 'teamA' : 'teamB',
message
};
}
app.get('/initialData', (req, res) => {
res.json({ teams });
});
app.get('/predict', (req, res) => {
const prediction = randomOutcome();
res.json({ ...prediction, teams });
});
app.listen(port, () => {
console.log(`Backend service listening at http://localhost:${port}`);
});
To install and check it’s working we can run the following commands:
$ cd k8s-worldcup/backend
$ npm init -y
$ npm i express
$ npm i cors
Edit the package.json to have the following script:
"scripts": {
"start:backend": "node app.js"
},
We can now run the code for tests:
$ npm run "start:backend"
> backend@1.0.0 start:backend
> node app.js
Backend service listening at http://localhost:3000
Go to the URL http://localhost:3000/predict to check it is working.
Refresh several times in order to verify the code works.
We now have a working backend.
Frontend Application
Our frontend application is using React. Let’s create the folder structure for our application:
$ cd worldcup
$ npx create-react-app worldcup-frontend
$ cd worldcup-frontend
Replace the src/App.js
file with the following one:
import React, { useState, useEffect } from 'react';
import './App.css';
const apiUrl = `https://${process.env.REACT_APP_API_URL}`; // Replace with the actual backend URL if needed
function App() {
const [teams, setTeams] = useState([]);
const [sortedTeams, setSortedTeams] = useState([]);
const [message, setMessage] = useState('');
useEffect(() => {
async function fetchInitialData() {
const response = await fetch(`${apiUrl}/initialData`);
const data = await response.json();
setTeams(data.teams);
}
fetchInitialData();
}, []);
useEffect(() => {
const sorted = [...teams].sort((a, b) => b.points - a.points);
setSortedTeams(sorted);
}, [teams]);
async function handlePredictButtonClick() {
const response = await fetch(`${apiUrl}/predict`);
const data = await response.json();
const updatedTeams = teams.map(team => {
if (team.country === data.teamA.country) {
return data.teamA;
} else if (team.country === data.teamB.country) {
return data.teamB;
} else {
return team;
}
});
setTeams(updatedTeams);
setMessage(data.message);
}
return (
<div className="App">
<header className="App-header">
<h1>WorldCup 2023</h1>
</header>
<main>
<section>
<button onClick={handlePredictButtonClick}>Predict Next Match</button>
<div>{message}</div>
</section>
<section className="standings">
{teams.map((team) => {
const maxPoints = Math.max(...sortedTeams.map((t) => t.points));
const pointPercentage = team.points / maxPoints;
var lineborder = "2px solid white";
if (parseInt(team.points) === parseInt(maxPoints)) {
lineborder = '2px solid red';
}
const hue = 500; // Red hue
const lightness = 90 - (pointPercentage * 70); // Lightness ranging from 60% to 100%
const cardStyle = {
backgroundColor: `hsl(${hue}, 50%, ${lightness}%)`,
color: lightness < 70 ? 'white' : 'black', // Set text color based on background lightness
border: `${lineborder}`
};
return (
<div key={team.country} className={`team-card`} style={cardStyle}>
<strong>{team.country}</strong>
<div>Points: {team.points}</div>
<div>Played: {team.matchesPlayed}</div>
<div>Won: {team.matchesWon}</div>
<div>Lost: {team.matchesLost}</div>
<div>Drawn: {team.matchesDrawn}</div>
</div>
);
})}
</section>
</main>
</div>
);
}
export default App;
Update the src/App.css file with these CSS styles:
.App {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
text-align: center;
}
.App-header {
background-color: #333;
color: #fff;
padding: 1rem;
}
button {
display: inline-block;
background-color: #333;
color: #fff;
padding: 0.5rem 1rem;
border: none;
cursor: pointer;
margin-bottom: 1rem;
}
button:hover {
background-color: #555;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
th,
td {
padding: 1rem;
border: 1px solid #ccc;
}
th {
background-color: #f4f4f4;
text-align: left;
}
/* Add these styles to your existing App.css */
.standings {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.team-card {
display: flex;
flex-direction: column;
align-items: center;
background-color: #f4f4f4;
padding: 1rem;
margin: 1rem;
width: 150px;
border: 1px solid #ccc;
border-radius: 5px;
}
.team-card strong {
margin-bottom: 0.5rem;
}
We can test and see if it works, and if it can connect to our backend application.
Install all the dependencies:
# Clear cache and old folders:
$ rm -rf node_modules package-lock.json
$ npm install
Once this is done, we have our node_modules folder. Let’s edit the scripts section in package.json
:
"scripts": {
"start": "REACT_APP_API_URL=LOCALHOST:3000 NODE_OPTIONS=--openssl-legacy-provider react-scripts start",
"start:k8s": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
Please note we add REACT_APP_API_URL
to point the React server to our Backend server. It’s not hard coded to the App.js
script, but is an environment variable. This will be pushed to the container via the yaml files in Kubernetes. In Kuberenetes we will use start:k8s
instead.
Let’s run the server:
$ npm start
Output should look something like:
Compiled successfully!
You can now view worldcup-frontend in the browser.
Local: http://localhost:3001
On Your Network: http://192.168.100.12:3001
Note that the development build is not optimized.
To create a production build, use npm run build.
webpack compiled successfully
And you should have something like this as result:
Now we have our react app running locally. We can see that the backend servers answered with 16 countries for the tournament.
Summary
In this two-part guide on How to prepare a NodeJS application for Kubernetes, we share our expertise in optimizing applications for kubernetes deployment. The Part 1 covered essential steps to ready your NodeJS app for containerization, while the second part will walk you through creating the necessary deployment yaml files for deploying to Kubernetes. The main takeaways from this post should be that we have separate containers for React and Backend (NodeJS). This article, like many others at Octopus Computer Solutions, shows our Kubernetes knowledge and how you can be prepared for fast, scalable, and secure deployment.