# devops

**Intuji - DevOps Final Assignmment**

  
  
As part of my final assignment . I have prepared documentation of how i complete this task ste by step.

---

### **Building a Fully Containerized Full Stack Application with CI/CD, Monitoring, and Log Management**

In this post, I will share my journey of developing a fully containerized full stack application using React for the frontend, Node.js for the backend, and MySQL for the primary database and replica databse. I integrated a CI/CD pipeline with Jenkins, set up monitoring using Grafana, and managed logs with the ELK/EFK stack. Let’s dive into the process step by step.

---

### **1\. Setting Up the Environment**

To get started, I ensured that I had the necessary tools installed on my machine:

* **Docker**: To run and manage all components in containers.
    
* **Docker Compose**: To simplify the orchestration of multi-container applications.
    
* **React**: For building the user interface.
    
* **Node.js**: For handling server-side logic.
    
* **MySQL**: As the primary database and Replica Database
    
* **Jenkins**: For automating the deployment pipeline.
    
* **Grafana**: For monitoring application performance.
    
* **ELK/EFK Stack**: For centralized logging and analytics.
    

---

### **2\. Frontend (React) in a Container**

I started by creating a React application that would serve as the frontend interface.

#### **Step 1: Create a React App**

Using create-react-app, I set up my frontend project:

```bash
npx create-react-app frontend
cd frontend
```

#### Step 2: Configure Environment Variables

To keep sensitive information organized, I created a `.env` file in the React project to store the backend API URL:

```bash
REACT_APP_API_URL='http://52.202.190.246:3005'
```

#### **Step 3: Dockerize the React App**

I created a Dockerfile for the frontend to containerize the application:

```bash
# Step 1: Use Node.js image to build the project
FROM node:14-alpine AS build-stage

# Set the working directory inside the container
WORKDIR /app

# Copy the package.json and package-lock.json
COPY package*.json ./

# Install the project dependencies
RUN npm install

# Copy the rest of the application source code
COPY . .

# Build the application (assuming this creates a 'build' folder)
RUN npm run build

# Step 2: Use Nginx to serve the build files
FROM nginx:alpine AS production-stage

# Copy the build output from the previous stage to Nginx's html directory
COPY --from=build-stage /app/build /usr/share/nginx/html

# Expose port 80
EXPOSE 80
# Command to run Nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

```

here is my src/App.js file for react frontend

```javascript
import React, { useState } from 'react';
import axios from 'axios';
import './styles.css'; // Assuming styles.css is in the same folder

function App() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [users, setUsers] = useState([]);
    const [errorMessage, setErrorMessage] = useState('');
    const [successMessage, setSuccessMessage] = useState('');

    // Load users from the backend using Axios
    const loadUsers = async () => {
        try {
            const response = await axios.get(`${process.env.REACT_APP_API_URL}/data`);
            setUsers(response.data);
        } catch (error) {
            console.error('Error loading users:', error);
            setErrorMessage('Error loading users. Please try again.');
        }
    };

    const handleSubmit = async (e) => {
        e.preventDefault();
        setErrorMessage(''); // Clear previous error message
        setSuccessMessage(''); // Clear previous success message

        try {
            // Send data to the backend to save it in the database
            await axios.post(`${process.env.REACT_APP_API_URL}/submit`, {
                name,
                email,
            });

            // Clear input fields after successful submission
            setName('');
            setEmail('');
            setSuccessMessage('User registered successfully!!');
        } catch (error) {
            console.error('Error submitting form:', error);
            setErrorMessage('Error submitting form. Please try again!!.');
        }
    };

    // Handle fetching and displaying the data when clicking "Read Data" button
    const handleReadData = () => {
        loadUsers();
    };

    return (
        <div>
            <h2>Registration Form</h2>
            <form onSubmit={handleSubmit}>
                <label htmlFor="name">Name:</label>
                <input
                    type="text"
                    id="name"
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                    required
                />
                <label htmlFor="email">Email:</label>
                <input
                    type="email"
                    id="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    required
                />
                <button type="submit">Submit</button>
            </form>

            {/* Display success or error messages */}
            {successMessage && <p className="success-message">{successMessage}</p>}
            {errorMessage && <p className="error-message">{errorMessage}</p>}

            {/* Button to read data */}
            <button onClick={handleReadData}>Read Data</button>

            <h2>Registered Users</h2>
            <table id="dataTable">
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Email</th>
                    </tr>
                </thead>
                <tbody>
                    {users.length > 0 ? (
                        users.map((user, index) => (
                            <tr key={index}>
                                <td>{user.name}</td>
                                <td>{user.email}</td>
                            </tr>
                        ))
                    ) : (
                        <tr>
                            <td colSpan="2">No registered users yet.</td>
                        </tr>
                    )}
                </tbody>
            </table>
        </div>
    );
}

export default App;
```

```css
body {
    font-family: Arial, sans-serif;
    margin: 20px;
}

h2 {
    color: #4A90E2;
}

form {
    margin-bottom: 20px;
}

input[type="text"],
input[type="email"] {
    padding: 10px;
    margin: 5px 0;
    border: 1px solid #ccc;
    border-radius: 5px;
    width: 100%;
}

button {
    background-color: #4A90E2;
    color: white;
    padding: 10px 15px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

button:hover {
    background-color: #357ABD;
}

table {
    width: 100%;
    border-collapse: collapse;
}

th, td {
    padding: 8px;
    text-align: left;
    border-bottom: 1px solid #ddd;
}

th {
    background-color: #4A90E2;
    color: white;
}

tr:hover {
    background-color: #f1f1f1;
}
```

### **3\. Backend (Node.js) in a Container**

Next, I set up the backend using Node.js to process requests and handle business logic.

#### **Step 1: Create a Node.js App**

In a separate directory, I initialized the backend project

```bash
mkdir backend
cd backend
npm init -y
npm install express mysql dotenv cores axios winston 
```

#### Step 2: Set Up API Routes and Database Connection

I configured the server in `server.js`:

```javascript
require('dotenv').config();
const express = require('express');
const mysql = require('mysql2/promise');
const bodyParser = require('body-parser');
const cors = require('cors');
const client = require('prom-client');
const winston = require('winston'); // Import winston for logging

// Configure the logger
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'app.log' }), // Log to file
        new winston.transports.Console(), // Log to console
    ],
});

const dbHostPrimary = process.env.DB_HOST; // Primary DB host
const dbHostReplica = process.env.DB_REPLICA_HOST; // Replica DB host (Add this to your .env)
const dbUser = process.env.DB_USER;
const dbPassword = process.env.DB_PASSWORD;
const dbName = process.env.DB_NAME;

const app = express();
app.use(cors());
app.use(bodyParser.json());

// Prometheus metrics setup
const register = new client.Registry();
const httpRequestDurationMicroseconds = new client.Histogram({
    name: 'http_request_duration_seconds',
    help: 'Duration of HTTP requests in seconds',
    labelNames: ['method', 'route', 'code'],
    registers: [register],
});

// Middleware to record request duration
app.use((req, res, next) => {
    const end = httpRequestDurationMicroseconds.startTimer();
    res.on('finish', () => {
        end({ method: req.method, route: req.route ? req.route.path : req.path, code: res.statusCode });
    });
    next();
});

// Endpoint to expose metrics
app.get('/metrics', async (req, res) => {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
});

let primaryPool; // Declare primary pool variable
let replicaPool; // Declare replica pool variable

// Check if the database exists and create it if it doesn't
const checkAndCreateDatabase = async (dbName) => {
    const connection = await mysql.createConnection({
        host: dbHostPrimary,
        user: dbUser,
        password: dbPassword,
    });

    try {
        const [rows] = await connection.query('SHOW DATABASES LIKE ?', [dbName]);
        if (rows.length === 0) {
            await connection.query(`CREATE DATABASE ${dbName}`);
            logger.info(`Database '${dbName}' created.`); // Log database creation
        } else {
            logger.info(`Database '${dbName}' already exists.`); // Log existence
        }
    } catch (error) {
        logger.error('Error checking or creating database:', error); // Log error
    } finally {
        connection.end();
    }
};

// Create users table if it does not exist
const createUsersTable = async (dbName) => {
    const connection = await primaryPool.getConnection();
    try {
        await connection.query(`CREATE TABLE IF NOT EXISTS ${dbName}.users (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            email VARCHAR(100) NOT NULL UNIQUE
        );`);
        logger.info('Users table created or already exists.'); // Log users table creation
    } catch (err) {
        logger.error('Error creating users table:', err.message); // Log error
    } finally {
        connection.release();
    }
};

const initDatabase = async (dbName) => {
    await checkAndCreateDatabase(dbName);
    await createUsersTable(dbName);
};

// Start the Express server and initialize the database
const startServer = async () => {
    const DB_NAME = 'aditya'; // Specify your database name
    await checkAndCreateDatabase(DB_NAME); // Check and create database first

    // Create the connection pools after ensuring the database exists
    primaryPool = mysql.createPool({
        host: dbHostPrimary,
        user: dbUser,
        password: dbPassword,
        database: DB_NAME,
        waitForConnections: true,
        connectionLimit: 10,
        queueLimit: 0,
    });

    replicaPool = mysql.createPool({
        host: dbHostReplica, // Use the replica host
        user: dbUser,
        password: dbPassword,
        database: DB_NAME,
        waitForConnections: true,
        connectionLimit: 10,
        queueLimit: 0,
    });

    await createUsersTable(DB_NAME); // Create the users table

    const PORT = 3001;
    app.listen(PORT, () => {
        logger.info(`Server is running on http://localhost:${PORT}`); // Log server start
    });
};

// Endpoint to submit data to the primary database
app.post('/submit', async (req, res) => {
    const { name, email } = req.body;
    const connection = await primaryPool.getConnection(); // Use primary pool
    try {
        const result = await connection.query('INSERT INTO users (name, email) VALUES (?, ?)', [name, email]);
        logger.info('Inserted user:', result[0].insertId); // Log user insertion
        res.status(201).json({ id: result[0].insertId, name, email });
    } catch (error) {
        logger.error('Error submitting user:', error); // Log error
        res.status(500).json({ error: 'Internal Server Error' });
    } finally {
        connection.release();
    }
});

// Endpoint to get users from the replica database
app.get('/data', async (req, res) => {
    const connection = await replicaPool.getConnection(); // Use replica pool
    try {
        const [rows] = await connection.query('SELECT * FROM users');
        res.json(rows);
    } catch (err) {
        logger.error('Error fetching users:', err); // Log error
        res.status(500).send('Error fetching users');
    } finally {
        connection.release();
    }
});

// Start the server
startServer();
```

#### Step 3: Configure Environment Variables

In the backend directory, I created a `.env` file:

```bash
DB_HOST=db_container
DB_USER=root
DB_PASSWORD=Aditya123!
DB_NAME=aditya
MYSQL_ROOT_PASSWORD=Aditya123!
MYSQL_DATABASE=aditya
DB_REPLICA_HOST=db_replica
```

#### Step 4: Dockerize the Backend

I created a `Dockerfile` for the backend:

```bash
# Use Node.js as the base image
FROM node:18

# Install MySQL client
RUN apt-get update && \
    apt-get install -y default-mysql-client netcat-openbsd && \
    rm -rf /var/lib/apt/lists/*

# Create a directory for the backend
RUN mkdir -p /backend

# Set the working directory inside the container
WORKDIR /backend

# Copy package.json and package-lock.json to the working directory
COPY package*.json ./

# Install all dependencies listed in package.json
RUN npm install cors express mysql2 dotenv axios winston

# Copy the rest of the application code
COPY . .

# Add a script to wait for the database to be ready
#COPY wait-for-it.sh ./
#RUN chmod +x wait-for-it.sh

# Expose the backend port
EXPOSE 3001

# Start the application, ensuring it waits for both databases
CMD ["node", "server.js"]
```

### 4\. Database (MySQL) in a Container

For data management, I set up MySQL in a container.

#### Step 1: Run MySQL in Docker

In the `docker-compose.yml` file, I added a MySQL service. I have also provided full compose file below.

```yaml
services:
  db_container:
    container_name: database
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_SERVER_ID: 1
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    command: --server-id=1 --log-bin=mysql-bin
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
```

I also configured a replica database for redundancy:

```yaml
db_replica:
    container_name: database_replica
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_SERVER_ID: 2
    volumes:
      - mysql_replica_data:/var/lib/mysql
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    command: --server-id=2
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
```

in order to replicate from primary db to replica db

in primary db :

```sql
SHOW MASTER STATUS;
```

Note down the file and position .

in replica db:

```sql
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO
  MASTER_HOST='db_container',
  MASTER_USER='root',
  MASTER_PASSWORD='Aditya123!',
  MASTER_LOG_FILE='mysql-bin.000018',
  MASTER_LOG_POS=1087;

START SLAVE;
```

Now primary db and replica db are synchronized and replica db is replicated.

### 5\. CI/CD Pipeline with Jenkins

Next, I set up a CI/CD pipeline using Jenkins for automation.

#### Step 1: Run Jenkins in a Docker Container

I added a Jenkins service to the `docker-compose.yml`:

```yaml
jenkins:
    container_name: jenkins
    image: jenkins/jenkins:lts
    user: root
    restart: always
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      JENKINS_ADMIN_ID: aditya
      JENKINS_ADMIN_PASSWORD: aditya
    privileged: true
    networks:
      - app-network
    depends_on:
      - backend
      - frontend
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
```

#### Step 2: Configure Jenkins Jobs

In Jenkins, I created jobs for building and deploying the frontend, backend, and database. Each job is triggered on code commits.

After creating user and password in jenkins i have installed necessary plugins alongwith following plugins.

* Slack notification: for slack notification handling
    
* Github Integration: For integrating with github
    

Provide necessary credential for github:

As my github repo is private i have to provide PAT (personal access token) and slack token as credential in jenkins.

Create token in github

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729833413554/4f31b48b-32f3-4650-9332-4f0d490ebb51.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729832979139/281684bf-00b0-4b4f-8b01-ad6ac63018c1.png align="center")

put your PAT in secret field.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729832816237/d50280f5-3f57-4250-8f42-7a61092175d7.png align="center")

Make sure you have workspace and channel in slack

#### Step 3: Set Up Notifications

To keep track of build status, I integrated Slack notifications for build successes and failures.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729833235564/1f3d95f1-d392-44bc-a462-00d88a7a034e.png align="center")

Note down the APP credential

put your OAuth token of slack in secret field below.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729832909131/562d88a7-9131-458d-b72f-d253baaf99a2.png align="center")

Make sure you have workspace and channel in slack

in system- go to slack configuration

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729833106111/2ccf644e-36b5-4470-bc9e-1317cec59a0f.png align="center")

Now create new item for cerating our pipeline

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729833792211/453bdf8a-51c0-403f-917d-d42f94fdeb7a.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729833852817/52813b03-1b91-40b9-ac9e-b185be30c56e.png align="center")

Added following code to setup stage in pipeline

```java
pipeline {
    agent any  // This will run on any available agent

    environment {
        GITHUB_CREDENTIALS = credentials('github-pat')  // Use your GitHub credentials
        SLACK_CHANNEL = 'C07SR8XAK97'  // Replace with your Slack channel ID
        SLACK_CREDENTIALS = credentials('slack-token')  // Slack credentials for notifications
        //EMAIL_RECIPIENTS = 'aditya.infisec@gmail.com'  // Replace with recipient emails
    }

    triggers {
        // This listens to the webhook
        githubPush()
    }

    stages {
        stage('Checkout') {
            steps {
                script {
                    withCredentials([string(credentialsId: 'github-pat', variable: 'GITHUB_TOKEN')]) {
                        // Clone the repository using the token in the URL
                        git branch: 'main', 
                            url: "https://${GITHUB_TOKEN}@github.com/Adityakafle/full-stack-project.git"
                    }
                }
            }
        }
        
        stage('Install and Verify Docker') {
            steps {
                script {
                    // Update, upgrade system, install Docker, start and enable Docker service, install Docker Compose, and check versions
                    sh '''
                    apt-get update -y
                    apt-get upgrade -y
                    apt-get install -y docker.io
                    
                    # Check Docker version
                    docker --version

                    # Install Docker Compose
                    curl -L "https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d '"' -f 4)/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
                    chmod +x /usr/local/bin/docker-compose

                    # Check Docker Compose version
                    docker-compose --version
                    '''
                }
            }
        }

        stage('Build Database') {
            steps {
                script {
                    echo 'Starting database containers...'
            
                    // Start the database containers without dependencies
                    sh 'docker-compose up --build -d --no-deps db_container'
                    sh 'docker-compose up --build -d --no-deps db_replica'

                    // Wait for both containers to be healthy
                    waitUntil {
                        script {
                            // Check the health status of the db_container
                            def dbContainerStatus = sh(script: 'docker inspect -f "{{.State.Health.Status}}" $(docker-compose ps -q db_container)', returnStdout: true).trim()
                            echo "db_container health status: ${dbContainerStatus}"

                            // Check the health status of the db_replica
                            def dbReplicaStatus = sh(script: 'docker inspect -f "{{.State.Health.Status}}" $(docker-compose ps -q db_replica)', returnStdout: true).trim()
                            echo "db_replica health status: ${dbReplicaStatus}"

                            // Return true only when both containers are healthy
                            return dbContainerStatus == 'healthy' && dbReplicaStatus == 'healthy'
                        }
                    }

                    echo 'Both db_container and db_replica are healthy!'
                }
            }
        }

        stage('Build Backend') {
            steps {
                script {
                    echo 'Starting backend container...'
                    // Build and start the backend container
                    sh 'docker-compose up --build -d --no-deps backend'
                }
            }
        }

        stage('Build Frontend') {
            steps {
                script {
                    echo 'Starting frontend container...'
                    // Build and start the frontend container
                    sh 'docker-compose up --build -d --no-deps frontend'
                }
            }
        }

        stage('Test and Deploy') {
            steps {
                script {
                    echo 'Running tests...'
                    // Placeholder for your test command
                    // Example: sh 'docker-compose run --rm test_container'
                    echo 'Tests completed, deploying application...'
                    // Deploy commands can be added here if needed
                }
            }
        }
    }

    post {
        success {
            script {
                // Send success notification to Slack
                def message = "Deployment succeeded!"
                sh """
                    curl -X POST -H 'Authorization: Bearer ${SLACK_CREDENTIALS}' \
                    -H 'Content-Type: application/json; charset=utf-8' \
                    -d '{"channel": "${SLACK_CHANNEL}", "text": "${message}"}' \
                    https://slack.com/api/chat.postMessage
                """
            }
        }
        failure {
            script {
                // Send failure notification to Slack
                def message = "Deployment failed!"
                sh """
                    curl -X POST -H 'Authorization: Bearer ${SLACK_CREDENTIALS}' \
                    -H 'Content-Type: application/json; charset=utf-8' \
                    -d '{"channel": "${SLACK_CHANNEL}", "text": "${message}"}' \
                    https://slack.com/api/chat.postMessage
                """
            }
        }
    }
}
```

Save the configuration

Also i have added correct payload url :

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729834093594/a4ad7fc2-3828-4435-87d8-96225e5a7ab6.png align="center")

Now , my jenkins is setup to automate my build process of three tier app. i will create 4 containers from compose file when any changes happen i.e if someone committed or pushed code.

if there’s no any error it will pass all the stages

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729834339781/f24285b2-5346-4bd1-a429-74d157898c2e.png align="center")

### 6\. Monitoring with Prometheus, Grafana, and cAdvisor

For real-time monitoring and metrics collection, I integrated **Prometheus**, **Grafana**, and **cAdvisor**.

#### Step 1: Adding Prometheus and cAdvisor to Docker Compose

Prometheus collects metrics from cAdvisor, which monitors container resource usage.

Here’s the setup for Prometheus and cAdvisor in `docker-compose.yml`:

```yaml
 cadvisor:
    image: gcr.io/cadvisor/cadvisor
    container_name: cadvisor
    ports:
      - "8082:8080"
    volumes:
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
    networks:
      - app-network

  prometheus:
    image: prom/prometheus
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - app-network
    restart: unless-stopped

  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=aditya
      - GF_SECURITY_ADMIN_PASSWORD=aditya
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - app-network
    depends_on:
      - prometheus
```

I configured Grafana to connect with Prometheus and set up dashboards for real-time monitoring.

### 7\. Log Management with ELK/EFK Stack

For centralized logging, I used **Elasticsearch**, **Logstash**, and **Kibana** (ELK) to collect, process, and visualize logs.

#### Step 1: Adding Elasticsearch and Kibana to Docker Compose

Here’s the Elasticsearch and Kibana setup:

Architecture

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1729847821683/556be250-3ee4-40c8-9908-6e1d0531bcf7.png align="center")
