Aller au contenu principal
JenkinsCI/CDDevOps

Jenkins #3 — Pipeline Avancé : Scripté, Shared Libraries et Parallel Stages

30 min de lecture Jenkins — Chapitre 3

Maîtrise les pipelines Jenkins avancés : scripté vs déclaratif, shared libraries, stages parallèles, input/approval et patterns de production.

Le pipeline déclaratif couvre 90% des besoins. Pour les 10% restants — logique dynamique, code réutilisable entre projets, matrices de test — il faut monter en puissance. Ce cours couvre le pipeline scripté, les Shared Libraries, les stages parallèles, et les approval gates.

Scripté vs déclaratif : quand choisir lequel

Le pipeline scripté utilise du Groovy pur. Pas de structure imposée, pas de pipeline {} — juste un node {} et du code. C’est puissant mais moins lisible.

node('linux') {
    try {
        stage('Checkout') { checkout scm }
        stage('Build') {
            sh 'npm ci'
            sh 'npm run build'
        }
        stage('Test') { sh 'npm test' }

        if (env.BRANCH_NAME == 'main') {
            stage('Deploy') { sh './deploy.sh production' }
        }
    } catch (Exception e) {
        currentBuild.result = 'FAILURE'
        slackSend color: 'danger', message: "❌ ${env.JOB_NAME} échoué"
        throw e
    } finally {
        cleanWs()
    }
}

💡 Le meilleur compromis : reste en déclaratif et utilise script {} pour les blocs qui nécessitent du Groovy (boucles, conditions complexes). Ça garde la lisibilité tout en permettant la flexibilité.

⚠️ Si tu te retrouves avec script {} dans chaque stage, c’est un signal : extrais la logique dans une Shared Library plutôt que de polluer le Jenkinsfile.

Shared Libraries : le code pipeline réutilisable

Le problème classique : 15 microservices avec des Jenkinsfiles quasi identiques. Un changement de logique = 15 fichiers à modifier. La Shared Library centralise le code pipeline dans un repo Git dédié.

Structure

jenkins-shared-lib/
├── vars/                  # Fonctions globales (le plus utilisé)
│   ├── buildDocker.groovy
│   ├── notifySlack.groovy
│   └── standardPipeline.groovy
├── src/                   # Classes Groovy (logique complexe)
│   └── com/example/
│       └── Docker.groovy
└── resources/             # Fichiers statiques (templates)

Créer une fonction réutilisable

Fichier vars/buildDocker.groovy — chaque fichier dans vars/ devient une fonction appelable directement dans les Jenkinsfiles :

def call(Map config = [:]) {
    def image = config.imageName ?: error("imageName requis")
    def registry = config.registry ?: 'registry.company.com'
    def tag = config.tag ?: env.BUILD_NUMBER

    echo "🐳 Building ${registry}/${image}:${tag}"
    sh "docker build -t ${registry}/${image}:${tag} ."

    withCredentials([usernamePassword(
        credentialsId: config.credentialsId ?: 'registry-creds',
        usernameVariable: 'USER', passwordVariable: 'PASS'
    )]) {
        sh "echo \$PASS | docker login ${registry} -u \$USER --password-stdin"
        sh "docker push ${registry}/${image}:${tag}"
    }
    return "${registry}/${image}:${tag}"
}

Créer un pipeline template complet

Le pattern le plus puissant : un template dans vars/standardPipeline.groovy qui encapsule tout le workflow.

def call(Map config = [:]) {
    pipeline {
        agent none
        options {
            timeout(time: config.timeout ?: 30, unit: 'MINUTES')
            buildDiscarder(logRotator(numToKeepStr: '10'))
            timestamps()
        }
        stages {
            stage('Test') {
                agent { docker { image config.buildImage ?: 'node:20-alpine' } }
                steps {
                    checkout scm
                    sh config.testCommand ?: 'npm ci && npm test'
                }
            }
            stage('Build & Push') {
                agent any
                steps {
                    checkout scm
                    buildDocker(imageName: config.imageName, registry: config.registry)
                }
            }
            stage('Deploy') {
                agent any
                when { branch 'main' }
                steps {
                    sh config.deployCommand ?: "kubectl apply -f k8s/"
                }
            }
        }
        post {
            success { slackSend color: 'good', message: "✅ ${env.JOB_NAME} #${env.BUILD_NUMBER}" }
            failure { slackSend color: 'danger', message: "❌ ${env.JOB_NAME} #${env.BUILD_NUMBER}" }
        }
    }
}

Résultat : chaque microservice a un Jenkinsfile de 10 lignes :

@Library('my-shared-lib') _

standardPipeline(
    imageName: 'mon-service-api',
    buildImage: 'node:20-alpine',
    testCommand: 'npm ci && npm test -- --coverage',
    timeout: 20
)

🔥 Un changement dans la lib = tous les pipelines mis à jour au prochain build. Configure la lib dans Manage Jenkins → System → Global Pipeline Libraries avec le repo Git et la branche par défaut.

Stages parallèles et matrix

Les stages parallèles exécutent plusieurs branches simultanément. Le gain de temps est énorme sur les suites de tests.

stage('Quality Gates') {
    failFast true
    parallel {
        stage('Unit Tests') {
            agent { docker { image 'node:20' } }
            steps { sh 'npm run test:unit' }
        }
        stage('Integration Tests') {
            agent { docker { image 'node:20' } }
            steps { sh 'npm run test:integration' }
        }
        stage('E2E Tests') {
            agent { docker { image 'cypress/included:13' } }
            steps { sh 'npm run test:e2e' }
        }
        stage('Security Scan') {
            agent { docker { image 'node:20' } }
            steps { sh 'npm audit --audit-level=high' }
        }
    }
}

failFast true stoppe tous les stages parallèles dès qu’un seul échoue — pas besoin d’attendre les 3 autres si les unit tests cassent en 30 secondes.

Pour les matrices de compatibilité (Node 18/20/22 × Linux/Windows), la directive matrix génère automatiquement les combinaisons :

stage('Compatibility') {
    matrix {
        axes {
            axis { name 'NODE_VERSION'; values '18', '20', '22' }
            axis { name 'OS'; values 'linux', 'windows' }
        }
        excludes {
            exclude {
                axis { name 'NODE_VERSION'; values '18' }
                axis { name 'OS'; values 'windows' }
            }
        }
        stages {
            stage('Test') {
                agent { label "${OS}" }
                steps { sh "nvm use ${NODE_VERSION} && npm test" }
            }
        }
    }
}

🎯 5 combinaisons testées en parallèle (18/windows exclu). Sans matrix, ça serait 5 stages copié-collés.

Input et Approval Gates

L’input pause le pipeline et attend une action humaine. Indispensable pour les déploiements production qui nécessitent une validation.

stage('Approve Production') {
    agent none  // Libère l'executor pendant l'attente
    options { timeout(time: 1, unit: 'HOURS') }
    steps {
        script {
            def approval = input(
                message: 'Déployer en production ?',
                ok: '🚀 Deploy!',
                submitter: 'ops-team,release-managers',
                parameters: [
                    choice(name: 'STRATEGY', choices: ['rolling', 'blue-green', 'canary'])
                ]
            )
            env.DEPLOY_STRATEGY = approval
        }
    }
}

stage('Deploy Production') {
    agent any
    steps {
        sh "./deploy.sh production --strategy=${env.DEPLOY_STRATEGY}"
    }
}

⚠️ agent none sur le stage d’input est critique — sans ça, l’executor est bloqué pendant toute l’attente. Avec 4 executors et 4 builds en attente d’approbation, plus rien ne tourne.

💡 Utilise milestone pour éviter les race conditions : si le build #5 est approuvé avant le #4, le milestone annule automatiquement le #4 pour ne pas écraser le déploiement plus récent.

Cas entreprise : 40 microservices, 1 Shared Library

Une plateforme SaaS avec 40 microservices — Java, Node.js, Python. Avant la Shared Library : 40 Jenkinsfiles de 80-150 lignes chacun, maintenus par chaque équipe. Résultat : des incohérences partout (certains pushaient des images sans tag, d’autres oubliaient le scan de sécurité).

Après la migration :

  • 1 Shared Library avec 3 templates : javaPipeline, nodePipeline, pythonPipeline
  • Chaque template inclut : build → test → scan SAST → Docker build/push → deploy staging auto → approval → deploy prod
  • 40 Jenkinsfiles de 8-15 lignes qui appellent le bon template avec leurs paramètres

Un audit sécurité a demandé d’ajouter un scan Trivy sur toutes les images. Changement dans la lib : 1 commit, 1 review, 1 merge. Les 40 services ont eu le scan Trivy au build suivant, sans toucher un seul Jenkinsfile.

Pièges classiques et résumé

⚠️ La sandbox Groovy — Jenkins exécute les Jenkinsfiles dans une sandbox qui bloque certaines méthodes Groovy. Les Shared Libraries dans vars/ tournent hors sandbox (trusted), mais src/ est sandboxé par défaut. Si tu as des RejectedAccessException, configure les script approvals dans Manage Jenkins → In-process Script Approval.

⚠️ Le stash trop grosstash stocke les fichiers sur le controller Jenkins. Pour des artifacts de 500MB+, c’est un goulot d’étranglement. Utilise archiveArtifacts ou un stockage externe (S3, Nexus, Artifactory) pour les gros fichiers.

🔥 Le versionning de la lib — En prod, pin ta Shared Library sur un tag : @Library('my-lib@v2.1.0'). Ne pointe jamais sur main en production — un merge accidentel dans la lib casserait tous tes pipelines simultanément.

Le pipeline avancé Jenkins repose sur trois piliers. Les Shared Libraries centralisent la logique et transforment 40 Jenkinsfiles verbeux en 40 appels de 10 lignes. Les stages parallèles et matrix divisent le temps de feedback. Les approval gates avec input + milestone sécurisent les déploiements production. Combine les trois pour un CI/CD maintenable, rapide et fiable.

🖥️ Pratique sur ton propre serveur

Pour suivre Jenkins en conditions réelles, tu as besoin d'un VPS. DigitalOcean offre 200$ de crédit gratuit pour démarrer.

Obtenir 200$

Articles liés