Operations 12 min read

Comprehensive CI/CD Pipeline Tutorial with Jenkins, GitLab Webhooks, GitOps, and Kubernetes Deployment

This article provides a detailed tutorial on designing CI/CD pipelines using Jenkins, GitLab webhooks, and GitOps practices, covering single- and multi-application workflow designs, Jenkinsfile configurations for build, test, SonarQube scanning, Docker image creation, and Kubernetes deployment with Helm.

DevOps Cloud Academy
DevOps Cloud Academy
DevOps Cloud Academy
Comprehensive CI/CD Pipeline Tutorial with Jenkins, GitLab Webhooks, GitOps, and Kubernetes Deployment

The guide walks through the design of CI/CD pipelines for both single‑application and multi‑application environments, illustrating how to prepare a code repository and outline the CI workflow steps such as fetching the correct commit, building, testing, scanning, and image creation.

It explains how to use GitLab WebHook to trigger Jenkins jobs, parse the JSON payload to identify the changed service modules, and then launch the appropriate CI jobs for each module.

pipeline {
    agent any
    stages {
        stage("GetData") {
            steps {
                script {
                    echo "${webHookData}"
                    data = readJSON text: "${webHookData}"
                    env.branchName = data.ref - "refs/heads/"
                    env.commitId = data.checkout_sha
                    env.projectId = data.project_id
                    commits = data["commits"]
                    changeServices = []
                    for (commit in commits) {
                        for (add in commit.added) {
                            s = add.split("/") as List
                            if (s.size() > 1 && changeServices.indexOf(s[0]) == -1) {
                                changeServices.add(s[0])
                            }
                        }
                        for (m in commit.modified) {
                            s = m.split("/") as List
                            if (s.size() > 1 && changeServices.indexOf(s[0]) == -1) {
                                changeServices.add(s[0])
                            }
                        }
                        for (r in commit.removed) {
                            s = r.split("/") as List
                            if (s.size() > 1 && changeServices.indexOf(s[0]) == -1) {
                                changeServices.add(s[0])
                            }
                        }
                    }
                    println(changeServices)
                }
            }
        }
        stage('DefineService') {
            steps {
                script {
                    services = ['service02', 'service01']
                    for (service in services) {
                        if (changeServices.indexOf(service) != -1) {
                            jobName = "microservicecicd-${service}-service-CI"
                            build job: jobName, wait: false, parameters: [
                                string(name: 'branchName', value: "${env.branchName}"),
                                string(name: 'commitId', value: "${env.commitId}"),
                                string(name: 'projectId', value: "${env.projectId}")
                            ]
                        }
                    }
                }
            }
        }
    }
}

The CI job Jenkinsfile further defines stages for code checkout, Maven build & test, SonarQube analysis, Docker image creation, and pushing the image to a private registry.

pipeline {
    agent { node { label "build" } }
    stages {
        stage('GetCode') {
            steps {
                script {
                    checkout([
                        $class: 'GitSCM',
                        branches: [[name: "${branchName}"]],
                        extensions: [[
                            $class: 'SparseCheckoutPaths',
                            sparseCheckoutPaths: [[path: "${moduleName}"], [path: 'Dockerfile']]
                        ]],
                        userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "${srcUrl}"]]
                    ])
                }
            }
        }
        stage('Build&Test') {
            steps {
                script {
                    sh """
                    cd ${moduleName}
                    mvn clean package
                    """
                }
            }
            post { always { junit "${moduleName}/target/surefire-reports/*.xml" } }
        }
        stage('SonarScan') {
            steps {
                script {
                    sonarDate = sh(returnStdout: true, script: 'date +%Y%m%d%H%M%S').trim()
                    withCredentials([string(credentialsId: 'sonar-admin-user', variable: 'sonartoken'),
                                     string(credentialsId: 'gitlab-user-token', variable: 'gitlabtoken')]) {
                        sh """
                        cd ${moduleName}
                        sonar-scanner \
                          -Dsonar.projectKey=${JOB_NAME} \
                          -Dsonar.projectName=${JOB_NAME} \
                          -Dsonar.projectVersion=${sonarDate} \
                          -Dsonar.sources=src \
                          -Dsonar.host.url=http://sonar.idevops.site \
                          -Dsonar.login=${sonartoken} \
                          -Dsonar.gitlab.commit_sha=${commitId} \
                          -Dsonar.gitlab.ref_name=${branchName} \
                          -Dsonar.gitlab.project_id=${projectId} \
                          -Dsonar.gitlab.url=http://gitlab.idevops.site \
                          -Dsonar.gitlab.user_token=${gitlabtoken}
                        """
                    }
                }
            }
        }
        stage('BuildImage') {
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: 'aliyun-registry-admin', passwordVariable: 'password', usernameVariable: 'username')]) {
                        env.nowDate = sh(returnStdout: true, script: 'date +%Y%m%d%H%M%S').trim()
                        env.releaseVersion = "${env.branchName}"
                        env.imageTag = "${releaseVersion}-${env.nowDate}-${commitId}"
                        env.dockerImage = "registry.cn-beijing.aliyuncs.com/microservicecicd/microservicecicd-${moduleName}-service:${env.imageTag}"
                        env.jarName = "${moduleName}-${branchName}-${commitId}"
                        sh """
                        docker login -u ${username} -p ${password} registry.cn-beijing.aliyuncs.com
                        cd ${moduleName} && docker build -t ${dockerImage} -f ../Dockerfile --build-arg SERVICE_NAME=${jarName} .
                        docker push ${dockerImage}
                        docker rmi ${dockerImage}
                        """
                    }
                }
            }
        }
    }
}

For GitOps integration, an additional stage "PushFile" updates environment configuration files in a separate Git repository after the image is pushed, ensuring the deployment manifests reference the new image tag.

stage("PushFile") {
    steps {
        script {
            if ("${env.branchName}".contains("RELEASE-")) {
                env.branchName = "master"
            } else {
                env.branchName = "feature"
            }
            for (i = 0; i < 3; i++) {
                response = GetRepoFile(40, "${moduleName}%2fvalues.yaml", "${env.branchName}")
                yamlData = readYaml text: "${response}"
                yamlData.image.version = "${releaseVersion}-${env.nowDate}"
                yamlData.image.commit = "${commitId}"
                writeYaml file: 'test.yaml', data: yamlData, charset: 'UTF-8'
                newYaml = sh(returnStdout: true, script: 'cat test.yaml').trim()
                base64Content = newYaml.bytes.encodeBase64().toString()
                try {
                    UpdateRepoFile(40, "${moduleName}%2fvalues.yaml", base64Content, "${env.branchName}")
                    break
                } catch (e) {
                    sh "sleep 2"
                    continue
                }
            }
        }
    }
}

The CD pipeline mirrors the CI flow: a webhook‑triggered CD‑scheduler job parses the GitLab payload, determines which services changed, and then runs Helm commands to deploy or upgrade the services in a Kubernetes namespace.

pipeline {
    agent any
    stages {
        stage('GetCommitService') {
            steps {
                script {
                    webhookdata = readJSON text: "${WebHookData}"
                    changeServices = []
                    for (commit in webhookdata["commits"]) {
                        for (add in commit.added) {
                            s = add.split("/") as List
                            if (s.size() > 1 && changeServices.indexOf(s[0]) == -1) changeServices.add(s[0])
                        }
                        for (m in commit.modified) {
                            s = m.split("/") as List
                            if (s.size() > 1 && changeServices.indexOf(s[0]) == -1) changeServices.add(s[0])
                        }
                        for (r in commit.removed) {
                            s = r.split("/") as List
                            if (s.size() > 1 && changeServices.indexOf(s[0]) == -1) changeServices.add(s[0])
                        }
                    }
                }
            }
        }
        stage('DefineService') {
            steps {
                script {
                    services = ['service02', 'service01']
                    for (service in services) {
                        if (changeServices.indexOf(service) != -1) {
                            jobName = "microservicecicd-${service}-service-CD"
                            build job: jobName, wait: false, parameters: [string(name: 'branchName', value: "${branchName}")]
                        }
                    }
                }
            }
        }
    }
}

Finally, the CD job checks out the environment repository, creates the target namespace if needed, and uses Helm to install or upgrade the service, followed by listing releases and history for verification.

pipeline {
    agent { node { label "k8s" } }
    stages {
        stage('GetCode') {
            steps {
                script {
                    checkout([
                        $class: 'GitSCM',
                        branches: [[name: "${env.branchName}"]],
                        extensions: [[
                            $class: 'SparseCheckoutPaths',
                            sparseCheckoutPaths: [[path: "${serviceName}"]]
                        ]],
                        userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "http://gitlab.idevops.site/microservicecicd/microservicecicd-env.git"]]
                    ])
                }
            }
        }
        stage('HelmDeploy') {
            steps {
                script {
                    sh """
                    kubectl create ns ${nameSpace}-uat || echo false
                    helm install ${serviceName} --namespace ${nameSpace}-uat ./ ${serviceName} || helm upgrade ${serviceName} --namespace ${nameSpace}-uat ./ ${serviceName}
                    helm list --namespace ${nameSpace}-uat
                    helm history ${serviceName} --namespace ${nameSpace}-uat
                    """
                }
            }
        }
    }
}
dockerci/cdkubernetesDevOpsgitlabGitOpsJenkins
DevOps Cloud Academy
Written by

DevOps Cloud Academy

Exploring industry DevOps practices and technical expertise.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.