Home / CI/CD Automation
Jenkins & Groovy Architecture

Jenkins Scripted Pipeline Guide: Groovy Architecture from Beginner to Enterprise 🛠️

A hands-on engineering guide to mastering Jenkins Scripted Pipelines. Learn the Groovy execution model, build basic continuous integration workflows, and construct production-ready multi-stage deployment engines with error handling and shared libraries.

Published by AHP • cicdclouds.com

1. The Core Fundamentals: Scripted vs. Declarative Pipelines

In the Jenkins ecosystem, Pipeline-as-Code allows development teams to define their complete build, test, and release cycle inside a version-controlled file called a Jenkinsfile. Jenkins provides two distinct syntactical styles:

  • Declarative Pipeline (pipeline { ... }): An opinionated, highly structured configuration format. It enforces predefined blocks (such as agent, stages, steps, and post) designed for standard application delivery with built-in validation.
  • Scripted Pipeline (node { ... }): A general-purpose Domain-Specific Language (DSL) executed directly on the Apache Groovy runtime. It places minimal restrictions on structure, granting developers complete programmatic control to utilize loops, conditional branching, try-catch blocks, and dynamic runtime computations.

Why Choose Scripted Pipelines?

While Declarative is recommended for straightforward projects, Scripted Pipelines shine in complex enterprise scenarios where:

  1. Build stages must be generated dynamically based on an external matrix or dynamic environment payload.
  2. Custom exception handling and granular rollback logic are required at each step.
  3. Teams require direct access to JVM objects, file system handles, and external API responses during pipeline orchestration.

2. Understanding Groovy Execution & The CPS Interpreter

Jenkins executes Scripted Pipelines using a specialized engine known as the CPS (Continuation-Passing Style) Interpreter. The CPS engine serializes the complete state of the pipeline to disk at each step boundary. If the Jenkins master crashes or restarts mid-build, the pipeline can resume precisely where it stopped.

Critical Groovy Syntax Conventions in Jenkins

  • Closures: Code blocks passed as arguments (surrounded by curly braces { ... }) that define stages or execution scopes.
  • String Interpolation: Single-quoted strings ('echo hello') are plain text literals, whereas double-quoted strings ("echo ${BRANCH_NAME}") evaluate dynamic Groovy expressions.
  • Serialization Safety: Because state is saved to disk continuously, non-serializable objects (such as raw database connections, iterators, or open file streams) must not be kept open across pipeline steps.

3. Simple Project: Clean Java Maven CI Pipeline

Let us begin with a clear, functional Scripted Pipeline. This project runs on an agent labeled linux-build-agent, checks out source code from Git, compiles a Java project with Maven, and runs automated JUnit tests with cleanup handling.

Simple Project Jenkinsfile

node('linux-build-agent') {
    // Variable declarations
    def mvnHome = tool 'Maven-3.9'
    
    try {
        stage('1. Checkout Code') {
            echo "Checking out application source code from repository..."
            checkout scm
        }

        stage('2. Build Application') {
            echo "Compiling Java sources..."
            sh "${mvnHome}/bin/mvn clean compile"
        }

        stage('3. Run Tests & Collect Reports') {
            echo "Executing unit tests..."
            sh "${mvnHome}/bin/mvn test"
            
            // Archive test results into Jenkins UI
            junit 'target/surefire-reports/*.xml'
        }

        stage('4. Package Artifact') {
            echo "Packaging JAR artifact..."
            sh "${mvnHome}/bin/mvn package -DskipTests=true"
            
            // Archive build binary
            archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
        }
    } catch (Exception err) {
        currentBuild.result = 'FAILURE'
        echo "Build failed with error: ${err.getMessage()}"
        throw err
    } finally {
        echo "Performing workspace cleanup..."
        cleanWs()
    }
}

Walkthrough of Key Steps:

  • node('label'): Allocates an executor on a matching build node and establishes a dedicated workspace.
  • tool 'Maven-3.9': Dynamically looks up and injects the installation path of Maven configured in Global Tool Configuration.
  • try-catch-finally: Ensures that even if the compile or test step fails, cleanWs() is invoked in the finally block to prevent lingering build bloat on the agent.

4. Advanced Enterprise Project: DevSecOps & Kubernetes Deployment

In a modern enterprise ecosystem, our pipeline must do far more than compile code. It must:

  1. Run parallel code quality and static security analysis (SonarQube).
  2. Containerize the validated application via Docker and push it to a private registry with strict commit-hash immutability.
  3. Execute automated vulnerability scanning using Trivy.
  4. Deploy to an Amazon EKS / Kubernetes cluster with automated rolling status checks and immediate rollbacks on failure.
  5. Dispatch contextual Slack / Webhook notifications.

Production-Grade Enterprise Jenkinsfile

node('docker-agent') {
    // Scoped pipeline variables
    def appImage
    def gitCommit
    def imageTag
    def dockerRegistry = "https://registry.hub.docker.com"
    def registryCreds  = "dockerhub-production-creds"
    def k8sNamespace   = "production"

    // Wrap execution with a hard 60-minute timeout
    timeout(time: 60, unit: 'MINUTES') {
        try {
            stage('1. SCM Extraction & Metadata') {
                checkout scm
                // Retrieve Git short hash for reproducible, immutable container tagging
                gitCommit = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
                imageTag = "cicdclouds/enterprise-app:${gitCommit}"
                echo "Target deployment version identified: ${imageTag}"
            }

            stage('2. Parallel Quality & Security Gates') {
                parallel(
                    'Unit & Integration Testing': {
                        echo "Executing Maven test suite..."
                        sh 'mvn clean test -B'
                        junit 'target/surefire-reports/*.xml'
                    },
                    'SonarQube Static Analysis': {
                        echo "Running static security and code smell scanning..."
                        withSonarQubeEnv('SonarQube-Enterprise-Server') {
                            sh 'mvn sonar:sonar -Dsonar.projectKey=cicdclouds-enterprise-app'
                        }
                    }
                )
            }

            stage('3. Quality Gate Check') {
                // Halt pipeline if SonarQube quality gate fails (Coverage < 80%, Vulnerabilities > 0)
                timeout(time: 5, unit: 'MINUTES') {
                    def qg = waitForQualityGate()
                    if (qg.status != 'OK') {
                        error "Pipeline halted! Quality Gate check failed with status: ${qg.status}"
                    }
                }
            }

            stage('4. Docker Build & Vulnerability Scan') {
                echo "Compiling container image..."
                docker.withRegistry(dockerRegistry, registryCreds) {
                    appImage = docker.build(imageTag, "--build-arg JAR_FILE=target/*.jar .")
                    
                    echo "Running Trivy vulnerability scanning on container..."
                    sh """
                        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
                          aquasec/trivy:latest image --severity CRITICAL --exit-code 1 ${imageTag}
                    """
                    
                    echo "Publishing verified container image..."
                    appImage.push()
                    appImage.push('latest')
                }
            }

            stage('5. Kubernetes Deployment & Verification') {
                withCredentials([file(credentialsId: 'k8s-kubeconfig-prod', variable: 'KUBECONFIG')]) {
                    echo "Rolling out new release to Kubernetes cluster (${k8sNamespace})..."
                    sh """
                        kubectl set image deployment/enterprise-app-deploy \
                          enterprise-app=${imageTag} -n ${k8sNamespace}
                    """

                    // Monitor rollout health with automatic timeout
                    def rolloutStatus = sh(
                        script: "kubectl rollout status deployment/enterprise-app-deploy -n ${k8sNamespace} --timeout=180s",
                        returnStatus: true
                    )

                    if (rolloutStatus != 0) {
                        echo "ALERT: Pods failed to become healthy. Triggering automated rollback!"
                        sh "kubectl rollout undo deployment/enterprise-app-deploy -n ${k8sNamespace}"
                        error "Deployment failed! Rollback executed successfully."
                    }
                }
            }

            stage('6. Notification') {
                echo "Deployment successfully verified across all pods!"
                // Optional: slackSend channel: '#deployments', color: 'good', message: "Release ${imageTag} succeeded."
            }

        } catch (Exception err) {
            currentBuild.result = 'FAILURE'
            echo "Critical Pipeline Error: ${err.getMessage()}"
            // Optional: slackSend channel: '#deployments', color: 'danger', message: "Build Failed: ${err.getMessage()}"
            throw err
        } finally {
            echo "Sanitizing agent workspace and clearing Docker artifacts..."
            sh "docker rmi ${imageTag} || true"
            cleanWs()
        }
    }
}

5. Scaling Jenkins: Modularizing with Shared Libraries

In an enterprise managing dozens or hundreds of microservices, duplicating raw Groovy code across every repository leads to technical debt. The standard DevOps practice is to abstract repeated logic into a Jenkins Shared Library.

Standard Shared Library Directory Structure

(root)
+- src/                     # Traditional Groovy source files (OOP Helper Classes)
|   +- com/cicdclouds/k8s/
|       +- ClusterDeployer.groovy
+- vars/                    # Global variables and custom DSL steps
|   +- buildJavaApp.groovy
|   +- notifySlack.groovy
+- resources/               # Static non-Groovy files (JSON templates, SQL seeds)

Example Custom Step (vars/deployK8s.groovy)

def call(Map config = [:]) {
    String namespace = config.namespace ?: 'production'
    String deployment = config.deployment
    String image = config.image

    withCredentials([file(credentialsId: 'k8s-kubeconfig-prod', variable: 'KUBECONFIG')]) {
        sh """
            kubectl set image deployment/${deployment} ${deployment}=${image} -n ${namespace}
            kubectl rollout status deployment/${deployment} -n ${namespace} --timeout=120s
        """
    }
}

Now, any team's Jenkinsfile becomes exceptionally concise and maintainable:

@Library('cicdclouds-shared-library@v2.1.0') _

node('docker-agent') {
    stage('Deploy') {
        deployK8s(
            deployment: 'order-service',
            image: "cicdclouds/order-service:${env.BUILD_NUMBER}",
            namespace: 'production'
        )
    }
}

6. Production Troubleshooting & Groovy Anti-Patterns

When developing Jenkins Scripted Pipelines at enterprise scale, engineers regularly encounter common architectural pitfalls:

1. The java.io.NotSerializableException

Root Cause: Because Jenkins CPS continuously serializes local state, creating non-serializable variables (such as direct instances of java.util.Scanner, open file descriptors, or non-serializable third-party objects) inside pipeline steps causes the build to fail with a serialization exception.

Remedy: Annotate helper functions that use complex local calculations with @NonCPS, or discard the non-serializable reference before invoking the next pipeline step.

2. Workspace Disk Exhaustion (Zombie Files)

Root Cause: If pipelines terminate abruptly or omit workspace cleanup, gigabytes of compiled binaries, node modules, and Docker cache layers accumulate on executor disks.

Remedy: Always wrap pipeline bodies in a try-finally block and place cleanWs() inside the finally section.

3. Long-Running Shell Commands Without Timeouts

Root Cause: If a deployment script encounters an unresponsive remote host or waits on an unacknowledged prompt, the build thread hangs indefinitely, tying up an executor slot.

Remedy: Wrap individual external network calls or the global execution with a strict timeout(time: N, unit: 'MINUTES') block.