Building an End-to-End CI/CD Pipeline for Java Applications - Part 1: Using Docker and Tomcat

The first part of our end-to-end CI/CD series, focusing on setting up a robust pipeline for Java applications using Jenkins, Docker, and Tomcat.

Building an End-to-End CI/CD Pipeline for Java Applications - Part 1: Using Docker and Tomcat

Table of Contents

Introduction

In this tutorial, we’ll walk through the process of creating a robust end-to-end Continuous Integration and Continuous Deployment (CI/CD) pipeline for a Java application. This is the first part of our E2E CI/CD series, focusing on Jenkins integration with Docker and Tomcat. We’ll leverage industry-standard tools to automate building, testing, and deploying applications efficiently.

Code Repository

The sample code for this project is available at Spring PetClinic GitHub Repository.

Prerequisites

Before starting this project, make sure you have the following prerequisites:

  1. Jenkins: Ensure Jenkins is installed and running on a server. Refer to the Jenkins Installation Guide
  2. Docker: Install Docker on your server. Refer to the Docker Installation Guide
  3. Java: Ensure your Java application is built and ready for deployment.
  4. Maven: Install Maven or use the Maven version bundled with Jenkins.

Setting Up Jenkins

In this guide, we’ll set up Jenkins on an AWS EC2 instance and configure it to build and deploy a Java application using Docker and Tomcat. Follow these steps for a seamless setup.

Note: To restart jenkins service use: sudo systemctl restart jenkins

Prerequisites

Before proceeding, ensure you have the following:

  • AWS Account: Access to launch EC2 instances.
  • Security Group Configuration: Open port 8080 for Jenkins and 22 for SSH.
  • Java Application Source Code: Ready to deploy.
  • Tools Installed on Local Machine:
    • SSH client
    • AWS CLI (optional)

Step 1: Launch an EC2 Instance

  1. Select an Instance Type:

    • Choose an Ubuntu 22.04 AMI.
    • Use a t2.large instance for optimal performance.
  2. Configure Security Group:

    • Open the following ports:
      • 8080 for Jenkins.
      • 22 for SSH.
  3. Access the Instance:

    • SSH into the instance using the command:
      ssh -i "<your-key-pair>.pem" ubuntu@<public-ip-address>
      

Step 2: Install Java

Jenkins requires Java to run. Install OpenJDK using the following commands:

sudo apt update -y
sudo apt install openjdk-17-jdk -y
java -version

Verify that the installation is successful by checking the Java version.

Step 3: Install Jenkins

  1. Add Jenkins Repository:

    curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee "/usr/share/keyrings/jenkins-keyring.asc"
    echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null
    
  2. Install Jenkins:

    sudo apt update -y
    sudo apt install jenkins -y
    
  3. Start Jenkins Service:

    sudo systemctl start jenkins
    sudo systemctl status jenkins
    
  4. Access Jenkins:

    • Open a web browser and navigate to http://<public-ip>:8080.
  5. Unlock Jenkins:

    • Retrieve the initial password:
      sudo cat /var/lib/jenkins/secrets/initialAdminPassword
      
    • Enter the password on the Jenkins setup page and follow the setup wizard to complete the installation.

Step 4: Install Essential Jenkins Plugins

Once Jenkins is running, install the following plugins:

  • Pipeline
  • Git
  • Maven Integration
  • SonarQube Scanner
  • OWASP Dependency-Check
  • Docker Pipeline

You can add plugins from Manage Jenkins > Manage Plugins.

Step 5: Set up the admin user.

Step 6: Configure Jenkins Global Tools

Next we need to configure it so go to Global Tool Configuration:

  1. JDK –> Add JDK –> Name it as jdk17 –> Install automatically –> Install from Adoptium. –> Click on 17 version.
  2. Maven –> Add Maven –> Name it as maven3 –> Install automatically –> Add the path of the maven on your server.–> Use the version 3.8.6
  3. SonarQube Scanner –> Add SonarQube Scanner –> Name it as sonarqube scanner –> And then add the path of the sonarqube scanner on your server.
  4. Configure Docker:
    • Install Docker on the EC2 instance:
      sudo apt install docker.io -y
      sudo systemctl start docker
      sudo usermod -aG docker jenkins
      
    • Restart Jenkins to apply the changes.

Creating a Pipeline Job

  1. Go to Dashboard –> New Item –> Enter the name of the project –> Select Pipeline –> OK
  2. First choose the discard old builds –> And then choose the number of builds to keep. For me it is 2.
  3. Next choose the SCM as Git.
  4. Add the repository URL.
  5. Add the credentials if any.
  6. Next choose the branch as main.
  7. In the pipeline section add the following code:

Creating a Pipeline:

  1. First choose the hello world pipeline to get the boiler plate code.
  2. Next we need to define the tools which we have added in jenkins so that they have global scope:
pipeline {
    agent any
    tools {
        maven 'maven3'
        jdk 'jdk11'
    }
}
  1. Next stage is to add git check out:
stages {
    stage('git checkout') {
        steps {
            git url: 'https://github.com/your-repository/your-project.git'
        }
    }
}

For any help we can use pipeline syntax to get the help.

  1. Next stage is to do the build/compile the code. This helps check the syntax of the code and identify any errors that need to be fixed:
stages {
    stage('compile') {
        steps {
            sh 'mvn compile'
        }
    }
}
  1. Next is to do unit testing to check the functionality of the code and fix any errors:
stages {
    stage('unit testing') {
        steps {
            sh 'mvn test'
        }
    }
}
  1. Next is to do the SonarQube analysis to check the code quality and fix any errors.

For this first we need to configure the sonarqube in jenkins:

  1. Go to Manage Jenkins.
  2. Click on Manage Credentials.
  3. Click on global.
  4. Click on add credentials.
  5. Add credentials type as secret text.
  6. Add the token which you have created in sonarqube.
  7. Add id as sonar and description as sonar.

Also go to Manage Jenkins –> Configure System –> SonarQube servers. Here we need to add the sonarqube server url and the credentials which we have created in the previous step.

What is the difference between configure system and global tool configuration?

Configure system is for system level configuration like SonarQube servers, Docker, Kuberenetes etc. Global tool configuration is for tool/plugin specific configuration like JDK, Maven, Git etc.

Next we need to go to global tool configuration and add the sonarqube scanner:

  1. SonarQube Scanner –> Add SonarQube Scanner –> Name it as sonarqube scanner –> And then add the version as 4.0.0.2922

Now go to the pipeline section and add the sonarqube scanner. First we need to create an environment variable for the sonarqube server url:

pipeline {
    environment {
        SONARQUBE_URL = tool name: 'sonar-scanner' // It should be same as the name of the sonarqube scanner in the global tool configuration.
    }
}

For below take help of pipeline syntax:

stages {
    stage('sonarqube analysis') {
        steps {
            withSonarQubeEnv('sonarqube') { // It should be same as the name of the sonarqube server in the global tool configuration.
                sh '''
                $SONARQUBE_URL/bin/sonar-scanner -Dsonar.projectKey=myproject -Dsonar.\ qualitygate.wait=true -Dsonar.projectName=myproject -Dsonar.\ projectVersion=1.0 -Dsonar.sources=src -Dsonar.java.binaries=.
                '''
            }
        }
    }
}

Understand the report in the sonarqube server, understand the issues, and learn how to assign the issues to the developers.

  1. Next is to do the OWASP dependency check to identify vulnerabilities in the code dependencies and fix them.

For this first we need to install the OWASP dependency check plugin in jenkins:

  1. Go to Manage Jenkins.
  2. Click on Manage Plugins.
  3. Click on available plugins.
  4. Search for OWASP dependency check.
  5. Install the plugin.

Next we need to configure the OWASP dependency check in jenkins:

  1. Go to Global Tool Configuration.
  2. Click on Dependency Check.
  3. Name it as OWASP dependency check.
  4. Click on install automatically.
  5. Add the version as 7.1.0.

Next we need to configure the OWASP dependency check in the pipeline:

pipeline {
  stages {
    stage('OWASP dependency check') {
      steps {
        dependencyCheck additionalArguments: '--scan ./', odcInstallation: 'owasp-dependency-check'
        dependencyCheckPublisher pattern: '**/dependency-check-report.xml', healthy: '0', unhealthy: '1'
      }
    }
  }
}

You can check the report in the jenkins server. Under Build History –> Last build –> Under Workspace –> target –> site –> dependency-check-report.html

  1. Next is to build the maven project to create a jar file of the project:
pipeline {
    stages {
        stage('build') {
            steps {
                sh 'mvn clean install'
            }
        }
    }
}

Where can we get the jar file? We get the jar file in the target folder. So the path is ./target/my-app-1.0-SNAPSHOT.jar. Go to the ec2 instance and check the target folder.

  1. Next is to do the docker build to create a docker image of the project:

For this we need to install docker plugin in the jenkins server:

  1. Go to Manage Jenkins.
  2. Click on Manage Plugins.
  3. Click on available plugins.
  4. Search for Docker and Docker Pipeline and Docker API and docker-build-step.
  5. Install the plugin.

Next we need to configure the docker in global tool configuration:

  1. Go to Manage Jenkins.
  2. Click on global tool configuration.
  3. Click on Docker.
  4. Install automatically. And download the latest version of docker.
  5. Name it as docker.

Next we need to configure the docker in the pipeline. Before that we need to give the credentials for docker hub:

  1. Go to Manage Jenkins.
  2. Click on Manage Credentials.
  3. Click on global.
  4. Click on add credentials.
  5. Add credentials type as secret text.
  6. Add the username and password for docker hub.
  7. Add id as dockerhub and description as dockerhub.
pipeline {
    agent any
    tools {
        docker 'docker'
    }
}

Next we need to do the docker build:

stages {
    stage('docker build') {
        steps {
            script {
                withDockerRegistry([credentialsId: 'dockerhub', toolName: 'docker']) {
                    sh 'docker build -t mydockerhubusername/mydockerimage:mytag .'
                }
            }
        }
    }
}

Once build is done you can see the image in ec2.

Before pushing the image we can use trivy to scan the image for vulnerabilities:

  1. First install trivy in jenkins server.
  2. Next we need to configure the trivy in the global tool configuration.
  3. Name it as trivy.
  4. Install automatically.
  5. Add the version as 0.32.0.

Next we need to configure the trivy in the pipeline:

pipeline {
    tools {
        trivy 'trivy'
    }
}
stages {
    stage('trivy scan') {
        steps {
            sh 'trivy image mydockerhubusername/mydockerimage:mytag'
            sh 'trivy image mydockerhubusername/mydockerimage:mytag > trivy-report.txt'
        }
    }
}

Next is push the image to docker hub:

stages {
    stage('docker push') {
        steps {
            script {
                withDockerRegistry([credentialsId: 'dockerhub', toolName: 'docker']) {
                    sh 'docker push mydockerhubusername/mydockerimage:mytag'
                }
            }
        }
    }
}
  1. Next is to deploy the application using Docker Container:
stages {
    stage('deploy using docker') {
        steps {
            sh 'docker run -p 8082:8082 mydockerhubusername/mydockerimage:mytag'
        }
    }
}

Complete Pipeline

Here is the complete pipeline:

pipeline {
    agent any

    tools {
        maven "maven3"
        jdk "jdk17"
        dockerTool "docker"  // Keep the correct tool declaration for Docker
    }

    environment {
        SONARQUBE_URL = tool name: 'sonarqube-scanner' // This line should match the name set in Jenkins
    }

    stages {
        stage('Git Code Checkout') {
            steps {
                git branch: 'main', changelog: false, poll: false, url: 'https://github.com/spring-projects/spring-petclinic'
            }
        }

        stage('Code Compile') {
            steps {
                sh "mvn compile"
            }
        }

        stage('Code Unit Testing') {
            steps {
                sh "mvn test"
            }
        }

        stage('SonarQube Analysis') {
            steps {
                // Use 'withSonarQubeEnv' directly for the analysis
                withSonarQubeEnv(credentialsId: 'sonar-cred', installationName: 'sonarqube-scanner') {
                    sh '''
                    $SONARQUBE_URL/bin/sonar-scanner \
                      -Dsonar.projectKey=myproject \
                      -Dsonar.projectName=myproject \
                      -Dsonar.projectVersion=1.0 \
                      -Dsonar.sources=src \
                      -Dsonar.java.binaries=.
                    '''
                }
            }
        }

        stage('OWASP Dependency Check') {
            steps {
                dependencyCheck additionalArguments: '--scan ./', odcInstallation: 'owasp-dependency-check'
                dependencyCheckPublisher pattern: '**/dependency-check-report.xml', healthy: '0', unhealthy: '1'
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean install'
            }
        }

        stage("Docker Build") {
            steps {
                script {
                    withDockerRegistry(credentialsId: 'docker', toolName: 'docker') {
                        sh 'docker build -t howaboutpullsomeimages/mydockerimage:latest .'
                    }
                }
            }
        }

        stage("Docker Push") {
            steps {
                script {
                    withDockerRegistry(credentialsId: 'docker', toolName: 'docker') {
                        sh 'docker push howaboutpullsomeimages/mydockerimage:latest'
                    }
                }
            }
        }

        stage("Deploy Using Docker") {
            steps {
                sh 'docker run -p 8082:8082 howaboutpullsomeimages/mydockerimage:latest'
            }
        }
    }
}

Setting Up Docker

In the jenkins server/ec2 instance, install Docker with these commands:

sudo apt-get update
sudo apt-get install docker.io -y
sudo systemctl start docker
sudo docker run hello-world
sudo systemctl enable docker
docker --version
sudo usermod -a -G docker $(whoami)
newgrp docker

Setting up SonarQube

To set up SonarQube on the Jenkins server where Docker is installed, run this Docker command:

docker run -d --name sonarqube -p 9000:9000 sonarqube:lts-community

Now go to the Jenkins server browser and navigate to the IP address of the Jenkins server followed by :9000.

The default username and password are both: admin

To create a token to connect Jenkins with SonarQube:

  1. Go to Administration tab.
  2. Then go to security tab.
  3. Click on users.
  4. Click on update token.
  5. Create a token name (e.g., jenkins) and save it for Jenkins.

Now add this token in Jenkins credentials:

  1. Go to Manage Jenkins.
  2. Click on Manage Credentials.
  3. Click on global.
  4. Click on add credentials.
  5. Add credentials type as secret text.
  6. Add the token which you created in SonarQube.
  7. Add id as sonar and description as sonar.

Setting up Trivy

Install Trivy in the same EC2 instance where Jenkins is installed:

sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy

In the next part of this series, we’ll expand our CI/CD pipeline to include Kubernetes for container orchestration, providing a more scalable and robust deployment strategy.

Table of Contents