my blog

$ cat jenkins/part-01.md

Jenkins CI/CD Guide: SonarQube, Nexus, AWS EKS, and Docker

Introduction: Building CI/CD Pipelines

After getting my GitHub Actions certification, I wanted to build real pipelines, not just tutorials. I chose Jenkins because it’s still the most widely used CI/CD tool in enterprise environments (according to JetBrains DevEcosystem Survey 2024).

I built different pipelines in my home lab, progressing from basic Docker deployment to a complete enterprise toolchain integrating SonarQube for code quality, Nexus for artifact management, AWS ECR for private container registry, and AWS EKS for Kubernetes orchestration.

This technical guide shows you exactly how I integrated each tool, with real production code and detailed explanations of every technical decision.

Project 1: My First Pipeline – Simple Java Deployment

Repository: Jenkins/java-1/Project-java-1

I started with the most basic CI/CD workflow: build a Java application, create a Docker image, and run it.

The Pipeline Code:

pipeline{
    agent any
    tools{
        maven 'Maven'
    }
    options {
        buildDiscarder logRotator(daysToKeepStr: '30', numToKeepStr: '1')
    }
    environment{
        dockerhub_cred = credentials('docker-cred')
    }
    stages{
        stage('Checkout stage'){
            steps{
                checkout scmGit(branches: [[name: '*/master']], 
                    userRemoteConfigs: [[url: 'https://github.com/mohammadrezachegini/Node-JS-Jenkins']])
            }
        }
        stage('Maven Build'){
            steps{
                sh 'mvn package'
            }
        }
        stage('docker build'){
            steps{
                sh "docker build -t lowyiiii/project-docker:${BUILD_NUMBER} ."
            }
        }
        stage('dockerhub push'){
            steps{
                sh "echo $dockerhub_cred_PSW | docker login -u $dockerhub_cred_USR --password-stdin"
                sh "docker push lowyiiii/project-docker:${BUILD_NUMBER}"
            }
        }
        stage('Docker run'){
            steps{
                sh "docker run -d -p 80:8080 --name addressbook lowyiiii/project-docker:${BUILD_NUMBER}"
            }
        }
    }
}

What I Learned:

1. Jenkins Credentials Are Powerful

environment{
    dockerhub_cred = credentials('docker-cred')
}

When I use credentials(‘docker-cred’), Jenkins automatically creates two environment variables:

  • $dockerhub_cred_USR = my DockerHub username
  • $dockerhub_cred_PSW = my DockerHub password

This means I never put passwords in my code. Everything stays secure in Jenkins credential store.

2. Build Numbers for Version Tracking

sh "docker build -t lowyiiii/project-docker:${BUILD_NUMBER} ."

${BUILD_NUMBER} is a Jenkins variable that increases automatically (1, 2, 3…). So my Docker images become:

  • lowyiiii/project-docker:1
  • lowyiiii/project-docker:2
  • lowyiiii/project-docker:3

This helps me track which version is deployed where.

3. Secure Docker Login

sh "echo $dockerhub_cred_PSW | docker login -u $dockerhub_cred_USR --password-stdin"

I use –password-stdin instead of putting password directly in command. This is important because Jenkins shows all shell commands in the console output. With –password-stdin, my password doesn’t appear in the logs.

My Mistake: In the beginning, I tried to run the container on port 80, but sometimes another container was already using that port. I had to add cleanup steps to remove old containers first.

Project 2: Moving to Kubernetes – Cloud Native Deployment

Repository: Jenkins/java-2/Project-java-2

After learning the basics, I wanted to deploy to Kubernetes on AWS EKS. This is closer to how real companies work.

The Updated Pipeline:

pipeline{
    agent any
    tools{
        maven 'Maven'
    }
    environment{
        cred = credentials('aws-key')
        dockerhub_cred = credentials('docker-cred')
        DOCKER_IMAGE = "lowyiiii/project-k8s"
        DOCKER_TAG = "$BUILD_NUMBER"
    }
    stages{
        stage('Checkout stage'){
            steps{
                checkout scmGit(branches: [[name: '*/master']], 
                    userRemoteConfigs: [[url: 'https://github.com/mohammadrezachegini/Node-JS-Jenkins']])
            }
        }
        stage('Maven Build'){
            steps{
                sh 'mvn package'
            }
        }
        stage('Docker build'){
            steps{
                sh 'docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} .'
            }
        }
        stage('DockerHub push'){
            steps{
                sh "echo $dockerhub_cred_PSW | docker login -u $dockerhub_cred_USR --password-stdin"
                sh "docker push ${DOCKER_IMAGE}:${DOCKER_TAG}"
                sh "docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest"
                sh "docker push ${DOCKER_IMAGE}:latest"
            }
        }
        stage('Kubernetes deploy'){
            steps{
                sh 'aws eks update-kubeconfig --region us-east-1 --name devops-working'
                sh 'kubectl apply -f Application.yaml'
            }
        }
    }
}

What Changed and Why:

1. Multiple Credentials

environment{
    cred = credentials('aws-key')
    dockerhub_cred = credentials('docker-cred')
}

Now I need two credentials:

  • aws-key for accessing AWS EKS
  • docker-cred for DockerHub

The AWS credentials automatically set these environment variables:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY

2. Multi-Tag Strategy

sh "docker push ${DOCKER_IMAGE}:${DOCKER_TAG}"
sh "docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest"
sh "docker push ${DOCKER_IMAGE}:latest"

I push the same image with TWO tags:

  • lowyiiii/project-k8s:5 (build-specific)
  • lowyiiii/project-k8s:latest (always newest)

Why? If something breaks in production, I can easily rollback to project-k8s:4 (the previous working version). But for development, I can always use :latest.

3. Kubernetes Configuration

sh 'aws eks update-kubeconfig --region us-east-1 --name devops-working'
sh 'kubectl apply -f Application.yaml'

The first command configures kubectl to talk to my EKS cluster. It updates ~/.kube/config file automatically. The second command deploys my application using Kubernetes manifests.

My Challenge: Understanding the difference between docker run (Project 1) and kubectl apply (Project 2) was difficult at first. With Docker, the container runs on the Jenkins server. With Kubernetes, the container runs on EKS cluster nodes. Much more scalable!

Project 3: Enterprise Grade – Complete DevOps Toolchain

Repository: Jenkins/java-3/Project-java-3

This is my most advanced pipeline. It includes code quality analysis, artifact management, private container registry, and Kubernetes deployment. This is similar to what I see in job descriptions for DevOps positions.

The Complete Pipeline:

pipeline{
    agent any
    tools{
        maven 'Maven'
    }
    environment{
        cred = credentials('aws-key')
    }
    stages{
        stage('Checkout stage'){
            steps{
                checkout scmGit(branches: [[name: '*/master']], 
                    userRemoteConfigs: [[url: 'https://github.com/mohammadrezachegini/Node-JS-Jenkins']])
            }
        }
        stage('sonar test') {
            steps{
                script{
                    def mvn = tool 'Maven';
                    withSonarQubeEnv(installationName: 'sonarqube-server') {
                        sh "${mvn}/bin/mvn clean verify sonar:sonar -Dsonar.projectKey=superproject -Dsonar.projectName='superproject'"
                    }
                }    
            }     
        }
        stage('maven build'){
            steps{
                sh 'mvn package'
            }
        }
        stage('nexus test'){
            steps{
                nexusArtifactUploader(
                    nexusVersion: 'nexus3',
                    protocol: 'http',
                    nexusUrl: '3.145.207.162:8081',
                    groupId: 'addressbook',
                    version: '2.0-SNAPSHOT',
                    repository: 'maven-snapshots',
                    credentialsId: 'nexus',
                    artifacts: [
                        [artifactId: 'SuperProject',
                        classifier: '',
                        file: 'target/addressbook-2.0.war',
                        type: 'war']
                     ]
                )
            }
        }
        stage('Docker build'){
            steps{
                sh 'docker build -t super-project .'
            }
        }
        stage('DockerHub push'){
            steps{
                sh 'aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 381492204839.dkr.ecr.us-east-1.amazonaws.com'
                sh "docker tag super-project:latest 381492204839.dkr.ecr.us-east-1.amazonaws.com/super-project:latest"
                sh "docker push 381492204839.dkr.ecr.us-east-1.amazonaws.com/super-project:latest"
                sh "docker tag super-project:latest 381492204839.dkr.ecr.us-east-1.amazonaws.com/super-project:${BUILD_NUMBER}"
                sh "docker push 381492204839.dkr.ecr.us-east-1.amazonaws.com/super-project:${BUILD_NUMBER}"
            }
        }
        stage('Kubernetes deploy'){
            steps{
                sh 'aws eks update-kubeconfig --region us-east-1 --name super-project'
                sh 'kubectl apply -f Application.yaml'
            }
        }
    }
}

New Tools and Technical Details:

1. SonarQube for Code Quality

stage('sonar test') {
    steps{
        script{
            def mvn = tool 'Maven';
            withSonarQubeEnv(installationName: 'sonarqube-server') {
                sh "${mvn}/bin/mvn clean verify sonar:sonar -Dsonar.projectKey=superproject -Dsonar.projectName='superproject'"
            }
        }    
    }     
}

Deep Technical Explanation:

Why def mvn = tool ‘Maven’?

  • The tool step is a Jenkins pipeline function that returns the path to a configured tool
  • Jenkins Global Tool Configuration has Maven installed at a specific path (e.g., /usr/local/maven-3.8.6)
  • def mvn stores this path in a variable, so I can use it like: ${mvn}/bin/mvn
  • This ensures the pipeline uses the EXACT Maven version configured in Jenkins, not whatever mvn might be on the system PATH

Why withSonarQubeEnv block?

withSonarQubeEnv(installationName: 'sonarqube-server') {
    // commands here
}

This block does THREE critical things:

  1. Injects environment variables:
  2. Configures Maven sonar plugin automatically:
  3. Enables quality gate webhook:

What happens during mvn clean verify sonar:sonar:

Step 1: clean

  • Deletes target/ directory
  • Ensures fresh build (no cached artifacts)

Step 2: verify

  • Compiles source code (compile phase)
  • Compiles test code (test-compile phase)
  • Runs unit tests (test phase)
  • Generates JaCoCo code coverage report (verify phase)
  • Creates test reports in target/surefire-reports/
  • Generates coverage XML in target/site/jacoco/jacoco.xml

Step 3: sonar:sonar

  • Maven invokes SonarQube Scanner plugin
  • Scanner reads source files from src/main/java
  • Reads test files from src/test/java
  • Parses compiled bytecode from target/classes
  • Analyzes code coverage from JaCoCo report
  • Sends all data to SonarQube server via REST API

SonarQube Analysis Process:

  1. Static Analysis:
  2. Security Hotspots:
  3. Code Coverage:
  4. Technical Debt Calculation:
  5. Quality Gate Evaluation:

Why -Dsonar.projectKey=superproject?

  • SonarQube needs a unique identifier for each project
  • Project key is like a primary key in database
  • Format convention: organization_project or just project-name
  • Used in SonarQube URL: http://sonarqube/dashboard?id=superproject

Example SonarQube Output:

[INFO] Analysis report uploaded in 2s
[INFO] ANALYSIS SUCCESSFUL, you can browse http://localhost:9000/dashboard?id=superproject
[INFO] Quality Gate: PASSED
[INFO] New Code Coverage: 85.6%
[INFO] Technical Debt: 2d 3h
[INFO] Code Smells: 23
[INFO] Bugs: 2
[INFO] Vulnerabilities: 0

Why I use SonarQube:

Before SonarQube:

  • No visibility into code quality
  • Security vulnerabilities went undetected
  • No objective measure of code health
  • Technical debt accumulated silently

After SonarQube:

  • Every build gets quality metrics
  • Security issues caught before merge
  • Quality trends visible over time
  • Refactoring priorities are data-driven

Real example from my project: SonarQube detected:

// VULNERABILITY: SQL Injection
String query = "SELECT * FROM users WHERE id = " + userId;
statement.executeQuery(query);

// FIXED: Using PreparedStatement
String query = "SELECT * FROM users WHERE id = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setInt(1, userId);
ResultSet rs = stmt.executeQuery();

Without SonarQube, I would have deployed this vulnerability to production!

2. Nexus Artifact Repository

nexusArtifactUploader(
    nexusVersion: 'nexus3',
    protocol: 'http',
    nexusUrl: '3.145.207.162:8081',
    groupId: 'addressbook',
    version: '2.0-SNAPSHOT',
    repository: 'maven-snapshots',
    credentialsId: 'nexus',
    artifacts: [
        [artifactId: 'SuperProject',
        classifier: '',
        file: 'target/addressbook-2.0.war',
        type: 'war']
    ]
)

Deep Technical Explanation:

Nexus Repository Manager Architecture:

Nexus acts as a central binary repository for your organization. Think of it as a private npm registry, PyPI, or Maven Central, but under your control.

What each parameter means:

nexusVersion: ‘nexus3’

  • Specifies Nexus Repository Manager version (2.x vs 3.x)
  • API endpoints differ between versions
  • Nexus 3 uses REST API v1: /service/rest/v1/
  • Nexus 2 uses different URL structure

protocol: ‘http’ and nexusUrl: ‘3.145.207.162:8081’

  • Full Nexus URL becomes: http://3.145.207.162:8081
  • In production, should be https for security
  • Port 8081 is Nexus default (Tomcat embedded server)
  • Jenkins plugin constructs full upload URL: http://3.145.207.162:8081/repository/maven-snapshots/addressbook/SuperProject/2.0-SNAPSHOT/

Maven Coordinates (GAV):

groupId: 'addressbook'
artifactId: 'SuperProject'
version: '2.0-SNAPSHOT'

This creates Maven coordinates in the format:

groupId:artifactId:version
addressbook:SuperProject:2.0-SNAPSHOT

Why these coordinates matter:

  1. Unique Identification:
  2. Dependency Resolution:
  3. Storage Path:

repository: ‘maven-snapshots’

Nexus uses TWO standard repositories:

1. maven-snapshots (Development):

  • For -SNAPSHOT versions
  • Versions can be overwritten
  • Used during active development
  • Example: 2.0-SNAPSHOT updated 50 times during development

2. maven-releases (Production):

  • For release versions like 2.0, 2.1, 3.0
  • Versions are IMMUTABLE (cannot be overwritten)
  • Used for production deployments
  • Ensures reproducibility

Repository Policy Difference:

Upload SuperProject-2.0-SNAPSHOT.war → maven-snapshots ✅
Upload SuperProject-2.0-SNAPSHOT.war → maven-snapshots ✅ (overwrites)

Upload SuperProject-2.0.war → maven-releases ✅
Upload SuperProject-2.0.war → maven-releases ❌ (ERROR: already exists)

credentialsId: ‘nexus’

  • References Jenkins credential with ID ‘nexus’
  • Credential contains Nexus username/password
  • Jenkins injects credentials during upload
  • Nexus uses BASIC authentication: Authorization: Basic base64(username:password)

artifacts array:

artifacts: [
    [artifactId: 'SuperProject',
    classifier: '',
    file: 'target/addressbook-2.0.war',
    type: 'war']
]

Why array of artifacts?

  • One upload can send MULTIPLE files
  • Example: Upload JAR + sources JAR + javadoc JAR

Common artifact types:

[
    // Main artifact
    [artifactId: 'SuperProject', file: 'target/app.war', type: 'war'],
    // Sources
    [artifactId: 'SuperProject', file: 'target/app-sources.jar', type: 'jar', classifier: 'sources'],
    // Javadoc
    [artifactId: 'SuperProject', file: 'target/app-javadoc.jar', type: 'jar', classifier: 'javadoc']
]

Classifier examples:

  • sources – source code JAR
  • javadoc – documentation JAR
  • tests – test classes JAR
  • linux-x64 – platform-specific binary
  • Empty string – main artifact (default)

Upload Process (Behind the Scenes):

  1. Jenkins reads WAR file:
  2. Calculates checksums:
  3. Constructs upload URL:
  4. Creates multipart upload:
  5. Sends with authentication:
  6. Nexus processes:
  7. Metadata XML created:

Why I use Nexus:

The “Build Once, Deploy Everywhere” Principle:

Without Nexus:

Dev pipeline:  git clone → mvn package → deploy to dev
QA pipeline:   git clone → mvn package → deploy to staging
Prod pipeline: git clone → mvn package → deploy to prod

Problem: Three different WAR files!

  • Different build times = different artifacts
  • “It works in staging but fails in prod”
  • No guarantee it’s the same binary

With Nexus:

Build pipeline:  git clone → mvn package → upload to Nexus (addressbook:SuperProject:2.0-SNAPSHOT)

Dev pipeline:    Download from Nexus → deploy to dev
QA pipeline:     Download from Nexus → deploy to staging
Prod pipeline:   Download from Nexus → deploy to prod

Result: EXACT same WAR file in all environments!

Additional Nexus Benefits:

  1. Audit Trail:
  2. Rollback Capability:
  3. Dependency Proxy:
  4. Bandwidth Savings:

Real-world example:

# Build #47 uploaded to Nexus
Jenkins → Nexus: addressbook:SuperProject:2.0-SNAPSHOT

# QA tests build #47 in staging
QA: "Build #47 passes all tests ✅"

# Deploy THE EXACT SAME build #47 to production
Ops: Downloads addressbook:SuperProject:2.0-SNAPSHOT (build #47) from Nexus
Ops: Deploys to production

# Build #48 has a bug
Jenkins → Nexus: addressbook:SuperProject:2.0-SNAPSHOT (build #48)
Deploy to dev → BUG DETECTED 🐛

# Production is SAFE because it still runs build #47
# Nexus preserved build #47, so we can:
# 1. Keep prod on #47 (stable)
# 2. Fix bug in #49
# 3. Test #49
# 4. Deploy #49 when ready

This is why Netflix, Google, Amazon all use artifact repositories!

3. AWS ECR Instead of DockerHub

sh 'aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 381492204839.dkr.ecr.us-east-1.amazonaws.com'
sh "docker tag super-project:latest 381492204839.dkr.ecr.us-east-1.amazonaws.com/super-project:latest"
sh "docker push 381492204839.dkr.ecr.us-east-1.amazonaws.com/super-project:latest"

Technical explanation:

  • aws ecr get-login-password generates a temporary password (valid for 12 hours)
  • 381492204839 is my AWS account ID
  • dkr.ecr.us-east-1.amazonaws.com is the ECR registry URL in us-east-1 region
  • I push with latest tag AND build number tag

Why ECR instead of DockerHub:

  • Private by default (DockerHub free tier is public)
  • Integrated with AWS IAM (better security)
  • Faster pulls from EKS (same AWS region)
  • No DockerHub rate limits

The Complete Flow Visualized

Here’s what happens when I push code:

1. GitHub Push
   ↓
2. Jenkins detects change
   ↓
3. Checkout code from GitHub
   ↓
4. SonarQube analyzes code quality
   ↓ (if quality gate passes)
5. Maven compiles and tests
   ↓
6. Maven creates WAR file
   ↓
7. Upload WAR to Nexus
   ↓
8. Docker builds image with WAR
   ↓
9. Push image to AWS ECR
   ↓
10. Update kubeconfig for EKS
   ↓
11. kubectl applies Kubernetes manifests
   ↓
12. Application running on EKS!

Key Lessons I Learned

1. Start Simple, Add Complexity Gradually

I didn’t build project-3 first. I went:

  • Project 1: Basic Docker (5 stages)
  • Project 2: Add Kubernetes (5 stages)
  • Project 3: Add SonarQube + Nexus + ECR (7 stages)

Each project taught me something new without overwhelming me.

2. Credentials Management is Critical

environment{
    cred = credentials('aws-key')
    dockerhub_cred = credentials('docker-cred')
}

I store ALL credentials in Jenkins. My code has ZERO hardcoded passwords. This is how you prevent security leaks.

3. Build Once, Deploy Everywhere

With Nexus, I build my WAR file once and store it. Then I can deploy that exact file to:

  • Development environment
  • Staging environment
  • Production environment

Same artifact, different configs. This is a professional practice.

4. Multi-Tag Strategy Saves Time

sh "docker push ${DOCKER_IMAGE}:${DOCKER_TAG}"
sh "docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest"
sh "docker push ${DOCKER_IMAGE}:latest"
  • :latest for development (always newest)
  • :5 for production (specific version for rollback)

5. Quality Gates Prevent Bad Code

SonarQube integration means:

  • No security vulnerabilities go to production
  • Code coverage must be above threshold
  • Technical debt is tracked

Before SonarQube, I didn’t know my code had problems!

My Current Setup

Jenkins Master Node:

  • Coordinates all builds
  • Stores credentials
  • Manages build history

External Services I Integrated:

  • SonarQube (port 9000) for code quality
  • Nexus (port 8081) for artifact storage
  • AWS ECR for private Docker registry
  • AWS EKS for Kubernetes orchestration

Build Retention:

  • Keep builds for 30 days
  • Keep last 2 builds
  • Automatic cleanup to save disk space

All code is available on my GitHub (Link on BIO)

$ ls -t root-access/ | head -3

Hello world! uncategorized 1 min Building a Job Board API, Part 1: The Foundation job-board-api · 1/1 16 min Part 13 – Kubernetes Autoscaling in Action: A FastAPI Microservice That Scales Itself home-lab · 13/13 6 min

Leave a comment

Your email address will not be published. Required fields are marked *