Back to CI/CD Pipeline
Q31Question 31 of 56

Avoiding a Separate Pipeline per Microservice: Shared Libraries, Templated Pipelines, and Per-Branch Environment Routing

Key Concepts
  • A Jenkins Shared Library is a versioned Git repository with reusable Groovy steps under vars/ and structured classes under src/.
  • One library step like bankPipeline contains the entire standard pipeline, and each service's Jenkinsfile shrinks to two lines.
  • Per-service variation comes from a small config file like bank-ci.yaml loaded by the template with readYaml.
  • Branch-to-environment routing maps env.BRANCH_NAME to a target namespace so every service routes consistently.
  • The Prod deploy stage has an input approval gate restricted by submitter and wrapped in a timeout.
  • Pinning the library to a version tag gives reproducibility and an audit trail across all services.

Definition

One versioned Shared Library plus a tiny per-service config replaces thousands of copy-pasted pipelines, and the branch name decides the environment with an approval gate before Prod.
  • Shared Library – a versioned Git repository with reusable Groovy steps (vars/) and classes (src/), loaded with @Library.
  • Templated pipeline – the entire standard pipeline lives in one library step; each microservice's Jenkinsfile calls it with per-service parameters.
  • Per-service config – a small file like bank-ci.yaml checked into each service repo, read by the template with readYaml.
  • Branch-to-environment routing – the template maps env.BRANCH_NAME to a target namespace (dev branch to Dev, qa to QA, main to Prod).
  • Approval gate – an input step restricted by submitter to named release managers, wrapped in a timeout.

The National Bank of India has thousands of branches, and nobody writes a separate operations manual for each one. Instead there is one master manual at head office (the Shared Library), and every branch has a one-page cover sheet saying its name and manager. When head office improves a procedure, every branch gets it. Which counter a cheque goes to depends on the door: the dev door routes to the practice counter, the qa door to testing, and the main vault needs a manager's signature.

Understanding the Concepts

What is a Jenkins Shared Library?

A Shared Library is a Git repository with a fixed layout. The vars/ directory holds global steps and the src/ directory holds regular Groovy classes. The library is registered in Jenkins and loaded per-job with @Library.

TEXT
bank-shared-lib/
  vars/
    bankPipeline.groovy      # Callable as bankPipeline()
    bankDeploy.groovy         # Callable as bankDeploy()
  src/
    com/bank/ci/Deployer.groovy
    com/bank/ci/Scanner.groovy
  resources/
    pod-template.yaml
GROOVY
// vars/bankPipeline.groovy: the full templated pipeline
def call(Map config) {
    pipeline {
        agent { kubernetes { yaml libraryResource('pod-template.yaml') } }
        stages {
            stage('Build')  { steps { sh "mvn clean package -pl ${config.service}" } }
            stage('Test')   { steps { sh "mvn verify -pl ${config.service}" } }
            stage('Scan')   { steps { sh 'mvn sonar:sonar' } }
            stage('Image')  { steps { sh "docker build -t \${REGISTRY}/${config.service}:\${GIT_COMMIT} ." } }
            stage('Deploy') { steps { bankDeploy(service: config.service) } }
        }
    }
}

How does the templated pipeline pattern work?

Each microservice's Jenkinsfile shrinks to two lines. It loads the library and calls the template step with only what is unique to that service.

GROOVY
// transfer-service/Jenkinsfile (entire file)
@Library('bank-shared-lib@v3') _
bankPipeline(service: 'transfer-service', javaVersion: 21)
GROOVY
// fraud-service/Jenkinsfile (entire file)
@Library('bank-shared-lib@v3') _
bankPipeline(service: 'fraud-service', javaVersion: 21)

When the security team mandates a new image scan, you add it once to bankPipeline.groovy and every service picks it up on the next build.

How does per-service configuration work?

For variation beyond simple parameters, each service checks in a small config file that the template loads with readYaml.

YAML
# transfer-service/bank-ci.yaml
service: transfer-service
javaVersion: 21
deployTargets: [dev, qa, prod]
extraStages:
  - name: Load Test
    command: gatling:test
GROOVY
// Inside bankPipeline.groovy
def config = readYaml file: 'bank-ci.yaml'
if (config.extraStages) {
    for (extra in config.extraStages) {
        stage(extra.name) { steps { sh extra.command } }
    }
}

How does branch-to-environment routing work?

The template reads env.BRANCH_NAME and maps it to a target environment. Because the mapping is in one place, every service routes consistently.

GROOVY
// Inside bankPipeline.groovy
def envMap = [dev: 'dev-ns', qa: 'qa-ns', main: 'prod-ns']
def targetNs = envMap[env.BRANCH_NAME]

if (targetNs) {
    stage("Deploy to ${targetNs}") {
        steps {
            sh "kubectl set image deployment/${config.service} " +
               "${config.service}=\${REGISTRY}/${config.service}:\${GIT_COMMIT} " +
               "-n ${targetNs}"
        }
    }
}

In Declarative syntax you can also use when { branch 'qa' } on each deploy stage.

GROOVY
stage('Deploy QA') {
    when { branch 'qa' }
    steps { sh "kubectl apply -f deploy/qa/ -n qa-ns" }
}
stage('Deploy Prod') {
    when { branch 'main' }
    steps { /* ... with approval gate */ }
}

How do approval gates protect production?

The Prod deploy stage is preceded by an input step restricted by submitter to a named group and wrapped in a timeout.

GROOVY
stage('Deploy Prod') {
    when { branch 'main' }
    steps {
        timeout(time: 4, unit: 'HOURS') {
            input message: 'Deploy to production?',
                  submitter: 'release-managers'
        }
        sh "helm upgrade ${config.service} ./chart " +
           "--set image.tag=${env.GIT_COMMIT} -n prod-ns"
    }
}

Lower environments have no gate. The gate is defined once in the library, so no service can accidentally skip it.

What are common mistakes?

GROOVY
// WRONG: copy-pasting the pipeline into every service.
// 200 services = 200 identical Jenkinsfiles to maintain.
// transfer-service/Jenkinsfile (100+ lines)
pipeline {
    stages {
        stage('Build') { steps { sh 'mvn package' } }
        stage('Test')  { steps { sh 'mvn verify' } }
        stage('Scan')  { steps { sh 'mvn sonar:sonar' } }
        stage('Deploy'){ steps { sh 'kubectl apply ...' } }
    }
}
// When you add a scan step, you must edit 200 files.

// CORRECT: one library, one template, two-line Jenkinsfiles.
@Library('bank-shared-lib@v3') _
bankPipeline(service: 'transfer-service')
// Add a scan step once in the library. All 200 services get it.
GROOVY
// WRONG: loading the library from the main branch (unpinned).
@Library('bank-shared-lib') _  // Gets whatever is on main

// CORRECT: pinning to a version tag.
@Library('bank-shared-lib@v3') _  // Known, audited version

How do you handle service-specific stages?

The template supports optional hooks so a service can add stages without modifying the library.

GROOVY
// Inside bankPipeline.groovy
if (config.preDeploy) {
    stage('Pre-Deploy Hook') {
        steps { sh config.preDeploy }
    }
}
YAML
# transfer-service/bank-ci.yaml: service-specific hook
preDeploy: './scripts/db-migrate.sh'

What should a senior engineer know?

  • Pin the library to a version tag for reproducibility and audit. Library changes go through code review and their own tests.
  • Validate per-service config: reject unknown keys and sanitize dangerous values, because the template runs with broad permissions.
  • A bug in the shared library breaks every service at once, so library changes are high-impact and must be tested thoroughly.
  • Support truly service-specific stages with optional hooks or extra-stages in the config, so the template stays generic.
GROOVY
// Senior pattern: validated config loading
def config = readYaml file: 'bank-ci.yaml'
def allowed = ['service', 'javaVersion', 'deployTargets', 'preDeploy', 'extraStages']
def unknown = config.keySet() - allowed
if (unknown) {
    error "Unknown config keys: ${unknown}. Fix bank-ci.yaml."
}

Visual Overview

DIAGRAM
  Shared Library repo (bank-shared-lib)
  ┌─────────────────────────────────┐
  │  vars/bankPipeline.groovy        │  ← Full pipeline template
  │  vars/bankDeploy.groovy          │  ← Reusable deploy step
  │  src/com/bank/ci/Scanner.groovy  │  ← Structured classes
  └─────────────────────────────────┘
           │
           │  @Library('bank-shared-lib@v3') _
           ▼
  ┌──────────────────────┐  ┌──────────────────────┐
  │ transfer-service      │  │ fraud-service         │
  │ Jenkinsfile (2 lines) │  │ Jenkinsfile (2 lines) │
  │ bank-ci.yaml          │  │ bank-ci.yaml          │
  └──────────────────────┘  └──────────────────────┘

  Branch-to-environment routing:

  dev branch  ───▶  Dev namespace   (no gate)
  qa  branch  ───▶  QA  namespace   (no gate)
  main branch ───▶  Prod namespace  (approval gate)
                         │
                    ┌────┴────┐
                    │  input   │
                    │ submitter│
                    │ release- │
                    │ managers │
                    └─────────┘

Key Takeaway

To avoid maintaining hundreds of near-identical pipelines, I centralize the pipeline logic in a Jenkins Shared Library. The entire standard pipeline lives in one library step, and each microservice keeps a tiny two-line Jenkinsfile that loads the library and calls that step. When the security team mandates a new scan, I add it once to the template and every service picks it up on the next build.

Environment routing is driven by the branch name and owned by the template. A push to dev deploys to the Dev namespace, qa to QA, and main to Prod. Production is protected by an approval gate with input restricted to named release managers and wrapped in a timeout, so no service can skip it.

Governance is the senior concern. I pin the library to a version tag so every service gets a known, audited version. Library changes go through their own code review and tests, because a bug in the shared library breaks every service at once. I validate per-service config and reject unknown keys, because the template runs with broad permissions.

Remember one sentence: one versioned Shared Library plus a tiny per-service config replaces thousands of copy-pasted pipelines.

---

Share
Use ← → arrow keys to navigate