Creating a fully automated Multibranch CI/CD pipeline for React

The development of React applications has been greatly facilitated by tools like Vite, which offers a faster development experience with hot module replacement and simpler configuration. However, the development experience doesn't end there. Running tests, checking for linting errors, and deploying to a staging environment to see how updates from developers work together can also take up considerable time for developers. Many companies still perform these processes manually, which can slow down the workflow. In this article, we will create a multibranch CI/CD pipeline for a React application to automate the building, testing, and deployment processes using open source tools like Docker, Jenkins, and Ansible.

Index

  1. Step 1: Dockerizing the Application

  2. Step 2: Setting up Nexus.
  3. Step3: Setting up Jenkins.
  4. Step4: Creating the pipeline
  5. Step 5: Ansible and Jenkinsfile Configuration

Step 1: Dockerizing the Application

In this pipeline, we will use Docker to Containerize our application. This makes our application portable. Create a file named 'Dockerfile' in the root of the project directory and paste the following code. This code is basically creating a multi-stage build where the first stage is the normal build process of a react application and the second stage involves copying the 'dist' folder from the 1st build (builder) to the nginx default html path to serve the html and copying 'nginx.conf' from the currect directory.

# Dockerfile

# Step 1: Build the application
FROM node:alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Step 2: Set up the production environment
FROM nginx:stable-alpine-perl
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 5000
CMD ["nginx", "-g", "daemon off;"]

nginx configuration file.

# nginx.conf
server {
    listen 8080;
    server_name localhost;
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
}

Both the files will be saved in the root folder of the project.

Step 2: Setting up Nexus.

Nexus is an open source Artifact Repository Manager. We will use Nexus to Store our docker image versions and use these images for deployment for either staging or production environment based on which branch the changes are deployed. (This is explained later) To get up and running with nexus, you will require a VM running. You can use any cloud provoder you are familiar will the steps will remain more or less same. I will be using a DigitalOcean Droplet. You will also require a firewall setup to allow inbound traffic to the port at which nexus will be running.(default port is 8081).

  1. Create a Digital Ocean Droplet
  2. Add SSH key of you machine to later access the VM.
  3. Attach the Firewall rules
  4. SSH into the machine and install OpenJDK - 8. (Nexus uses Java version-8)
sudo apt install openjdk-8-jre-headless
sudo apt install openjdk-8-jdk-headless
java -version
  1. Download Nexus
cd /opt
wget https://download.sonatype.com/nexus/3/latest-unix.tar.gz
ls
tar -zxvf latest-unix.tar.gz
ls
# 2 folders named nexus-[version] and sonatype-work/nexus3 have been extracted
  1. Create new User called nexus
  2. Attach nexus user to the 2 folders
chown -R nexus:nexus [folder 1]
chown -R nexus:nexus [folder 2]
ls -l
  1. Set nexus configuration so Nexus will run as a nexus user.
vim nexus-[version]/bin/nexus.rc
# inside the editor
run_as_user="nexus"
  1. Run Nexus.
su - nexus
# Start Nexus
/opt/nexus-[version]/bin/nexus start

# Check if running
ps aux | grep nexus

# Install net-tools
exit
apt install net-tools
su - nexus
netstat -lnpt

You should now be able to run nexus in you browser at http://<DROPLET-IP>:8081.Further more you will require to create a Docker hosted repository in nexus, create a nx-docker user to access the repository, create a new role for the repository with suitable permissions. Attach the Role to the user. Refer official documentation to do these steps. Keep in mind after creating the repository add an http or https connector (based on what your jenkins server is running) port as 5000.

Step 3: Setting up Jenkins.

Jenkins is an open source automation server which allows us to build pipelines for our application, it comes with a number of built in pluggins which we will be using to build the pipeline for out React application along with some manual bash scripting. To get up and running with Jenkins, The Intial Process of Setting up the machine and Firewall will remain the same as for nexus. For the Installation we will be using a docker image of Jenkins.

  1. SSH into the machine.
  2. Update your system
  3. Install Docker

  4. Pull the Jenkins Image jenkins/jenkins:lts
  5. Run The Jenkins Container.

Aditionally since we will be running many docker commands in our pipeline it is required that we install docker inside the Jenkins container. Refer this article to install Docker inside Docker. Once Docker is setup inside the container, create a daemon.json file at the destination /etc/docker and add the following code. Do the same outside of the container.

# daemon.json

{
  "insecure-registries": ["<Nexus_Repo_Url>:5000"]
}

Assuming you now have Jenkins running, open Jenkins in you browser and login into Jenkins using initial admin password.

# find the initial adming password here inside the jenkins container
cat /var/lib/jenkins/secrets/initialAdminPassword

Once you are in, Install the following plugins.

  • Multibranch Scan Webhook Trigger
  • Gitlab Plugin
  • Nodejs (Install manually inside container if the required version is not available on Jenkins)
  • Ansible (same as Nodejs)

Restart Jenkins.

Step4: Creating the pipeline.

Once everyting is setup and all the plugins are Installed. We will First add Some Credentials that we will require. Manage Jenkins > Mange Credentials.

  1. Gitlab Credentials - Add you gitlab usename and password and give the id and description as 'gitlab-cred'.
  2. Nexus User - Create a new username and password and add the username and password of the nexus user which we created earlier.

Once all the credentials are set, we will create a new Multi-branch-pipeline. Name it as react-multibranch-pipeline.

  1. Create a New Multibranch pipeline.
  2. Under Branch Sources → Add source → Chose Git → and provide Gitlab URL and Credentials (Credentials are optional if it is public repo).
  3. Under Build Configuration → choose Jenkinsfile path. Most of the case it will be in the root directory of your repository.
  4. Apply and Save the job.

Now Jenkins automatically scans the repository and create a job for each branch wherever it finds a Jenkinsfile and initiate first build. To automate this process on every push event to our repository, enable the option "scan by webhook" under "Scan Multibranch Pipeline Triggers". Here we should give a token. I am giving it as "gitlabtoken".

  • In Gitlab navigate to your project settings > Webhooks.
  • Set the url to '<JENKINS_URL>/multibranch-webhook-trigger/invoke?token=gitlabtoken'.
  • Disable SSL verification and click Add webhook.

Make changes to the code and check if the respective branch pipeline gets triggered.

Step 5: Ansible and Jenkinsfile Configuration

Thank you for sticking with me till now. Since our pipeline is setup you can now create seperate Jenkinsfile for every branch which will be used when a push event is triggered to any of the branches or create a same file and add a 'when' directive to specify steps for a specific branch in a single file. For this Example we will be creating a single Jenkinsfile.

# when directive in Jenkinsfile
when {
    branch pattern:'feature/[a-z]*',comparator: "REGEXP"
}
steps {
// Some pipeline step for feature branch
}

Jenkinsfile

This file is written in Declarative Pipeline Syntax of Jenkins which follow the same rule as Groovy Syntax.

pipeline {
    agent any

    environment {
        NEXUS_REPO_URL = 'http://206.189.129.73:5000/repository/docker-hosted/'
        DOCKER_IMAGE_VERSION = "1.0.0"
        DOCKER_IMAGE_TAG = "whiteboard"
        DOCKER_IMAGE_TYPE = "release"
        STAGING_SERVER_IP = ""
        PROD_SERVER_IP = ""
        SSH_PRIVATE_KEY_PATH="/var/jenkins_home/.ssh/id_rsa"
    }

    stages {
        stage("Checkout from SCM"){
            steps {
                sh 'echo "SCM checkout of branch \'$GIT_BRANCH\'"'
                git branch: env.GIT_BRANCH, credentialsId: 'gitlab-cred', url: 'https://gitlab.com/Kirtish_Patil/whiteboard.git'
            }
        }

        stage("Testing the application"){
            steps {
                script {
                    sh '''
                    echo "Installing npm packages"
                    npm i
                    echo "Running tests"
                    npm run test
                    if [ $? -ne 0 ]; then
                        echo "Tests Failed"
                        exit 1
                    fi
                    echo "Tests Completed Successfully"
                    '''
                }
            }

        stage("Building Docker Image") {
            steps {
                sh 'echo "Building Docker Image for branch \'$GIT_BRANCH\'" of tag \'${DOCKER_IMAGE_TAG}-${DOCKER_IMAGE_TYPE}:${DOCKER_IMAGE_VERSION}\''
                script {
                    dockerImage = docker.build("${DOCKER_IMAGE_TAG}-${DOCKER_IMAGE_TYPE}:${DOCKER_IMAGE_VERSION}")
                }
            }
        }

        stage("Push Docker image to Nexus"){
            steps {
                script {
                    docker.withRegistry("${NEXUS_REPO_URL}", 'nexus-repo-cred'){
                        dockerImage.push()
                    }
                }
            }
        }

        stage("Deploying to staging environment"){
            when {
                branch 'master'
            }
            steps {
                sh '''
                echo "Deploying to master"
                ansible-playbook -i ansible/hosts  ansible/setup-docker.yml | tee ansible_setup_output.txt
                ansible-playbook -i ansible/hosts  ansible/deploy-app.yml | tee ansible_deploy_output.txt

                echo "===== Ansible Setup Output ====="
                cat ansible_setup_output.txt
                echo "===== Ansible Deploy Output ====="
                cat ansible_deploy_output.txt
                '''
            }
        }

        stage("Global stage for all branches") {
            steps {
                echo "Process was successful..."
            }
        }
    }
    post {
        always {
            script {
                cleanWs()
                sh 'docker image prune -f'
            }
        }
    }
}

Ansible

Ansible is another automation open source tool that allows us to provide provisioning of nodes/vms. In this pipeline we will be dividing our ansible-playbook into 2 file, 1 for setup and 2nd for deployment, thus provisioning our machine on which our application will be hosted.

  • Setup
# ansible/setup-docker.yaml
---
- name: Setup Docker on Staging Environment
  hosts: whiteboard_staging
  become: yes
  tasks:
    - name: Update apt package index
      apt:
        update_cache: yes

    - name: Install Snap
      apt:
        name: snapd
        state: present

    - name: Ensure snapd is running
      systemd:
        name: snapd
        state: started
        enabled: yes

    - name: Install Docker using snap
      snap:
        name: docker
        state: present

    - name: Create Docker daemon configuration directory
      file:
        path: /etc/docker
        state: directory
        mode: "0755"

    - name: Configure Docker daemon for insecure registry
      copy:
        content: |
          {
            "insecure-registries": ["206.189.129.73:5000"]
          }
        dest: /var/snap/docker/current/config/daemon.json
        owner: root
        group: root
        mode: "0644"

    - name: Restart Docker service
      systemd:
        name: snap.docker.dockerd
        state: restarted
        enabled: yes

    - name: Ensure Docker service is running
      service:
        name: snap.docker.dockerd
        state: started
  • Deployment
# ansible/deploy-app.yaml

- name: Deploy App on staging Environment
  hosts: whiteboard_staging
  become: yes
  tasks:
    - name: Ensure Docker daemon is running
      become: yes
      service:
        name: snap.docker.dockerd
        state: started

    - name: Login to Nexus Docker Registry
      command: sudo docker login -u nx-docker -p 123 206.189.129.73:5000

    - name: Pull Docker Image
      command: sudo docker pull 206.189.129.73:5000/whiteboard-feature:1.0.0

    - name: Run Docker Container
      command: sudo docker run -d -p 8080:8080 206.189.129.73:5000/whiteboard-feature:1.0.0

Also add a hosts file for specifying the ip address of the machine where our application will be hosted along with the path to the ssh private key file.

[whiteboard_staging]
68.183.83.29 ansible_ssh_private_key_file=~/.ssh/id_rsa ansible_user=root

Conclusion.

And we are done, there are n number of possibilities to where you can take this pipeline to. Setting up a CI/CD pipeline for your React application helps automate testing, building, and deployment, making your workflow faster and more reliable. It ensures consistent code quality and quicker releases, allowing you to deliver better applications with less effort.