Jenkins Pipeline Fundamentals

Module 28: DevOps & Deployment - Monday, Lecture 3

Introduction to Jenkins

In our previous lectures, we explored CI/CD concepts and GitHub Actions. Now, we turn to Jenkins, one of the most established and versatile CI/CD platforms in the industry. Think of Jenkins as the Swiss Army knife of automation tools—highly configurable, with a vast ecosystem of plugins and extensions.

While GitHub Actions is tightly integrated with GitHub repositories, Jenkins is a standalone application that can connect to almost any source control system and can be deployed on your own infrastructure, giving you complete control over your CI/CD environment.

flowchart TD A[Source Code Repositories] --> B[Jenkins Server] B --> C[Build Agents] C --> D[Test Environments] D --> E[Deployment Targets] B --> F[Email Notifications] B --> G[Chat Integrations] B --> H[Monitoring Systems]

Jenkins vs. GitHub Actions

Feature Jenkins GitHub Actions
Hosting Self-hosted Cloud-hosted (GitHub)
Setup Complexity Higher (installation, configuration) Lower (built into GitHub)
Extensibility 1,800+ plugins Marketplace actions
SCM Integration Multiple SCMs GitHub-focused
Customization Highly customizable Limited to YAML configuration
Infrastructure Control Complete control Limited to GitHub-provided runners or self-hosted runners

Jenkins Architecture

Before diving into Jenkins Pipelines, it's important to understand the overall architecture of Jenkins.

flowchart TD A[Controller] --> B[Agent 1] A --> C[Agent 2] A --> D[Agent 3] A --> E[Web UI] A --> F[REST API] A --> G[Pipeline Engine] A --> H[Plugin Manager] B --> I[Build Jobs] C --> J[Test Jobs] D --> K[Deploy Jobs]

Key Components

Installation and Setup

While we won't go through the installation process in detail, here are the basic steps:

  1. Install Java Development Kit (JDK) on your server
  2. Download and install Jenkins (via package manager, Docker, or war file)
  3. Access the web interface (typically http://localhost:8080)
  4. Complete the initial setup wizard
  5. Install recommended plugins
  6. Create admin user and configure security
  7. Configure system settings (JDK, Maven, etc.)
  8. Set up build agents if needed
# Docker quick start example
docker run -d -p 8080:8080 -p 50000:50000 \
  -v jenkins_home:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --restart unless-stopped \
  --name jenkins jenkins/jenkins:lts

Jenkins Pipeline Introduction

Jenkins Pipeline is a suite of plugins that supports implementing and integrating continuous delivery pipelines into Jenkins. Instead of creating multiple individual jobs, a pipeline defines the entire delivery process as code.

Pipeline Types

Jenkins offers two syntax options for defining pipelines:

graph TD A[Jenkins Pipeline] --> B[Declarative Pipeline] A --> C[Scripted Pipeline] B --> B1[YAML syntax] B --> B2[Structured sections] B --> B3[Limited flexibility] B --> B4[Easier to learn] C --> C1[Groovy syntax] C --> C2[Programmatic flow] C --> C3[High flexibility] C --> C4[Steeper learning curve]

Pipeline Concepts

Basic Declarative Pipeline Syntax

pipeline {
    agent any
    
    stages {
        stage('Build') {
            steps {
                echo 'Building..'
                sh 'npm install'
                sh 'npm run build'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
                sh 'npm test'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
                sh 'npm run deploy'
            }
        }
    }
    
    post {
        always {
            echo 'This will always run'
        }
        success {
            echo 'Build successful!'
        }
        failure {
            echo 'Build failed!'
        }
    }
}

Creating Your First Pipeline

To create a pipeline in Jenkins:

  1. Navigate to Jenkins dashboard and click "New Item"
  2. Enter a name and select "Pipeline" as the item type
  3. Configure the pipeline definition (either directly or by pointing to a Jenkinsfile in SCM)
  4. Save and run the pipeline

Declarative Pipeline in Depth

Declarative Pipeline provides a more structured and opinionated syntax, making it easier to get started and maintain consistent pipelines.

Pipeline Structure

pipeline {
    // Where to execute the pipeline
    agent { ... }
    
    // Pipeline-wide options
    options { ... }
    
    // Environment variables 
    environment { ... }
    
    // Pipeline parameters
    parameters { ... }
    
    // Tools to auto-install and put on PATH
    tools { ... }
    
    // Triggers for running the pipeline
    triggers { ... }
    
    // Stages of the pipeline
    stages {
        stage('Name') { ... }
        stage('Another') { ... }
    }
    
    // Post-execution actions
    post { ... }
}

Agent Configuration

The agent directive specifies where the pipeline, or a specific stage, will execute:

// Execute on any available agent
agent any

// Execute on a specific agent
agent { label 'my-agent' }

// Execute in a Docker container
agent {
    docker {
        image 'node:18-alpine'
        args '-v /tmp:/tmp'
    }
}

// Execute with a Docker image for each stage
agent {
    dockerfile {
        filename 'Dockerfile.build'
        additionalBuildArgs '--build-arg VERSION=1.0.0'
    }
}

// No global agent, specify at stage level
agent none

Environment Variables

Define environment variables for the pipeline:

environment {
    // Simple variable
    DEBUG_MODE = 'true'
    
    // Credentials from Jenkins credentials store
    AWS_CREDS = credentials('aws-key')
    
    // Dynamic variable using script
    BUILD_NUMBER_COMBINED = "${env.BUILD_NUMBER}-${Math.round(Math.random() * 1000)}"
}

Stages and Steps

Stages organize the pipeline into discrete sections, while steps define the actual work:

stages {
    stage('Build') {
        steps {
            // Shell command (platform-specific)
            sh 'npm install'
            
            // Windows batch command
            bat 'npm run build'
            
            // Print message
            echo 'Building application...'
            
            // Multi-line script
            sh '''
                echo Starting build process
                mkdir -p build
                npm run build
                echo Build completed
            '''
        }
    }
    
    stage('Test') {
        steps {
            // Run script from workspace
            sh './run_tests.sh'
            
            // Archive test results
            junit 'test-results/*.xml'
        }
    }
}

Conditional Execution

Use conditionals to control when stages or steps execute:

stage('Deploy to Production') {
    when {
        // Only execute on main branch
        branch 'main'
        
        // Only when parameter is true
        expression { params.DEPLOY_TO_PROD == true }
        
        // All conditions must be true
        allOf {
            branch 'main'
            environment name: 'DEPLOY_TO_PROD', value: 'true'
        }
        
        // Any condition must be true
        anyOf {
            branch 'main'
            branch 'release'
        }
    }
    
    steps {
        // Deploy steps here
    }
}

Post-Execution Actions

The post section defines actions to run at the end of the pipeline or stage:

post {
    // Always execute, regardless of build status
    always {
        echo 'This will always run'
        cleanWs() // Clean workspace
    }
    
    // Execute on successful build
    success {
        echo 'Build successful!'
        emailext subject: 'Build Successful',
                 body: 'Your build has completed successfully.',
                 to: 'team@example.com'
    }
    
    // Execute on failed build
    failure {
        echo 'Build failed!'
        slackSend channel: '#builds',
                  color: 'danger',
                  message: "Build failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
    }
    
    // Execute when build status changes from failure to success
    fixed {
        echo 'Build fixed!'
    }
    
    // More post conditions: unstable, regression, aborted, changed
}

Advanced Pipeline Features

Parallel Execution

Run stages or steps in parallel to speed up the pipeline:

stage('Parallel Tests') {
    parallel {
        stage('Unit Tests') {
            steps {
                sh 'npm run test:unit'
            }
        }
        stage('Integration Tests') {
            steps {
                sh 'npm run test:integration'
            }
        }
        stage('E2E Tests') {
            steps {
                sh 'npm run test:e2e'
            }
        }
    }
}
graph TD A[Parallel Tests] --> B[Unit Tests] A --> C[Integration Tests] A --> D[E2E Tests] B --> E[Next Stage] C --> E D --> E

Shared Libraries

Reuse code across multiple pipelines with shared libraries:

// In Jenkinsfile
@Library('my-shared-library') _

pipeline {
    agent any
    
    stages {
        stage('Build') {
            steps {
                // Call a function from the shared library
                buildNodeApp()
            }
        }
    }
}

In your shared library (vars/buildNodeApp.groovy):

def call(Map config = [:]) {
    def nodeVersion = config.nodeVersion ?: '18'
    
    sh "nvm use ${nodeVersion}"
    sh 'npm ci'
    sh 'npm run build'
    
    if (config.runTests) {
        sh 'npm test'
    }
}

Matrix Builds

Test across multiple configurations simultaneously:

stage('Matrix Tests') {
    matrix {
        axes {
            axis {
                name 'NODE_VERSION'
                values '16', '18', '20'
            }
            axis {
                name 'BROWSER'
                values 'chrome', 'firefox', 'safari'
            }
        }
        
        excludes {
            exclude {
                axis {
                    name 'NODE_VERSION'
                    values '16'
                }
                axis {
                    name 'BROWSER'
                    values 'safari'
                }
            }
        }
        
        stages {
            stage('Test') {
                steps {
                    sh "npm test -- --browser=${BROWSER} --node=${NODE_VERSION}"
                }
            }
        }
    }
}

Input Steps

Pause the pipeline for human interaction:

stage('Approve Deployment') {
    steps {
        // Pause for manual approval
        input message: 'Deploy to production?',
              ok: 'Approve',
              submitter: 'admin,devops',
              parameters: [
                  choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Select deployment environment')
              ]
    }
}

Stashing and Unstashing

Pass files between stages or agents:

stage('Build') {
    steps {
        sh 'npm run build'
        // Save build artifacts for later stages
        stash includes: 'dist/**', name: 'app-build'
    }
}

stage('Deploy') {
    agent { label 'deploy-agent' }
    steps {
        // Retrieve previously stashed files
        unstash 'app-build'
        sh 'rsync -avz dist/ server:/var/www/html/'
    }
}

Scripted Pipeline

While Declarative Pipeline is recommended for most use cases, Scripted Pipeline provides more flexibility for complex workflows.

Basic Scripted Pipeline Syntax

node('my-agent') {
    // SCM checkout
    checkout scm
    
    // Environment variables
    def nodeVersion = '18'
    
    // Build stage
    stage('Build') {
        try {
            sh "nvm use ${nodeVersion}"
            sh 'npm ci'
            sh 'npm run build'
        } catch (e) {
            currentBuild.result = 'FAILURE'
            throw e
        }
    }
    
    // Test stage
    stage('Test') {
        if (currentBuild.result != 'FAILURE') {
            sh 'npm test'
            
            // Conditional logic
            def testsPassed = sh(script: 'cat test-results.xml | grep failures="0"', returnStatus: true) == 0
            if (testsPassed) {
                echo 'All tests passed!'
            } else {
                echo 'Some tests failed'
                currentBuild.result = 'UNSTABLE'
            }
        }
    }
    
    // Deploy stage 
    stage('Deploy') {
        if (env.BRANCH_NAME == 'main' && currentBuild.result != 'FAILURE') {
            sh 'npm run deploy'
        }
    }
    
    // Post-build actions
    stage('Notify') {
        if (currentBuild.result == 'FAILURE') {
            emailext subject: 'Build Failed',
                     body: 'Check the logs for details',
                     to: 'team@example.com'
        }
    }
}

Key Differences from Declarative Pipeline

Feature Scripted Pipeline Declarative Pipeline
Syntax Groovy script Structured DSL
Flow Control Groovy programming constructs (if/else, loops, functions) Limited, defined sections (when, parallel)
Error Handling try/catch blocks post sections
Structure Flexible Rigid, predefined
Learning Curve Steeper (requires Groovy knowledge) Simpler

When to Use Scripted Pipeline

Practical Jenkins Pipeline Examples

Example 1: Complete CI/CD Pipeline for a Node.js Application

pipeline {
    agent {
        docker {
            image 'node:18-alpine'
            args '-v /tmp:/tmp'
        }
    }
    
    environment {
        CI = 'true'
        STAGING_SERVER = 'staging.example.com'
        PRODUCTION_SERVER = 'production.example.com'
        DEPLOY_CREDS = credentials('deploy-ssh-key')
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }
        
        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }
        
        stage('Test') {
            steps {
                sh 'npm test'
                junit 'test-results/*.xml'
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm run build'
                archiveArtifacts artifacts: 'dist/**/*', fingerprint: true
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh 'npm run deploy -- --server=$STAGING_SERVER'
            }
        }
        
        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                input message: 'Deploy to production?', ok: 'Approve'
                sh 'npm run deploy -- --server=$PRODUCTION_SERVER'
            }
        }
    }
    
    post {
        always {
            cleanWs()
        }
        success {
            slackSend channel: '#builds', color: 'good', message: "Build succeeded: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
        }
        failure {
            slackSend channel: '#builds', color: 'danger', message: "Build failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
        }
    }
}

Example 2: Multi-Branch Pipeline with Parallel Testing

pipeline {
    agent none
    
    stages {
        stage('Build') {
            agent {
                docker {
                    image 'maven:3.8.5-openjdk-17'
                }
            }
            steps {
                sh 'mvn -B -DskipTests clean package'
                stash includes: 'target/*.jar', name: 'app'
            }
        }
        
        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    agent {
                        docker {
                            image 'maven:3.8.5-openjdk-17'
                        }
                    }
                    steps {
                        sh 'mvn test'
                        junit 'target/surefire-reports/*.xml'
                    }
                }
                
                stage('Integration Tests') {
                    agent {
                        docker {
                            image 'maven:3.8.5-openjdk-17'
                        }
                    }
                    steps {
                        sh 'mvn verify -DskipUnitTests'
                        junit 'target/failsafe-reports/*.xml'
                    }
                }
                
                stage('Code Analysis') {
                    agent {
                        docker {
                            image 'maven:3.8.5-openjdk-17'
                        }
                    }
                    steps {
                        sh 'mvn sonar:sonar'
                    }
                }
            }
        }
        
        stage('Deploy') {
            when {
                anyOf {
                    branch 'main'
                    branch 'develop'
                }
            }
            agent {
                label 'deploy'
            }
            steps {
                unstash 'app'
                script {
                    if (env.BRANCH_NAME == 'main') {
                        sh './deploy.sh production'
                    } else if (env.BRANCH_NAME == 'develop') {
                        sh './deploy.sh staging'
                    }
                }
            }
        }
    }
}

Example 3: Cross-Platform Pipeline with Matrix Builds

pipeline {
    agent none
    
    stages {
        stage('Build and Test') {
            matrix {
                axes {
                    axis {
                        name 'PLATFORM'
                        values 'linux', 'windows', 'mac'
                    }
                    axis {
                        name 'BROWSER'
                        values 'chrome', 'firefox', 'edge'
                    }
                }
                
                excludes {
                    exclude {
                        axis {
                            name 'PLATFORM'
                            values 'mac'
                        }
                        axis {
                            name 'BROWSER'
                            values 'edge'
                        }
                    }
                }
                
                agent {
                    label "${PLATFORM}"
                }
                
                stages {
                    stage('Checkout') {
                        steps {
                            checkout scm
                        }
                    }
                    
                    stage('Install') {
                        steps {
                            script {
                                if (PLATFORM == 'windows') {
                                    bat 'npm ci'
                                } else {
                                    sh 'npm ci'
                                }
                            }
                        }
                    }
                    
                    stage('Test') {
                        steps {
                            script {
                                if (PLATFORM == 'windows') {
                                    bat "npm run test:${BROWSER}"
                                } else {
                                    sh "npm run test:${BROWSER}"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Jenkins Pipeline Best Practices

Code Organization

Performance Optimization

Security Considerations

Maintenance and Debugging

Practical Exercise: Creating a Jenkins Pipeline

Exercise Brief

Create a Jenkins Pipeline for a Java web application that:

  1. Builds the application with Maven
  2. Runs unit and integration tests in parallel
  3. Performs static code analysis
  4. Builds a Docker image
  5. Deploys to a staging environment
  6. Requests approval before deployment to production

Step-by-Step Approach

  1. Create a new Jenkinsfile in your project repository
  2. Define the pipeline structure with appropriate stages
  3. Configure build and test steps
  4. Add parallel execution for tests
  5. Include Docker image building
  6. Configure deployment stages with approval

Solution Outline

pipeline {
    agent none
    
    environment {
        DOCKER_REGISTRY = 'registry.example.com'
        IMAGE_NAME = 'java-webapp'
        IMAGE_TAG = "${env.BUILD_NUMBER}"
        DOCKER_CREDS = credentials('docker-registry-credentials')
    }
    
    stages {
        stage('Build') {
            agent {
                docker {
                    image 'maven:3.8.5-openjdk-17'
                    args '-v $HOME/.m2:/root/.m2'
                }
            }
            steps {
                sh 'mvn -B -DskipTests clean package'
                stash includes: 'target/*.jar', name: 'app'
                stash includes: 'Dockerfile', name: 'dockerfile'
            }
        }
        
        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    agent {
                        docker {
                            image 'maven:3.8.5-openjdk-17'
                            args '-v $HOME/.m2:/root/.m2'
                        }
                    }
                    steps {
                        sh 'mvn test'
                        junit 'target/surefire-reports/*.xml'
                    }
                }
                
                stage('Integration Tests') {
                    agent {
                        docker {
                            image 'maven:3.8.5-openjdk-17'
                            args '-v $HOME/.m2:/root/.m2'
                        }
                    }
                    steps {
                        sh 'mvn verify -DskipUnitTests'
                        junit 'target/failsafe-reports/*.xml'
                    }
                }
                
                stage('Code Analysis') {
                    agent {
                        docker {
                            image 'maven:3.8.5-openjdk-17'
                            args '-v $HOME/.m2:/root/.m2'
                        }
                    }
                    steps {
                        sh 'mvn sonar:sonar'
                    }
                }
            }
        }
        
        stage('Build Docker Image') {
            agent {
                label 'docker'
            }
            steps {
                unstash 'app'
                unstash 'dockerfile'
                sh '''
                    docker build -t ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} .
                    echo ${DOCKER_CREDS_PSW} | docker login ${DOCKER_REGISTRY} -u ${DOCKER_CREDS_USR} --password-stdin
                    docker push ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
                '''
            }
        }
        
        stage('Deploy to Staging') {
            agent {
                label 'deploy'
            }
            steps {
                sh '''
                    echo "Deploying to staging environment..."
                    kubectl set image deployment/webapp webapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} --namespace=staging
                    kubectl rollout status deployment/webapp --namespace=staging
                '''
            }
        }
        
        stage('Approve Production Deployment') {
            steps {
                input message: 'Deploy to production?', ok: 'Approve'
            }
        }
        
        stage('Deploy to Production') {
            agent {
                label 'deploy'
            }
            steps {
                sh '''
                    echo "Deploying to production environment..."
                    kubectl set image deployment/webapp webapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} --namespace=production
                    kubectl rollout status deployment/webapp --namespace=production
                '''
            }
        }
    }
    
    post {
        success {
            echo 'Pipeline completed successfully'
            slackSend channel: '#deployments', color: 'good', message: "Deployment successful: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
        }
        failure {
            echo 'Pipeline failed'
            slackSend channel: '#deployments', color: 'danger', message: "Deployment failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
        }
    }
}

Challenge Extension

Once you have the basic pipeline working, try extending it with these features:

Real-World Jenkins Pipeline Use Cases

Example 1: Financial Institution

A major bank uses Jenkins Pipeline for:

Example 2: E-commerce Platform

A large e-commerce company implements Jenkins Pipeline for:

Example 3: Game Development Studio

A game development company uses Jenkins Pipeline for:

Conclusion

Jenkins Pipeline provides a powerful, flexible framework for implementing CI/CD workflows. Key takeaways from this lecture include:

Jenkins remains one of the most powerful and customizable CI/CD tools available, and mastering Jenkins Pipeline will give you the skills to implement sophisticated automation workflows for projects of any size or complexity.

In tomorrow's lectures, we'll explore more advanced topics in DevOps, including production Docker environments and multi-stage builds.

Additional Resources