Questions
50 teams are duplicating pipeline code. Design a shared library architecture for the organization.
The Scenario
Your organization has grown to 50 engineering teams, each with their own Jenkins pipelines. You notice:
- Every team has copy-pasted the same deployment code with slight variations
- Security vulnerabilities found in one pipeline exist in 200+ others
- Teams spend days writing boilerplate instead of shipping features
- No standardization for notifications, approvals, or rollback procedures
Leadership wants a solution that enables consistency while allowing team flexibility.
The Challenge
Design a shared library architecture that provides reusable, versioned pipeline components while allowing teams to customize behavior for their specific needs.
A junior engineer might create one giant shared library with all logic, force all teams to use identical pipelines, or just document best practices and hope teams follow them. These approaches fail because monolithic libraries are hard to maintain, forced standardization kills productivity, and documentation without enforcement leads to drift.
A senior engineer designs a modular shared library with clear separation of concerns, versioned releases for stability, sensible defaults with override capabilities, and comprehensive testing. They implement a governance model that balances standardization with team autonomy.
Step 1: Design Library Structure
jenkins-shared-library/
├── vars/ # Global variables (pipeline steps)
│ ├── standardPipeline.groovy # Full pipeline template
│ ├── buildApp.groovy # Build step
│ ├── deployToK8s.groovy # Kubernetes deployment
│ ├── notifySlack.groovy # Slack notifications
│ ├── securityScan.groovy # Security scanning
│ └── runTests.groovy # Test execution
├── src/
│ └── org/
│ └── company/
│ └── jenkins/
│ ├── Config.groovy # Configuration classes
│ ├── DeploymentStrategy.groovy
│ ├── NotificationService.groovy
│ └── SecurityValidator.groovy
├── resources/
│ ├── templates/
│ │ ├── kubernetes/
│ │ │ ├── deployment.yaml
│ │ │ └── service.yaml
│ │ └── docker/
│ │ └── Dockerfile.template
│ └── scripts/
│ └── security-scan.sh
└── test/
└── groovy/
└── vars/
└── StandardPipelineTest.groovyStep 2: Create Core Pipeline Template
// vars/standardPipeline.groovy
def call(Map config = [:]) {
// Validate required parameters
validateConfig(config)
// Merge with defaults
def pipelineConfig = mergeWithDefaults(config)
pipeline {
agent { label pipelineConfig.agentLabel ?: 'default' }
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: pipelineConfig.timeoutMinutes ?: 60, unit: 'MINUTES')
timestamps()
ansiColor('xterm')
}
environment {
APP_NAME = pipelineConfig.appName
TEAM = pipelineConfig.team
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
when { expression { pipelineConfig.enableBuild != false } }
steps {
script {
buildApp(pipelineConfig.build ?: [:])
}
}
}
stage('Test') {
when { expression { pipelineConfig.enableTests != false } }
parallel {
stage('Unit Tests') {
steps {
runTests(type: 'unit', config: pipelineConfig.tests?.unit ?: [:])
}
}
stage('Integration Tests') {
when { expression { pipelineConfig.tests?.integration?.enabled != false } }
steps {
runTests(type: 'integration', config: pipelineConfig.tests?.integration ?: [:])
}
}
}
}
stage('Security Scan') {
when { expression { pipelineConfig.enableSecurityScan != false } }
steps {
securityScan(pipelineConfig.security ?: [:])
}
}
stage('Deploy to Dev') {
when {
branch 'develop'
expression { pipelineConfig.environments?.dev?.enabled != false }
}
steps {
deployToK8s(
environment: 'dev',
config: pipelineConfig.environments?.dev ?: [:]
)
}
}
stage('Deploy to Production') {
when {
branch 'main'
expression { pipelineConfig.environments?.prod?.enabled != false }
}
steps {
script {
// Require approval for production
if (pipelineConfig.environments?.prod?.requireApproval != false) {
approveDeployment(
environment: 'production',
approvers: pipelineConfig.environments?.prod?.approvers ?: ['platform-team']
)
}
deployToK8s(
environment: 'prod',
config: pipelineConfig.environments?.prod ?: [:]
)
}
}
}
}
post {
always {
script {
notifySlack(
channel: pipelineConfig.notifications?.slackChannel ?: '#builds',
status: currentBuild.result
)
}
}
failure {
script {
if (pipelineConfig.notifications?.pagerduty?.enabled) {
triggerPagerDuty(pipelineConfig.notifications.pagerduty)
}
}
}
}
}
}
def validateConfig(Map config) {
def required = ['appName', 'team']
def missing = required.findAll { !config.containsKey(it) }
if (missing) {
error "Missing required configuration: ${missing.join(', ')}"
}
}
def mergeWithDefaults(Map config) {
def defaults = [
agentLabel: 'docker',
timeoutMinutes: 60,
enableBuild: true,
enableTests: true,
enableSecurityScan: true
]
return defaults + config
}Step 3: Create Modular Components
// vars/buildApp.groovy
def call(Map config = [:]) {
def buildType = config.type ?: detectBuildType()
switch(buildType) {
case 'maven':
buildMaven(config)
break
case 'gradle':
buildGradle(config)
break
case 'npm':
buildNpm(config)
break
case 'docker':
buildDocker(config)
break
default:
error "Unknown build type: ${buildType}"
}
}
def buildDocker(Map config) {
def imageName = config.imageName ?: "${env.APP_NAME}"
def registry = config.registry ?: 'registry.company.com'
def tag = config.tag ?: "${env.GIT_COMMIT?.take(8) ?: 'latest'}"
withCredentials([usernamePassword(
credentialsId: config.registryCredentials ?: 'docker-registry',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh """
echo \$DOCKER_PASS | docker login ${registry} -u \$DOCKER_USER --password-stdin
docker build -t ${registry}/${imageName}:${tag} .
docker push ${registry}/${imageName}:${tag}
"""
}
// Return image reference for downstream stages
return "${registry}/${imageName}:${tag}"
}
def detectBuildType() {
if (fileExists('pom.xml')) return 'maven'
if (fileExists('build.gradle')) return 'gradle'
if (fileExists('package.json')) return 'npm'
if (fileExists('Dockerfile')) return 'docker'
error "Could not detect build type. Please specify config.type"
}// vars/deployToK8s.groovy
def call(Map args) {
def environment = args.environment
def config = args.config ?: [:]
def namespace = config.namespace ?: "${env.APP_NAME}-${environment}"
def kubeconfig = config.kubeconfig ?: "kubeconfig-${environment}"
withCredentials([file(credentialsId: kubeconfig, variable: 'KUBECONFIG')]) {
script {
// Apply Kubernetes manifests
def manifests = config.manifests ?: "k8s/${environment}"
// Substitute environment variables
sh """
envsubst < ${manifests}/deployment.yaml | kubectl apply -f -
envsubst < ${manifests}/service.yaml | kubectl apply -f -
"""
// Wait for rollout
def deploymentName = config.deploymentName ?: env.APP_NAME
timeout(time: config.rolloutTimeout ?: 10, unit: 'MINUTES') {
sh """
kubectl rollout status deployment/${deploymentName} \
-n ${namespace} \
--timeout=600s
"""
}
// Run smoke tests if configured
if (config.smokeTest) {
runSmokeTest(config.smokeTest)
}
}
}
}Step 4: Enable Team Customization
// Team's Jenkinsfile - Simple usage with defaults
@Library('company-shared-library@v2.0') _
standardPipeline(
appName: 'user-service',
team: 'platform'
)// Team's Jenkinsfile - Full customization
@Library('company-shared-library@v2.0') _
standardPipeline(
appName: 'payment-service',
team: 'payments',
agentLabel: 'secure-agents',
timeoutMinutes: 90,
build: [
type: 'docker',
imageName: 'payment-service',
registry: 'registry.company.com/payments'
],
tests: [
unit: [enabled: true],
integration: [
enabled: true,
database: 'postgres:13'
]
],
security: [
scanners: ['snyk', 'trivy'],
failOnCritical: true
],
environments: [
dev: [enabled: true, namespace: 'payments-dev'],
staging: [enabled: true, namespace: 'payments-staging'],
prod: [
enabled: true,
namespace: 'payments-prod',
requireApproval: true,
approvers: ['payments-leads', 'sre-team']
]
],
notifications: [
slackChannel: '#payments-builds',
pagerduty: [enabled: true, serviceKey: 'payments-pd']
]
)Step 5: Versioning and Release Strategy
// Jenkinsfile for the shared library itself
pipeline {
agent any
stages {
stage('Test') {
steps {
sh './gradlew test'
}
}
stage('Release') {
when { branch 'main' }
steps {
script {
def version = sh(
script: 'git describe --tags --abbrev=0',
returnStdout: true
).trim()
// Tag and push
sh """
git tag -a v${version} -m "Release ${version}"
git push origin v${version}
"""
}
}
}
}
}Configure in Jenkins:
Manage Jenkins > Configure System > Global Pipeline Libraries
Name: company-shared-library
Default version: main
Allow default version to be overridden: true
Include @Library changes in job recent changes: true
Retrieval method: Modern SCM
- Git
- Project Repository: https://github.com/company/jenkins-shared-library
- Behaviors: Discover branches, Discover tags Shared Library Best Practices
| Practice | Why | How |
|---|---|---|
| Semantic versioning | Teams can pin to stable versions | Use git tags: v1.0.0, v2.0.0 |
| Comprehensive testing | Catch bugs before teams hit them | Unit test with JenkinsPipelineUnit |
| Sensible defaults | Teams shouldn’t need to configure everything | Provide working defaults, allow overrides |
| Clear documentation | Teams need to know what’s available | README, examples, changelog |
| Deprecation policy | Allow time for teams to migrate | Warn for 2 versions before removing |
Practice Question
What is the primary advantage of using vars/ directory over src/ directory for shared library functions in Jenkins?