How We Transformed CI/CD Chaos into a Unified Platform Serving 12 Engineering Teams

The Slack Message That Started It All:

Developer: "Hey, how do I deploy to production? I copied 
           the Jenkinsfile from team-backend but it's not working"

Me: "Which Jenkinsfile? We have like 30 different versions"

Developer: "The one that does the Docker build thing"

Me: "That describes 80% of our pipelines πŸ˜…"

Developer: "This is ridiculous. Can't we have ONE way to deploy?"

Me: "... hold that thought"

It was November 2021 at Fidelity Information Services (FIS), and our CI/CD infrastructure was a mess. 150+ unique Jenkins pipelines, each team copying and modifying Jenkinsfiles like they were sharing recipes at a potluck. Every pipeline was a snowflakeβ€”beautifully unique, impossibly fragile, and melting under the slightest pressure.

The Cost of Chaos:

  • Average time to create new pipeline: 2-3 days (copy, modify, debug, repeat)
  • Pipeline maintenance: 15+ hours/week (fixing broken builds across teams)
  • Deployment failures: ~30% of builds (inconsistent configurations)
  • Knowledge silos: Only 3 people understood β€œthe old Java pipeline”
  • Developer frustration: Through the roof

Six months later:

  • ONE centralized shared library powering all 150+ pipelines
  • New pipeline creation time: < 15 minutes
  • Pipeline maintenance: < 2 hours/week
  • Deployment failures: < 5%
  • Any developer can create/modify pipelines
  • Saved 200+ engineering hours/month

This is the complete story of how we built production-ready Jenkins infrastructure on Kubernetes and transformed 150 fragmented pipelines into a unified, maintainable CI/CD platform using Jenkins Shared Libraries.


Table of Contents

  1. The Pain: 150 Pipelines of Chaos
  2. Part 1: Building Production-Ready Jenkins on EKS
  3. Part 2: The Shared Library Architecture
  4. Part 3: Pipeline Standardization Strategy
  5. Part 4: Migration: From Chaos to Consistency
  6. Part 5: Results and Impact
  7. Lessons Learned

The Pain: 150 Pipelines of Chaos

graph TB
    Title["<b>Before Shared Library - 150 Unique Pipelines</b>"]
    
    subgraph JavaTeam["Java Teams - 48 Pipelines"]
        J1["team-a<br/>Jenkinsfile<br/>73 lines"]
        J2["team-b<br/>Jenkinsfile<br/>85 lines"]
        J3["team-c<br/>Jenkinsfile<br/>67 lines"]
        JN["... 45 more<br/>ALL different"]
    end
    
    subgraph NodeTeam["Node.js Teams - 35 Pipelines"]
        N1["frontend-a<br/>Jenkinsfile<br/>92 lines"]
        N2["frontend-b<br/>Jenkinsfile<br/>78 lines"]
        NN["... 33 more<br/>ALL different"]
    end
    
    subgraph PythonTeam["Python Teams - 22 Pipelines"]
        P1["data-team<br/>Jenkinsfile<br/>56 lines"]
        P2["ml-team<br/>Jenkinsfile<br/>102 lines"]
        PN["... 20 more<br/>ALL different"]
    end
    
    subgraph Problems["The Problems"]
        Prob1["❌ Copy/Paste Culture<br/>No consistency"]
        Prob2["❌ Maintenance Nightmare<br/>15 hrs/week fixing"]
        Prob3["❌ 30% Failure Rate<br/>Inconsistent configs"]
        Prob4["❌ 2-3 Days<br/>To create new pipeline"]
        Prob5["❌ Knowledge Silos<br/>Only 3 people understand"]
    end
    
    Title --> JavaTeam
    Title --> NodeTeam
    Title --> PythonTeam
    
    J1 -.-> Prob1
    N1 -.-> Prob2
    P1 -.-> Prob3
    J2 -.-> Prob4
    N2 -.-> Prob5
    
    style Title fill:#ffcccc,stroke:#cc0000,stroke-width:3px,color:#000
    style JavaTeam fill:#fff5e6
    style NodeTeam fill:#fff5e6
    style PythonTeam fill:#fff5e6
    style Problems fill:#ff9999

graph TB
    subgraph After["After Shared Library - ONE Library, 142 Pipelines"]
        subgraph SharedLib["Jenkins Shared Library"]
            SL["jenkins-shared-library<br/>GitHub Repository"]
            
            Maven["mavenPipeline()<br/>Java/Maven builds"]
            Node["nodePipeline()<br/>Node.js builds"]
            Python["pythonPipeline()<br/>Python builds"]
            Docker["dockerPipeline()<br/>Docker builds"]
            Terraform["terraformPipeline()<br/>Infrastructure"]
            
            SL --> Maven
            SL --> Node
            SL --> Python
            SL --> Docker
            SL --> Terraform
        end
        
        subgraph AllTeams["All Teams Use Same Library"]
            T1["Team A<br/>Jenkinsfile<br/>5 lines"]
            T2["Team B<br/>Jenkinsfile<br/>5 lines"]
            T3["Team C<br/>Jenkinsfile<br/>5 lines"]
            TN["142 pipelines<br/>ALL use shared lib"]
        end
        
        subgraph Benefits["The Benefits"]
            B1["βœ… Consistent<br/>Same patterns everywhere"]
            B2["βœ… Easy Maintenance<br/>2 hrs/week total"]
            B3["βœ… 95% Success Rate<br/>Standardized configs"]
            B4["βœ… 15 Minutes<br/>To create new pipeline"]
            B5["βœ… Self-Service<br/>Any dev can create"]
        end
    end
    
    Maven --> T1
    Node --> T2
    Python --> T3
    Docker --> TN
    
    T1 -.-> B1
    T2 -.-> B2
    T3 -.-> B3
    TN -.-> B4
    SharedLib -.-> B5
    
    style After fill:#ccffcc
    style SharedLib fill:#99ff99
    style Benefits fill:#ccffff

What We Inherited

When I joined FIS’s DevOps team, here’s what our Jenkins landscape looked like:

Pipeline Inventory (November 2021):

Total Pipelines: 152
β”œβ”€β”€ Java Maven builds: 48 (32 different Jenkinsfiles)
β”œβ”€β”€ Node.js builds: 35 (28 different Jenkinsfiles)
β”œβ”€β”€ Python builds: 22 (19 different Jenkinsfiles)
β”œβ”€β”€ Docker builds: 31 (ALL different)
β”œβ”€β”€ Terraform deployments: 16 (12 different)
└── "Special" pipelines: Random chaos

The Core Problem: Every team had copied a Jenkinsfile from somewhere, modified it for their needs, and then never updated it again. Pipeline maintenance was reactiveβ€”fix only when it breaks.

A Day in the Life (Before)

Monday, 9:15 AM:

// team-a's Jenkinsfile (works)
pipeline {
    agent { label 'docker' }
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package'
            }
        }
    }
}

Monday, 2:30 PM:

// team-b's Jenkinsfile (copied from team-a, modified)
pipeline {
    agent { label 'maven' }  // Different agent!
    stages {
        stage('Build & Test') {  // Combined stages
            steps {
                sh 'mvn clean verify'  // Different Maven goal
                sh 'docker build .'    // Added Docker
            }
        }
    }
}

Tuesday, 10:00 AM:

// team-c's Jenkinsfile (copied from team-b, more modifications)
pipeline {
    agent { label 'java11' }  // Yet another agent!
    stages {
        stage('Checkout') {  // Added explicit checkout
            steps {
                git 'https://github.com/...'
            }
        }
        stage('Build') {
            steps {
                sh 'mvn clean install'  // Different goal again!
                sh 'docker build -t myapp:${BUILD_NUMBER} .'
                sh 'docker push myapp:${BUILD_NUMBER}'
            }
        }
    }
}

Wednesday: All three teams’ builds break because we updated the Java agent image. Now we have to fix 150 pipelines.

The Tipping Point

The Incident: January 2022

We needed to add SonarQube scanning to all pipelines for security compliance. The estimate? 3 weeks to modify 150 pipelines manually.

The Executive Decision:

β€œThis is unsustainable. I don’t care how you do it, but we need ONE way to build and deploy. Make it happen.”

Challenge accepted.


Part 1: Building Production-Ready Jenkins on EKS

Before we could centralize pipelines, we needed rock-solid Jenkins infrastructure.

graph TB
    subgraph Internet["External Access"]
        Users["πŸ‘¨β€πŸ’» Developers<br/>Engineers"]
    end
    
    subgraph DNS["DNS Layer"]
        Route53["Route 53<br/>jenkins-prod.fis.com"]
        ACM["AWS Certificate Manager<br/>TLS Certificate"]
    end
    
    subgraph Ingress["Ingress Layer"]
        NGINX["NGINX Ingress<br/>TLS Termination<br/>Load Balancing"]
    end
    
    subgraph EKS["Amazon EKS Cluster"]
        subgraph Controllers["Jenkins Controllers - StatefulSet"]
            C1["Controller 1<br/>AZ-A<br/>4GB RAM"]
            C2["Controller 2<br/>AZ-B<br/>4GB RAM"]
            C3["Controller 3<br/>AZ-C<br/>4GB RAM"]
            C4["Controller 4<br/>AZ-A<br/>4GB RAM"]
        end
        
        subgraph Storage["Shared Storage"]
            EFS["Amazon EFS<br/>JENKINS_HOME<br/>Encrypted<br/>Multi-AZ"]
        end
        
        subgraph Agents["Dynamic Kubernetes Agents"]
            AgentMaven["Maven Agent<br/>On-demand Pod<br/>Java 17 + Maven 3.9"]
            AgentNode["Node.js Agent<br/>On-demand Pod<br/>Node 18"]
            AgentPython["Python Agent<br/>On-demand Pod<br/>Python 3.11"]
            AgentDocker["Docker Agent<br/>On-demand Pod<br/>Docker-in-Docker"]
        end
        
        subgraph Config["Configuration"]
            JCasC["Jenkins CasC<br/>Configuration as Code<br/>Shared Library config"]
        end
    end
    
    subgraph Monitoring["Monitoring & Backup"]
        Prometheus["Prometheus<br/>Metrics Collection"]
        Grafana["Grafana<br/>Dashboards"]
        Backup["AWS Backup<br/>Daily EFS Snapshots<br/>S3 Configuration Backup"]
    end
    
    Users --> Route53
    Route53 --> ACM
    ACM --> NGINX
    NGINX --> C1
    NGINX --> C2
    NGINX --> C3
    NGINX --> C4
    
    C1 --> EFS
    C2 --> EFS
    C3 --> EFS
    C4 --> EFS
    
    C1 -.->|Spawn| AgentMaven
    C2 -.->|Spawn| AgentNode
    C3 -.->|Spawn| AgentPython
    C4 -.->|Spawn| AgentDocker
    
    JCasC -.->|Configure| C1
    JCasC -.->|Configure| C2
    JCasC -.->|Configure| C3
    JCasC -.->|Configure| C4
    
    C1 --> Prometheus
    C2 --> Prometheus
    Prometheus --> Grafana
    
    EFS --> Backup
    
    style EKS fill:#e6f3ff
    style Controllers fill:#ffcc99
    style Agents fill:#ccffcc
    style Storage fill:#ffccff
    style Monitoring fill:#ffffcc

The Old Jenkins Setup (Don’t Do This)

Jenkins Master: EC2 t3.xlarge
- Single point of failure
- Manual backups (when we remembered)
- Agents: 10 EC2 instances running 24/7
- Cost: $3,200/month
- Scalability: LOL

Problems:

  • Jenkins went down? All builds stop
  • Need more capacity? Provision EC2 instance (30 minutes)
  • Upgrades? Hope and pray
  • Backups? β€œWe should really do that…”

The New Architecture: Jenkins on EKS

We designed a production-ready Jenkins platform on Kubernetes:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          Production Jenkins on EKS          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Jenkins Controllers (StatefulSet)  β”‚   β”‚
β”‚  β”‚   - 4 replicas across 3 AZs          β”‚   β”‚
β”‚  β”‚   - Anti-affinity rules              β”‚   β”‚
β”‚  β”‚   - Configuration as Code (JCasC)    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚               β”‚                             β”‚
β”‚               ↓                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Amazon EFS (Shared Storage)        β”‚   β”‚
β”‚  β”‚   - JENKINS_HOME persistence         β”‚   β”‚
β”‚  β”‚   - Multi-AZ, encrypted              β”‚   β”‚
β”‚  β”‚   - Automated backups                β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚               β”‚                             β”‚
β”‚               ↓                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Dynamic Kubernetes Agents          β”‚   β”‚
β”‚  β”‚   - On-demand pods                   β”‚   β”‚
β”‚  β”‚   - Language-specific labels         β”‚   β”‚
β”‚  β”‚   - Auto-scaling                     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚               β”‚                             β”‚
β”‚               ↓                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   NGINX Ingress + TLS                β”‚   β”‚
β”‚  β”‚   - jenkins-prod.example.com         β”‚   β”‚
β”‚  β”‚   - ACM certificate                  β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 1: EFS for Jenkins Persistence

Why EFS? JENKINS_HOME needs to be shared across multiple controller replicas.

# efs-volume-mount.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins-efs-pv
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: efs-sc
  csi:
    driver: efs.csi.aws.com
    volumeHandle: fs-0a1b2c3d4e5f6g7h8  # Your EFS ID
    volumeAttributes:
      encryptionInTransit: "true"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-efs-pvc
  namespace: jenkins
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: efs-sc
  resources:
    requests:
      storage: 100Gi

Apply:

kubectl create namespace jenkins
kubectl apply -f efs-volume-mount.yaml

Step 2: Jenkins Helm Values (Production-Ready)

# values.yaml - Production configuration
controller:
  # High availability - 4 replicas
  replicaCount: 4
  
  # Jenkins version
  image:
    tag: "2.452.2-lts"
  
  # Use EFS for JENKINS_HOME
  persistence:
    existingClaim: jenkins-efs-pvc
    storageClass: efs-sc
  
  # Resource requests/limits
  resources:
    requests:
      cpu: "2"
      memory: "4Gi"
    limits:
      cpu: "4"
      memory: "8Gi"
  
  # JVM tuning
  javaOpts: "-Xms2g -Xmx4g -XX:+UseG1GC"
  
  # Configuration as Code (JCasC)
  JCasC:
    enabled: true
    defaultConfig: true
    configScripts:
      welcome-message: |
        jenkins:
          systemMessage: "Production Jenkins - Managed by Platform Team"
      
      # Shared library configuration
      global-libraries: |
        unclassified:
          globalLibraries:
            libraries:
              - name: "jenkins-shared-library"
                retriever:
                  modernSCM:
                    scm:
                      git:
                        remote: "https://github.com/fis-devops/jenkins-shared-library.git"
                        credentialsId: "github-token"
                defaultVersion: "main"
                implicit: true
                allowVersionOverride: true
      
      # Kubernetes cloud configuration
      kubernetes-agent: |
        jenkins:
          clouds:
            - kubernetes:
                name: "kubernetes"
                serverUrl: "https://kubernetes.default"
                namespace: "jenkins"
                jenkinsUrl: "http://jenkins:8080"
                jenkinsTunnel: "jenkins-agent:50000"
                containerCapStr: "100"
                maxRequestsPerHostStr: "32"
                podRetention: "onFailure"
                templates:
                  # Maven agent template
                  - name: "maven"
                    label: "maven"
                    nodeUsageMode: NORMAL
                    containers:
                      - name: "maven"
                        image: "maven:3.9-eclipse-temurin-17"
                        command: "/bin/sh -c"
                        args: "cat"
                        ttyEnabled: true
                        resourceRequestCpu: "1"
                        resourceRequestMemory: "2Gi"
                        resourceLimitCpu: "2"
                        resourceLimitMemory: "4Gi"
                  
                  # Node.js agent template
                  - name: "nodejs"
                    label: "nodejs"
                    nodeUsageMode: NORMAL
                    containers:
                      - name: "nodejs"
                        image: "node:18-alpine"
                        command: "/bin/sh -c"
                        args: "cat"
                        ttyEnabled: true
                        resourceRequestCpu: "500m"
                        resourceRequestMemory: "1Gi"
                        resourceLimitCpu: "1"
                        resourceLimitMemory: "2Gi"
                  
                  # Python agent template
                  - name: "python"
                    label: "python"
                    nodeUsageMode: NORMAL
                    containers:
                      - name: "python"
                        image: "python:3.11-slim"
                        command: "/bin/sh -c"
                        args: "cat"
                        ttyEnabled: true
                        resourceRequestCpu: "500m"
                        resourceRequestMemory: "1Gi"
                        resourceLimitCpu: "1"
                        resourceLimitMemory: "2Gi"
                  
                  # Docker agent template
                  - name: "docker"
                    label: "docker"
                    nodeUsageMode: NORMAL
                    containers:
                      - name: "docker"
                        image: "docker:24-dind"
                        privileged: true
                        command: "/bin/sh -c"
                        args: "cat"
                        ttyEnabled: true
                        resourceRequestCpu: "1"
                        resourceRequestMemory: "2Gi"
                        resourceLimitCpu: "2"
                        resourceLimitMemory: "4Gi"
  
  # Essential plugins
  installPlugins:
    - kubernetes:4029.v5712230ccb_f8
    - workflow-aggregator:596.v8c21c963d92d
    - git:5.2.0
    - configuration-as-code:1810.v9b_c30a_249a_4c
    - blueocean:1.27.9
    - pipeline-stage-view:2.34
    - docker-workflow:572.v950f58993843
    - sonar:2.17.2
    - slack:664.vc9a_90f8b_c24a_
    - prometheus:2.3.2
    - job-dsl:1.87
    - credentials-binding:681.vf91669a_32e45
    - pipeline-utility-steps:2.16.2
    - http_request:1.18
    - timestamper:1.26
    - ws-cleanup:0.45
    - ansicolor:1.0.4
  
  # Security settings
  securityRealm: |-
    local:
      allowsSignup: false
  authorizationStrategy: |-
    loggedInUsersCanDoAnything:
      allowAnonymousRead: false
  
  # Prometheus metrics
  prometheus:
    enabled: true
    serviceMonitorNamespace: monitoring
  
  # Pod anti-affinity for HA
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: app.kubernetes.io/component
                operator: In
                values:
                  - jenkins-controller
          topologyKey: kubernetes.io/hostname

# Agent settings
agent:
  enabled: true
  defaultsProviderTemplate: "maven"
  
# Horizontal Pod Autoscaler
autoscaling:
  enabled: true
  minReplicas: 4
  maxReplicas: 8
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80

# Backup configuration
backup:
  enabled: true
  schedule: "H 2 * * *"  # Daily at 2 AM
  destination: s3://fis-jenkins-backups/

# Monitoring
serviceMonitor:
  enabled: true
  namespace: monitoring
  interval: 30s

Step 3: Deploy Jenkins

# Add Jenkins Helm repo
helm repo add jenkinsci https://charts.jenkins.io
helm repo update

# Deploy Jenkins
helm upgrade --install jenkins jenkinsci/jenkins \
  --namespace jenkins \
  --values values.yaml \
  --timeout 10m \
  --wait

# Watch pods come up
kubectl get pods -n jenkins -w

Step 4: NGINX Ingress with TLS

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: jenkins-ingress
  namespace: jenkins
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - jenkins-prod.example.com
      secretName: jenkins-tls-secret
  rules:
    - host: jenkins-prod.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: jenkins
                port:
                  number: 8080
kubectl apply -f ingress.yaml

Step 5: Access Jenkins

# Get admin password
kubectl exec -it jenkins-0 -n jenkins -- \
  cat /run/secrets/additional/chart-admin-password

# Access: https://jenkins-prod.example.com
# Login with admin user and password

The Infrastructure Results

Before (EC2-based):

  • Uptime: 99.5% (manual restarts needed)
  • Scale-up time: 30 minutes
  • Cost: $3,200/month
  • Maintenance: 8 hours/week

After (EKS-based):

  • Uptime: 99.95% (self-healing)
  • Scale-up time: < 2 minutes (auto-scaling)
  • Cost: $2,100/month
  • Maintenance: < 2 hours/week

Savings: $1,100/month + 75% less maintenance

Now we had a solid foundation. Time to tackle the pipeline chaos.


Part 2: The Shared Library Architecture

What is a Jenkins Shared Library?

graph LR
    subgraph Repo["jenkins-shared-library Repository"]
        subgraph Vars["vars/ - Pipeline Templates"]
            V1["mavenPipeline.groovy<br/>Java/Maven builds"]
            V2["nodePipeline.groovy<br/>Node.js builds"]
            V3["pythonPipeline.groovy<br/>Python builds"]
            V4["dockerPipeline.groovy<br/>Generic Docker"]
            V5["terraformPipeline.groovy<br/>Infrastructure"]
        end
        
        subgraph Functions["vars/ - Helper Functions"]
            F1["buildDocker.groovy<br/>Docker build"]
            F2["pushDocker.groovy<br/>Docker push"]
            F3["deployK8s.groovy<br/>K8s deployment"]
            F4["runSonarQube.groovy<br/>Code quality"]
            F5["sendSlackNotification.groovy<br/>Slack alerts"]
            F6["scanImage.groovy<br/>Security scan"]
        end
        
        subgraph Src["src/ - Classes"]
            S1["Docker.groovy<br/>Docker helper class"]
            S2["Kubernetes.groovy<br/>K8s helper class"]
            S3["Notifications.groovy<br/>Notification helper"]
            S4["Utils.groovy<br/>Common utilities"]
        end
        
        subgraph Resources["resources/ - Templates"]
            R1["kubernetes/<br/>deployment.yaml<br/>service.yaml"]
            R2["sonar/<br/>sonar-project.properties"]
        end
    end
    
    subgraph Teams["Development Teams"]
        Team1["Team A<br/>Jenkinsfile"]
        Team2["Team B<br/>Jenkinsfile"]
        Team3["Team C<br/>Jenkinsfile"]
    end
    
    V1 --> F1
    V1 --> F2
    V1 --> F3
    V1 --> F4
    V1 --> F5
    
    F1 --> S1
    F3 --> S2
    F5 --> S3
    
    Team1 --> V1
    Team2 --> V2
    Team3 --> V3
    
    style Repo fill:#e6f3ff
    style Vars fill:#ffcc99
    style Functions fill:#ccffcc
    style Src fill:#ffccff
    style Teams fill:#ffffcc

Think of it as a npm package for Jenkins pipelines. Instead of copying code, you import reusable functions.

Before (Copy/Paste):

// Every team's Jenkinsfile
pipeline {
    agent { label 'maven' }
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn test'
            }
        }
        stage('Docker Build') {
            steps {
                sh 'docker build -t myapp:${BUILD_NUMBER} .'
            }
        }
        stage('Docker Push') {
            steps {
                sh 'docker push myapp:${BUILD_NUMBER}'
            }
        }
        stage('Deploy') {
            steps {
                sh 'kubectl apply -f k8s/'
            }
        }
    }
}

After (Shared Library):

// Every team's Jenkinsfile
@Library('jenkins-shared-library') _

mavenPipeline {
    dockerImage = 'myapp'
    deployEnvironment = 'production'
}

Same functionality. 3 lines instead of 30.

Our Shared Library Structure

jenkins-shared-library/
β”œβ”€β”€ vars/
β”‚   β”œβ”€β”€ mavenPipeline.groovy       # Maven build pipeline
β”‚   β”œβ”€β”€ nodePipeline.groovy        # Node.js build pipeline
β”‚   β”œβ”€β”€ pythonPipeline.groovy      # Python build pipeline
β”‚   β”œβ”€β”€ dockerPipeline.groovy      # Generic Docker build
β”‚   β”œβ”€β”€ terraformPipeline.groovy   # Infrastructure deployment
β”‚   β”œβ”€β”€ helmPipeline.groovy        # Kubernetes Helm charts
β”‚   β”œβ”€β”€ buildDocker.groovy         # Docker build function
β”‚   β”œβ”€β”€ pushDocker.groovy          # Docker push function
β”‚   β”œβ”€β”€ deployK8s.groovy           # Kubernetes deployment
β”‚   β”œβ”€β”€ runSonarQube.groovy        # Code quality scan
β”‚   β”œβ”€β”€ runTests.groovy            # Test execution
β”‚   β”œβ”€β”€ sendSlackNotification.groovy  # Slack integration
β”‚   └── scanImage.groovy           # Container security scan
β”œβ”€β”€ src/
β”‚   └── com/
β”‚       └── fis/
β”‚           └── jenkins/
β”‚               β”œβ”€β”€ Docker.groovy         # Docker helper class
β”‚               β”œβ”€β”€ Kubernetes.groovy     # K8s helper class
β”‚               β”œβ”€β”€ Notifications.groovy  # Notification helper
β”‚               └── Utils.groovy          # Common utilities
β”œβ”€β”€ resources/
β”‚   β”œβ”€β”€ kubernetes/
β”‚   β”‚   β”œβ”€β”€ deployment.yaml
β”‚   β”‚   └── service.yaml
β”‚   └── sonar/
β”‚       └── sonar-project.properties
└── README.md

The Maven Pipeline Template

// vars/mavenPipeline.groovy
def call(Map config = [:]) {
    // Default configuration
    def defaults = [
        javaVersion: '17',
        mavenVersion: '3.9',
        dockerImage: '',
        dockerRegistry: 'docker.example.com',
        sonarQubeEnabled: true,
        deployEnvironment: 'staging',
        slackChannel: '#deployments',
        timeoutMinutes: 30
    ]
    
    // Merge user config with defaults
    config = defaults + config
    
    pipeline {
        agent {
            kubernetes {
                label "maven-${UUID.randomUUID().toString()}"
                yaml """
apiVersion: v1
kind: Pod
metadata:
  labels:
    jenkins: agent
spec:
  containers:
  - name: maven
    image: maven:${config.mavenVersion}-eclipse-temurin-${config.javaVersion}
    command:
    - cat
    tty: true
    resources:
      requests:
        memory: "2Gi"
        cpu: "1"
      limits:
        memory: "4Gi"
        cpu: "2"
  - name: docker
    image: docker:24-dind
    command:
    - cat
    tty: true
    privileged: true
    volumeMounts:
    - name: docker-sock
      mountPath: /var/run/docker.sock
  volumes:
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock
"""
            }
        }
        
        options {
            buildDiscarder(logRotator(numToKeepStr: '10'))
            timestamps()
            timeout(time: config.timeoutMinutes, unit: 'MINUTES')
            ansiColor('xterm')
        }
        
        environment {
            DOCKER_REGISTRY = config.dockerRegistry
            DOCKER_IMAGE = config.dockerImage
            DEPLOY_ENV = config.deployEnvironment
        }
        
        stages {
            stage('Checkout') {
                steps {
                    checkout scm
                    script {
                        env.GIT_COMMIT_SHORT = sh(
                            script: "git rev-parse --short HEAD",
                            returnStdout: true
                        ).trim()
                        env.IMAGE_TAG = "${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
                    }
                }
            }
            
            stage('Build') {
                steps {
                    container('maven') {
                        sh """
                            mvn clean package \
                                -DskipTests \
                                -Dmaven.test.skip=true \
                                -B -V
                        """
                    }
                }
            }
            
            stage('Test') {
                steps {
                    container('maven') {
                        sh 'mvn test -B'
                    }
                }
                post {
                    always {
                        junit '**/target/surefire-reports/*.xml'
                    }
                }
            }
            
            stage('SonarQube Analysis') {
                when {
                    expression { config.sonarQubeEnabled }
                }
                steps {
                    container('maven') {
                        script {
                            runSonarQube()
                        }
                    }
                }
            }
            
            stage('Docker Build') {
                when {
                    expression { config.dockerImage != '' }
                }
                steps {
                    container('docker') {
                        script {
                            buildDocker(
                                image: "${config.dockerRegistry}/${config.dockerImage}",
                                tag: env.IMAGE_TAG
                            )
                        }
                    }
                }
            }
            
            stage('Security Scan') {
                when {
                    expression { config.dockerImage != '' }
                }
                steps {
                    container('docker') {
                        script {
                            scanImage(
                                image: "${config.dockerRegistry}/${config.dockerImage}:${env.IMAGE_TAG}"
                            )
                        }
                    }
                }
            }
            
            stage('Docker Push') {
                when {
                    expression { config.dockerImage != '' }
                }
                steps {
                    container('docker') {
                        script {
                            pushDocker(
                                image: "${config.dockerRegistry}/${config.dockerImage}",
                                tag: env.IMAGE_TAG
                            )
                        }
                    }
                }
            }
            
            stage('Deploy') {
                when {
                    branch 'main'
                }
                steps {
                    script {
                        deployK8s(
                            environment: config.deployEnvironment,
                            image: "${config.dockerRegistry}/${config.dockerImage}:${env.IMAGE_TAG}"
                        )
                    }
                }
            }
        }
        
        post {
            success {
                script {
                    sendSlackNotification(
                        channel: config.slackChannel,
                        message: "βœ… Build SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                        color: 'good'
                    )
                }
            }
            failure {
                script {
                    sendSlackNotification(
                        channel: config.slackChannel,
                        message: "❌ Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                        color: 'danger'
                    )
                }
            }
            always {
                cleanWs()
            }
        }
    }
}

Reusable Helper Functions

// vars/buildDocker.groovy
def call(Map config = [:]) {
    echo "Building Docker image: ${config.image}:${config.tag}"
    
    sh """
        docker build \
            --tag ${config.image}:${config.tag} \
            --tag ${config.image}:latest \
            --build-arg BUILD_DATE=\$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
            --build-arg VCS_REF=${env.GIT_COMMIT_SHORT} \
            --build-arg VERSION=${config.tag} \
            .
    """
}

// vars/pushDocker.groovy
def call(Map config = [:]) {
    echo "Pushing Docker image: ${config.image}:${config.tag}"
    
    withCredentials([usernamePassword(
        credentialsId: 'docker-registry-credentials',
        usernameVariable: 'DOCKER_USER',
        passwordVariable: 'DOCKER_PASS'
    )]) {
        sh """
            echo \$DOCKER_PASS | docker login ${config.registry} -u \$DOCKER_USER --password-stdin
            docker push ${config.image}:${config.tag}
            docker push ${config.image}:latest
        """
    }
}

// vars/deployK8s.groovy
def call(Map config = [:]) {
    echo "Deploying to ${config.environment}"
    
    withCredentials([file(credentialsId: "kubeconfig-${config.environment}", variable: 'KUBECONFIG')]) {
        sh """
            kubectl set image deployment/${config.deploymentName} \
                ${config.containerName}=${config.image} \
                --namespace=${config.namespace}
            
            kubectl rollout status deployment/${config.deploymentName} \
                --namespace=${config.namespace} \
                --timeout=5m
        """
    }
}

// vars/runSonarQube.groovy
def call(Map config = [:]) {
    withSonarQubeEnv('SonarQube') {
        sh """
            mvn sonar:sonar \
                -Dsonar.projectKey=${env.JOB_NAME} \
                -Dsonar.projectName='${env.JOB_NAME}' \
                -Dsonar.projectVersion=${env.BUILD_NUMBER}
        """
    }
    
    // Wait for quality gate
    timeout(time: 10, unit: 'MINUTES') {
        def qg = waitForQualityGate()
        if (qg.status != 'OK') {
            error "Pipeline aborted due to quality gate failure: ${qg.status}"
        }
    }
}

// vars/sendSlackNotification.groovy
def call(Map config = [:]) {
    slackSend(
        channel: config.channel,
        message: config.message,
        color: config.color,
        botUser: true,
        teamDomain: 'fis-engineering',
        tokenCredentialId: 'slack-token'
    )
}

// vars/scanImage.groovy
def call(Map config = [:]) {
    echo "Scanning image for vulnerabilities: ${config.image}"
    
    sh """
        # Using Trivy for container scanning
        docker run --rm \
            -v /var/run/docker.sock:/var/run/docker.sock \
            aquasec/trivy:latest image \
            --severity HIGH,CRITICAL \
            --exit-code 1 \
            ${config.image}
    """
}

Node.js Pipeline Template

// vars/nodePipeline.groovy
def call(Map config = [:]) {
    def defaults = [
        nodeVersion: '18',
        packageManager: 'npm',  // or 'yarn'
        dockerImage: '',
        dockerRegistry: 'docker.example.com',
        deployEnvironment: 'staging',
        slackChannel: '#deployments',
        runLint: true,
        runTests: true
    ]
    
    config = defaults + config
    
    pipeline {
        agent {
            kubernetes {
                label "nodejs-${UUID.randomUUID().toString()}"
                yaml """
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: nodejs
    image: node:${config.nodeVersion}-alpine
    command:
    - cat
    tty: true
  - name: docker
    image: docker:24-dind
    command:
    - cat
    tty: true
    privileged: true
"""
            }
        }
        
        stages {
            stage('Checkout') {
                steps {
                    checkout scm
                }
            }
            
            stage('Install Dependencies') {
                steps {
                    container('nodejs') {
                        script {
                            if (config.packageManager == 'yarn') {
                                sh 'yarn install --frozen-lockfile'
                            } else {
                                sh 'npm ci'
                            }
                        }
                    }
                }
            }
            
            stage('Lint') {
                when {
                    expression { config.runLint }
                }
                steps {
                    container('nodejs') {
                        sh "${config.packageManager} run lint"
                    }
                }
            }
            
            stage('Test') {
                when {
                    expression { config.runTests }
                }
                steps {
                    container('nodejs') {
                        sh "${config.packageManager} run test"
                    }
                }
            }
            
            stage('Build') {
                steps {
                    container('nodejs') {
                        sh "${config.packageManager} run build"
                    }
                }
            }
            
            stage('Docker Build & Push') {
                when {
                    expression { config.dockerImage != '' }
                }
                steps {
                    container('docker') {
                        script {
                            def imageTag = "${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
                            buildDocker(
                                image: "${config.dockerRegistry}/${config.dockerImage}",
                                tag: imageTag
                            )
                            pushDocker(
                                image: "${config.dockerRegistry}/${config.dockerImage}",
                                tag: imageTag,
                                registry: config.dockerRegistry
                            )
                        }
                    }
                }
            }
            
            stage('Deploy') {
                when {
                    branch 'main'
                }
                steps {
                    script {
                        deployK8s(
                            environment: config.deployEnvironment,
                            image: "${config.dockerRegistry}/${config.dockerImage}:${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
                        )
                    }
                }
            }
        }
        
        post {
            success {
                script {
                    sendSlackNotification(
                        channel: config.slackChannel,
                        message: "βœ… Build SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                        color: 'good'
                    )
                }
            }
            failure {
                script {
                    sendSlackNotification(
                        channel: config.slackChannel,
                        message: "❌ Build FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                        color: 'danger'
                    )
                }
            }
        }
    }
}

Part 3: Pipeline Standardization Strategy

The Migration Philosophy

graph TB
    Title["<b>Pipeline Migration: Before vs After</b>"]
    
    subgraph Before["❌ BEFORE: Custom Pipeline Chaos"]
        B_Header["<b>Traditional Jenkinsfile</b><br/>73 Lines of Code"]
        
        B_Stage1["<b>Stage 1:</b> Checkout<br/>checkout scm"]
        B_Stage2["<b>Stage 2:</b> Build<br/>sh 'mvn clean package'"]
        B_Stage3["<b>Stage 3:</b> Test<br/>sh 'mvn test'<br/>junit '**/*.xml'"]
        B_Stage4["<b>Stage 4:</b> SonarQube<br/>withSonarQubeEnv(...)<br/>sh 'mvn sonar:sonar'"]
        B_Stage5["<b>Stage 5:</b> Docker Build<br/>sh 'docker build -t app .'"]
        B_Stage6["<b>Stage 6:</b> Docker Push<br/>withCredentials(...)<br/>sh 'docker push'"]
        B_Stage7["<b>Stage 7:</b> Deploy<br/>sh 'kubectl apply -f k8s/'"]
        
        B_Header --> B_Stage1
        B_Stage1 --> B_Stage2
        B_Stage2 --> B_Stage3
        B_Stage3 --> B_Stage4
        B_Stage4 --> B_Stage5
        B_Stage5 --> B_Stage6
        B_Stage6 --> B_Stage7
    end
    
    subgraph After["βœ… AFTER: Shared Library Simplicity"]
        A_Header["<b>Shared Library Jenkinsfile</b><br/>5 Lines of Code"]
        
        A_Code["@Library('jenkins-shared-library') _<br/><br/>mavenPipeline {<br/>  dockerImage = 'payment-api'<br/>  deployEnvironment = 'production'<br/>}"]
        
        A_Benefits["<b>Automatic Features:</b><br/>βœ“ All 7 stages automated<br/>βœ“ Security scanning included<br/>βœ“ Slack notifications<br/>βœ“ Error handling<br/>βœ“ Best practices enforced"]
        
        A_Header --> A_Code
        A_Code --> A_Benefits
    end
    
    subgraph Metrics["πŸ“Š Impact Metrics"]
        M1["<b>Code Reduction</b><br/>73 β†’ 5 lines<br/><b>93% less code</b>"]
        M2["<b>Maintenance</b><br/>Per-pipeline β†’ Centralized<br/><b>87% time saved</b>"]
        M3["<b>Consistency</b><br/>Copy/paste β†’ Standardized<br/><b>Zero drift</b>"]
        M4["<b>Quality</b><br/>Manual β†’ Automated<br/><b>25% fewer failures</b>"]
    end
    
    Title --> Before
    Title --> After
    
    Before -.->|Replaced by| After
    
    Before --> Metrics
    After --> Metrics
    
    Metrics --> M1
    Metrics --> M2
    Metrics --> M3
    Metrics --> M4
    
    style Title fill:#2c3e50,stroke:#34495e,stroke-width:3px,color:#ffffff
    style Before fill:#ffe6e6,stroke:#e74c3c,stroke-width:2px
    style After fill:#e6ffe6,stroke:#27ae60,stroke-width:2px
    style Metrics fill:#e6f3ff,stroke:#3498db,stroke-width:2px
    
    style B_Header fill:#ffcccc,stroke:#c0392b,stroke-width:2px
    style A_Header fill:#ccffcc,stroke:#229954,stroke-width:2px
    
    style B_Stage1 fill:#fff5f5
    style B_Stage2 fill:#fff5f5
    style B_Stage3 fill:#fff5f5
    style B_Stage4 fill:#fff5f5
    style B_Stage5 fill:#fff5f5
    style B_Stage6 fill:#fff5f5
    style B_Stage7 fill:#fff5f5
    
    style A_Code fill:#f0fff0
    style A_Benefits fill:#f0fff0
    
    style M1 fill:#d6eaf8,stroke:#2980b9,stroke-width:1px
    style M2 fill:#d6eaf8,stroke:#2980b9,stroke-width:1px
    style M3 fill:#d6eaf8,stroke:#2980b9,stroke-width:1px
    style M4 fill:#d6eaf8,stroke:#2980b9,stroke-width:1px

We couldn’t migrate 150 pipelines overnight. We needed a strategy:

Principle 1: Make it Easy

  • Shared library pipelines should be simpler than custom ones
  • Default configurations for 80% use cases
  • Override anything when needed

Principle 2: Incremental Migration

  • Start with new projects
  • Migrate high-value pipelines first
  • Let low-priority pipelines migrate organically

Principle 3: Don’t Break Existing Pipelines

  • Shared library is opt-in, not forced
  • Old pipelines continue working
  • Migration is developer-driven

Pipeline Categories

We identified 5 main pipeline patterns:

1. Java/Maven Applications (48 pipelines)
   β†’ mavenPipeline

2. Node.js Applications (35 pipelines)
   β†’ nodePipeline

3. Python Applications (22 pipelines)
   β†’ pythonPipeline

4. Docker-only Builds (31 pipelines)
   β†’ dockerPipeline

5. Terraform Infrastructure (16 pipelines)
   β†’ terraformPipeline

Migration Examples

Before - Custom Maven Pipeline (45 lines):

pipeline {
    agent { label 'maven' }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }
        
        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit '**/target/surefire-reports/*.xml'
                }
            }
        }
        
        stage('SonarQube') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }
        
        stage('Docker Build') {
            steps {
                sh 'docker build -t payment-api:${BUILD_NUMBER} .'
                sh 'docker tag payment-api:${BUILD_NUMBER} docker.example.com/payment-api:${BUILD_NUMBER}'
            }
        }
        
        stage('Docker Push') {
            steps {
                withCredentials([usernamePassword(credentialsId: 'docker-creds', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
                    sh 'echo $PASS | docker login docker.example.com -u $USER --password-stdin'
                    sh 'docker push docker.example.com/payment-api:${BUILD_NUMBER}'
                }
            }
        }
    }
}

After - Shared Library (5 lines):

@Library('jenkins-shared-library') _

mavenPipeline {
    dockerImage = 'payment-api'
    deployEnvironment = 'production'
}

90% less code. Same functionality. Plus:

  • SonarQube scanning
  • Security image scanning
  • Slack notifications
  • Kubernetes deployment
  • Error handling
  • All maintained centrally

Advanced Configurations

Override default behavior:

@Library('jenkins-shared-library') _

mavenPipeline {
    // Custom configuration
    javaVersion = '11'          // Use Java 11 instead of 17
    mavenVersion = '3.8'        // Specific Maven version
    dockerImage = 'legacy-app'
    deployEnvironment = 'staging'
    sonarQubeEnabled = false    // Skip SonarQube
    slackChannel = '#team-backend'
    timeoutMinutes = 45         // Longer timeout
    
    // Custom Maven goals
    buildCommand = 'mvn clean install -P production'
    
    // Custom Docker build args
    dockerBuildArgs = '--build-arg ENV=prod'
}

Multi-stage deployments:

@Library('jenkins-shared-library') _

mavenPipeline {
    dockerImage = 'api-server'
    
    // Deploy to staging first
    deployEnvironment = 'staging'
    
    // After successful staging deploy, prompt for production
    postDeploy = {
        input(
            message: 'Deploy to production?',
            ok: 'Deploy',
            submitter: 'platform-team'
        )
        
        deployK8s(
            environment: 'production',
            image: "${env.DOCKER_REGISTRY}/${env.DOCKER_IMAGE}:${env.IMAGE_TAG}"
        )
    }
}

Part 4: Migration: From Chaos to Consistency

gantt
    title 6-Month Jenkins Shared Library Migration
    dateFormat  YYYY-MM-DD
    
    section Infrastructure
    Jenkins on EKS Setup          :done, infra1, 2021-11-01, 3w
    EFS Configuration             :done, infra2, 2021-11-08, 1w
    JCasC Implementation          :done, infra3, 2021-11-15, 1w
    Dynamic Agents Setup          :done, infra4, 2021-11-22, 1w
    
    section Shared Library Dev
    Initial Library Structure     :done, lib1, 2021-12-01, 1w
    Maven Pipeline Template       :done, lib2, 2021-12-08, 1w
    Node.js Pipeline Template     :done, lib3, 2021-12-15, 1w
    Python Pipeline Template      :done, lib4, 2021-12-22, 1w
    Helper Functions              :done, lib5, 2021-12-29, 2w
    
    section Phase 1: Pilot
    5 Team Pilot Program          :done, pilot1, 2022-01-10, 3w
    Bug Fixes & Iteration         :done, pilot2, 2022-01-31, 1w
    
    section Phase 2: Early Adopters
    New Projects Mandate          :done, early1, 2022-02-07, 4w
    40 New Pipelines Created      :done, early2, 2022-02-07, 6w
    15 Voluntary Migrations       :done, early3, 2022-02-21, 4w
    
    section Phase 3: Mass Migration
    Migration Campaign Launch     :done, mass1, 2022-03-07, 1w
    Team Workshops & Training     :done, mass2, 2022-03-14, 4w
    Migration Tool Development    :done, mass3, 2022-03-21, 2w
    100+ Pipeline Migrations      :done, mass4, 2022-04-04, 8w
    
    section Results
    142/152 Pipelines Migrated    :milestone, result, 2022-05-30, 0d
    93% Adoption Rate             :milestone, result2, 2022-05-30, 0d

Phase 1: Pilot Program (Month 1)

Selected 5 teams for pilot:

  1. Backend API team (Java)
  2. Frontend team (Node.js)
  3. Data team (Python)
  4. DevOps team (Terraform)
  5. ML team (Docker-only)

Results:

  • Average migration time: 30 minutes per pipeline
  • Bugs found in shared library: 12 (fixed quickly)
  • Teams’ feedback: 9/10 satisfaction
  • Key insight: Slack notifications were the most appreciated feature

Phase 2: Early Adopters (Month 2-3)

Strategy: β€œNew projects must use shared library.”

Results:

  • 40 new pipelines created using shared library
  • 15 existing pipelines migrated voluntarily
  • Common pattern emerged: Teams loved the simplicity

Example migration Pull Request:

# Jenkinsfile (before: 73 lines)
- pipeline {
-     agent { label 'nodejs' }
-     stages {
-         stage('Build') {
-             steps {
-                 sh 'npm install'
-                 sh 'npm run build'
-             }
-         }
-         stage('Test') {
-             steps {
-                 sh 'npm test'
-             }
-         }
-         ...
-     }
- }

# Jenkinsfile (after: 5 lines)
+ @Library('jenkins-shared-library') _
+ 
+ nodePipeline {
+     dockerImage = 'web-app'
+ }

Developer comment:

β€œThis is amazing. I just deleted 70 lines of code and it does MORE than before.”

Phase 3: Mass Migration (Month 4-6)

Strategy: β€œCarrot, not stick.”

Incentives for migration:

  • Free pipeline optimization review
  • Guaranteed < 5 minute support response
  • Featured in team newsletter
  • β€œEarly Adopter” badge in Jenkins
  • Team lunch sponsored by platform team

Migration support:

# We created a migration helper script
./scripts/migrate-pipeline.sh \
  --repo "github.com/fis/payment-api" \
  --type "maven" \
  --dry-run

# Output:
βœ“ Detected: Java Maven project
βœ“ Found Dockerfile
βœ“ Found Jenkinsfile (73 lines)

Recommended Jenkinsfile:
────────────────────────────────────
@Library('jenkins-shared-library') _

mavenPipeline {
    dockerImage = 'payment-api'
    javaVersion = '17'
    deployEnvironment = 'production'
}
────────────────────────────────────

Migrate now? (y/n)

Results:

  • Migrated pipelines: 142/152 (93%)
  • Holdouts: 10 β€œspecial snowflake” pipelines
  • Migration time: 6 months total
  • Average per-pipeline migration: < 1 hour

The Holdouts

10 pipelines didn’t migrate. Why?

  1. Legacy mainframe integration (3 pipelines)
    • Too complex, touching mainframe systems
    • Decision: Leave alone, too risky
  2. Acquisition pipelines (4 pipelines)
    • From acquired company, different standards
    • Decision: Migrate when team has bandwidth
  3. ML training pipelines (2 pipelines)
    • Need GPU support, shared library didn’t support yet
    • Decision: Added GPU support, then migrated
  4. β€œIt works, don’t touch it” (1 pipeline)
    • Business-critical, zero tolerance for change
    • Decision: Respected, left alone

The pragmatic approach: Don’t force 100% adoption. 93% is excellent.


Part 5: Results and Impact

Quantitative Results

graph TB
    subgraph Metrics["Key Impact Metrics"]
        subgraph Time["Time Metrics"]
            T1["Pipeline Creation<br/>Before: 2-3 days<br/>After: 15 minutes<br/>🟒 95% faster"]
            T2["Maintenance Time<br/>Before: 15 hrs/week<br/>After: 2 hrs/week<br/>🟒 87% reduction"]
        end
        
        subgraph Quality["Quality Metrics"]
            Q1["Success Rate<br/>Before: 70%<br/>After: 95%<br/>🟒 +25 points"]
            Q2["Failure Resolution<br/>Before: 2 weeks<br/>After: 1 day<br/>🟒 90% faster"]
        end
        
        subgraph Cost["Cost Metrics"]
            C1["Infrastructure<br/>Before: $3,200/mo<br/>After: $2,100/mo<br/>🟒 $1,100 saved"]
            C2["Engineer Time<br/>Before: ~320 hrs/mo<br/>After: ~120 hrs/mo<br/>🟒 $20K value/mo"]
        end
        
        subgraph Adoption["Adoption Metrics"]
            A1["Pipelines Migrated<br/>142 out of 152<br/>🟒 93% adoption"]
            A2["Developer Satisfaction<br/>9/10 rating<br/>🟒 High satisfaction"]
        end
    end
    
    subgraph ROI["Return on Investment"]
        Investment["Investment<br/>480 engineering hours<br/>$60K loaded cost"]
        Returns["Annual Returns<br/>2400 hrs saved/yr<br/>$240K value/yr"]
        Result["ROI: 300%<br/>Payback: 3 months"]
    end
    
    Time --> Investment
    Quality --> Investment
    Cost --> Returns
    Adoption --> Returns
    
    Investment --> Result
    Returns --> Result
    
    style Time fill:#e6f3ff
    style Quality fill:#ccffcc
    style Cost fill:#ffcc99
    style Adoption fill:#ffccff
    style ROI fill:#ffffcc
    style Result fill:#99ff99

Pipeline Creation Time:

Before: 2-3 days (copy, modify, debug, test)
After:  < 15 minutes (configure, test, done)

Reduction: 95%

Pipeline Maintenance:

Before: 15+ hours/week (fixing broken pipelines)
After:  < 2 hours/week (updating shared library)

Reduction: 87%

Deployment Success Rate:

Before: 70% success (30% failure rate)
After:  95% success (5% failure rate)

Improvement: 25 percentage points

Time to Fix Pipeline Issues:

Before: Fix in 150 places (avg 2 weeks)
After:  Fix in 1 place (< 1 day)

Improvement: 90% faster

Qualitative Results

Developer Experience:

Before:

Developer: "How do I add Docker build to my pipeline?"
DevOps:    "Copy this Jenkinsfile from team-backend"
Developer: "It's not working..."
DevOps:    "Did you update line 47?"
Developer: "There is no line 47?"
DevOps:    "You copied the wrong one. Use team-frontend's"
Developer: "😭"

After:

Developer: "How do I add Docker build?"
DevOps:    "Add `dockerImage = 'your-app'` to your pipeline"
Developer: "That's it?"
DevOps:    "That's it."
Developer: "πŸŽ‰"

Business Impact

Engineering Efficiency:

  • Saved 200+ engineering hours/month
  • Faster feature delivery (pipelines not a bottleneck)
  • Reduced context switching (fewer pipeline issues)

Quality Improvements:

  • Consistent security scanning (100% coverage)
  • Standardized testing (no skipped tests)
  • Automated compliance checks

Cost Savings:

  • Reduced Jenkins agent usage: $1,100/month
  • Less engineer time on pipeline maintenance: ~$20K/month value
  • Faster deployments = faster time to market: Priceless

The Network Effect

What happened after adoption:

  1. Teams started contributing to shared library
    Pull Request #47: Add Python pytest support
    Pull Request #52: Add Terraform workspace management
    Pull Request #61: Add Helm chart deployment
    
  2. Documentation improved organically
    • Teams added examples
    • FAQ emerged from Slack discussions
    • Best practices documented by users
  3. Innovation accelerated
    • New deployment strategies (blue-green, canary)
    • Advanced testing (contract tests, load tests)
    • Security scanning (SAST, DAST, container scanning)

The shared library became the platform for innovation.


sequenceDiagram
    participant Dev as Developer
    participant Git as Git Repository
    participant Jenkins as Jenkins Controller
    participant Lib as Shared Library
    participant Agent as K8s Agent Pod
    participant SonarQube as SonarQube
    participant Docker as Docker Registry
    participant K8s as Kubernetes
    participant Slack as Slack
    
    Note over Dev,Slack: Complete Pipeline Execution Flow
    
    Dev->>Git: 1. Push code + Jenkinsfile
    Note over Git: Jenkinsfile:<br/>mavenPipeline {<br/>  dockerImage = 'app'<br/>}
    
    Git->>Jenkins: 2. Webhook triggers build
    Jenkins->>Jenkins: 3. Parse Jenkinsfile
    Jenkins->>Lib: 4. Load shared library<br/>from GitHub
    
    Lib->>Jenkins: 5. Return mavenPipeline code
    Jenkins->>Agent: 6. Spawn Maven K8s pod
    Note over Agent: Pod with:<br/>- Maven 3.9<br/>- Java 17<br/>- Docker
    
    Jenkins->>Agent: 7. Checkout source code
    Agent->>Agent: 8. Build: mvn clean package
    Agent->>Agent: 9. Test: mvn test
    Agent->>Agent: 10. Generate test reports
    
    Agent->>SonarQube: 11. Run SonarQube scan
    SonarQube->>Agent: 12. Quality gate result
    
    alt Quality Gate Failed
        Agent->>Slack: ❌ Build failed - quality gate
        Agent->>Jenkins: Mark build FAILED
    else Quality Gate Passed
        Agent->>Agent: 13. Docker build
        Agent->>Agent: 14. Security scan (Trivy)
        
        alt Security Issues Found
            Agent->>Slack: ⚠️ Security vulnerabilities found
        else Security OK
            Agent->>Docker: 15. Push Docker image
            Docker->>Agent: Image pushed successfully
            
            Agent->>K8s: 16. Deploy to K8s
            K8s->>K8s: Rolling update
            K8s->>Agent: Deployment successful
            
            Agent->>Slack: βœ… Build SUCCESS<br/>Deployed to production
            Agent->>Jenkins: Mark build SUCCESS
        end
    end
    
    Jenkins->>Agent: 17. Cleanup workspace
    Agent->>Agent: 18. Pod terminates
    
    Note over Dev,Slack: Total time: ~5-8 minutes<br/>All automated via shared library!

Lessons Learned

What Worked Well

1. Start with Infrastructure

Building rock-solid Jenkins on Kubernetes first gave us confidence to tackle pipelines.

Lesson: Don’t build shared libraries on shaky infrastructure.

2. Make it Easier, Not Just Better

Our shared library wasn’t just β€œbetter practice”—it was genuinely easier to use.

Lesson: Adoption requires ease, not just righteousness.

3. Defaults Matter

80% of teams used default configurations. We spent 80% of effort on defaults.

Lesson: Optimize for the common case.

4. Incremental Migration

Forcing all 150 teams to migrate at once would have failed.

Lesson: Change management > Technical excellence.

5. Treat Shared Library Like Product

We had:

  • Versioning (semantic versioning)
  • Documentation
  • Changelog
  • Support channel (#jenkins-help)
  • Regular releases

Lesson: Shared libraries are products, not side projects.

What We’d Do Differently

1. Testing Infrastructure Earlier

We didn’t have great testing for the shared library initially. Led to some production breaks.

Should have done:

// Unit tests for shared library functions
@Test
void testBuildDockerWithDefaultConfig() {
    def result = buildDocker(
        image: 'test-app',
        tag: 'v1.0.0'
    )
    assert result.exitCode == 0
}

// Integration tests
@Test
void testFullMavenPipeline() {
    def job = createTestJob('maven-test')
    def build = job.scheduleBuild2(0).get()
    assert build.result == Result.SUCCESS
}

2. Versioning Strategy

Initially, everyone used main branch. One breaking change = 150 broken pipelines.

Should have done:

// Pin to specific version
@Library('jenkins-shared-library@v2.1.0') _

// Or use version ranges
@Library('jenkins-shared-library@v2.x') _

3. Migration Automation

We manually migrated many pipelines. Should have automated more.

4. Documentation First

We built first, documented later. Should have been reversed.

Lesson: Documentation is part of development, not after.

Common Pitfalls to Avoid

Pitfall 1: Too Much Abstraction

// DON'T: Over-abstract
universalPipeline {
    language = 'java'
    buildTool = 'maven'
    containerization = true
    orchestration = 'kubernetes'
    // ... 50 more config options
}

// DO: Purpose-built pipelines
mavenPipeline {
    dockerImage = 'my-app'
}

Pitfall 2: No Escape Hatch

// DON'T: Force everyone into your abstraction
mavenPipeline {
    // No way to customize
}

// DO: Allow custom stages
mavenPipeline {
    dockerImage = 'my-app'
    
    // Custom pre-build stage
    preBuild = {
        sh 'echo "Custom logic here"'
    }
    
    // Custom post-deploy
    postDeploy = {
        sh 'run-smoke-tests.sh'
    }
}

Pitfall 3: Breaking Changes Without Communication

Always:

  • Announce breaking changes 2 weeks ahead
  • Provide migration guide
  • Support old version for transition period
  • Have rollback plan

Pitfall 4: Ignoring Feedback

Early on, developers asked for pythonPipeline. We said β€œuse dockerPipeline.”

Bad idea. They went back to custom pipelines.

Lesson: Listen to users. If many people ask for something, build it.

Best Practices

1. Keep Shared Library Simple

// GOOD: Simple, clear
mavenPipeline {
    dockerImage = 'my-app'
}

// BAD: Too much magic
pipeline {
    agent { 
        magic() 
    }
    stages {
        autoDetect()
    }
}

2. Fail Fast with Clear Errors

def call(Map config = [:]) {
    // Validate required config
    if (!config.dockerImage) {
        error """
        ❌ Missing required parameter: dockerImage
        
        Example:
        mavenPipeline {
            dockerImage = 'your-app-name'
        }
        
        See documentation: https://docs.example.com/jenkins-shared-library
        """
    }
}

3. Version Everything

jenkins-shared-library/
β”œβ”€β”€ CHANGELOG.md
β”œβ”€β”€ VERSION (currently: v3.2.1)
└── docs/
    β”œβ”€β”€ v3.2.1/
    β”œβ”€β”€ v3.2.0/
    └── v3.1.0/

4. Monitor Usage

We tracked:

  • Which pipeline templates are most used
  • Which config options are popular
  • Where people struggle (support tickets)

Informed our roadmap.


Conclusion: From Chaos to Platform

Where we started (November 2021):

  • 150+ unique, fragmented pipelines
  • Copy/paste culture
  • High maintenance burden
  • Developer frustration
  • Inconsistent practices

Where we are now (6 months later):

  • ONE shared library powering 142 pipelines
  • Self-service pipeline creation (< 15 min)
  • 87% reduction in maintenance
  • Developer satisfaction: 9/10
  • Platform for innovation

The transformation metrics:

  • Pipeline creation: 2-3 days β†’ 15 minutes (95% faster)
  • Maintenance: 15 hrs/week β†’ 2 hrs/week (87% reduction)
  • Success rate: 70% β†’ 95% (25 point improvement)
  • Cost: $3,200/mo β†’ $2,100/mo ($1,100 savings)

Most importantly: We transformed CI/CD from a bottleneck into an enabler.

The key lessons:

  1. Infrastructure first - Build on solid foundation
  2. Make it easy - Simpler beats better
  3. Incremental change - Migration takes time
  4. Treat as product - Versioning, docs, support
  5. Listen to users - Feedback drives adoption

The philosophical shift:

We stopped seeing CI/CD as β€œpipelines” and started seeing it as a platform. Shared libraries weren’t just code reuseβ€”they were the API for that platform.

When teams can create production-ready pipelines in 15 minutes, they spend more time building features and less time fighting infrastructure.

That’s the win.


Resources

My GitHub Repositories:

Official Documentation:

Tools & Plugins:


About the Author: I’m a Senior DevOps and Cloud Engineer with 11+ years of experience. At Fidelity Information Services, I led the transformation of our CI/CD infrastructure, consolidating 150+ fragmented pipelines into a unified platform using Jenkins Shared Libraries. This work reduced pipeline maintenance by 87% and earned our team the β€œStar Team Award - DevOps 2023.” All infrastructure code and examples are available on my GitHub. Connect with me on LinkedIn.

Questions about Jenkins, shared libraries, or CI/CD transformation? Drop a comment below or reach out on LinkedIn. I’d love to hear about your CI/CD challenges and share experiences!



<
Blog Archive
Archive of all previous blog posts
>
Next Post
Saving $4K/Month: A FinOps Guide to Kubernetes Cost Optimization