Jenkins Pipeline : automatisation avancée
Jenkins reste une solution CI/CD populaire pour les organisations qui nécessitent un contrôle total sur leur infrastructure de build. Là où GitHub Actions ou GitLab CI imposent leur plateforme, Jenkins s'installe sur vos propres serveurs, se connecte à n'importe quel dépôt Git et s'étend grâce à un écosystème de plugins très large. Cette liberté a un prix : c'est à vous de maintenir le contrôleur, les agents et les plugins à jour.
Depuis Jenkins 2, la bonne façon de décrire un build n'est plus de cliquer dans l'interface, mais d'écrire un Jenkinsfile versionné à la racine du projet. Le pipeline évolue alors avec le code : une branche peut modifier sa propre chaîne de build, et chaque changement passe en revue comme n'importe quelle autre modification.
Déclaratif ou scripté ?
Jenkins propose deux syntaxes. Le pipeline scripté est du Groovy libre : puissant, mais difficile à relire et à valider. Le pipeline déclaratif impose une structure (pipeline, agent, stages, post) qui est vérifiée avant l'exécution. Pour une application PHP classique, le déclaratif couvre tous les besoins ; quand une logique complexe devient nécessaire, un bloc script { } ou une bibliothèque partagée permet d'en sortir ponctuellement.
Pipeline déclaratif
Voici un pipeline complet pour un projet Symfony : installation des dépendances, analyse statique et tests en parallèle, puis déploiement depuis la branche principale.
pipeline {
agent {
dockerfile {
filename 'Dockerfile.ci'
}
}
environment {
APP_ENV = 'test'
DATABASE_URL = credentials('database-url')
}
stages {
stage('Install') {
steps {
sh 'composer install --prefer-dist'
}
}
stage('Quality') {
parallel {
stage('PHPStan') {
steps {
sh 'vendor/bin/phpstan analyse src'
}
}
stage('CS Fixer') {
steps {
sh 'vendor/bin/php-cs-fixer fix --dry-run --diff'
}
}
}
}
stage('Test') {
steps {
sh 'php bin/phpunit --log-junit results.xml'
}
post {
always {
junit 'results.xml'
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh './deploy.sh production'
}
}
}
post {
failure {
slackSend channel: '#ci', message: "Build FAILED: ${env.JOB_NAME}"
}
}
}
Lecture du pipeline, bloc par bloc
agent { dockerfile { … } }: chaque build s'exécute dans un conteneur jetable créé à partir de l'image décrite dans leDockerfile.cidu projet (détaillé plus bas). Le plugin Docker Pipeline est requis, et l'agent doit disposer de Docker.environment: définit les variables visibles par toutes les étapes.credentials('database-url')récupère un secret stocké dans Jenkins ; pour un identifiant de type « Secret text », la variable contient directement la valeur, et Jenkins la masque dans les logs.parallel: PHPStan et PHP-CS-Fixer ne dépendent pas l'un de l'autre, ils tournent donc en même temps. Le stage parent échoue si l'un des deux échoue.junit: placé dans unpost { always { } }, il publie le rapport de tests même quand des tests échouent. Jenkins affiche alors l'historique et les tests instables.when { branch 'main' }: cette condition ne fonctionne que dans un job Multibranch Pipeline, où Jenkins connaît le nom de la branche. Dans un job Pipeline simple, le stage serait toujours ignoré.post { failure { } }: la notification Slack ne part qu'en cas d'échec.slackSendest fourni par le plugin Slack Notification.
Une image de build avec Composer
L'image officielle php:8.3-cli ne contient ni Composer, ni git, ni l'extension zip. C'est pourquoi le pipeline s'appuie sur un Dockerfile.ci versionné avec le projet, qui ajoute ce dont le build a besoin :
FROM php:8.3-cli
RUN apt-get update \
&& apt-get install -y --no-install-recommends git unzip libzip-dev libicu-dev openssh-client \
&& docker-php-ext-install zip intl pdo_mysql \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
Avec dockerfile { filename 'Dockerfile.ci' }, Jenkins construit l'image, la met en cache sur l'agent et lance le build dedans. L'environnement de CI est ainsi décrit dans le dépôt, et non dans la configuration d'une machine.
Un pipeline plus robuste
Le premier exemple fonctionne, mais il manque plusieurs garde-fous qu'on ajoute systématiquement en production : une durée maximale, l'interdiction des builds concurrents sur la même branche, la rotation de l'historique, l'archivage des artefacts et une validation humaine avant la mise en production.
pipeline {
agent {
dockerfile {
filename 'Dockerfile.ci'
}
}
options {
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
timestamps()
}
environment {
APP_ENV = 'test'
COMPOSER_HOME = "${env.WORKSPACE}/.composer"
}
stages {
stage('Install') {
steps {
sh 'composer install --prefer-dist --no-progress --no-interaction'
}
}
stage('Package') {
steps {
sh 'mkdir -p dist && tar --exclude=./dist --exclude=./.git -czf dist/app.tar.gz .'
archiveArtifacts artifacts: 'dist/app.tar.gz', fingerprint: true
}
}
stage('Deploy production') {
when {
branch 'main'
beforeInput true
}
input {
message 'Déployer en production ?'
ok 'Déployer'
submitter 'release-managers'
}
steps {
sshagent(credentials: ['deploy-ssh-key']) {
sh './deploy.sh production'
}
}
}
}
post {
always {
cleanWs()
}
}
}
Quelques explications :
timeoutinterrompt un build bloqué (test qui attend un service indisponible, par exemple) au lieu de monopoliser un exécuteur pendant des heures.disableConcurrentBuilds()évite que deux déploiements de la même branche se chevauchent.COMPOSER_HOMEpointe vers le workspace : Jenkins lance le conteneur avec l'UID de l'agent, dont le répertoire personnel n'est souvent pas accessible en écriture dans l'image.inputmet le pipeline en pause jusqu'à validation par un membre du grouperelease-managers. AvecbeforeInput true, la condition de branche est évaluée avant la demande : les autres branches ne sont jamais bloquées.sshagent(plugin SSH Agent) charge la clé de déploiement uniquement le temps de l'étape, sans jamais l'écrire dans le workspace.cleanWs()(plugin Workspace Cleanup) supprime le workspace à la fin, pour que le build suivant reparte d'un état propre.
Pièges fréquents
- Interpolation des secrets : écrire
sh "mysql -p${DB_PASSWORD}"avec des guillemets doubles fait interpoler le secret par Groovy avant l'exécution, et Jenkins affiche un avertissement de sécurité. Utilisez des guillemets simples,sh 'mysql -p"$DB_PASSWORD"', pour laisser le shell lire la variable d'environnement. - Agent bloqué pendant un
input: avec un agent global, l'exécuteur reste réservé pendant l'attente de validation, et letimeoutglobal continue de s'écouler. Pour de longues attentes, déclarezagent noneau niveau du pipeline et un agent par stage. - Plugins non maintenus : chaque plugin est une dépendance. Limitez-vous à ceux dont vous avez besoin et mettez-les à jour régulièrement, car ils sont une source fréquente de vulnérabilités.
- Builds sur le contrôleur : configurez zéro exécuteur sur le nœud principal et faites tourner les builds sur des agents dédiés, pour protéger le contrôleur et ses secrets.
Valider le Jenkinsfile avant de pousser
Une faute de syntaxe dans un Jenkinsfile ne se découvre souvent qu'après le push. Jenkins expose un linter pour les pipelines déclaratifs, utilisable avec un jeton d'API :
curl -X POST --user "$JENKINS_USER:$JENKINS_TOKEN" \
-F "jenkinsfile=<Jenkinsfile" \
"$JENKINS_URL/pipeline-model-converter/validate"
La réponse indique si le fichier est valide ou détaille la ligne en erreur. Cette commande s'intègre facilement dans un hook Git ou dans l'éditeur.
Mutualiser avec les Shared Libraries
Quand plusieurs projets partagent les mêmes étapes (notification, déploiement, publication d'image), dupliquer le Jenkinsfile devient vite ingérable. Les Shared Libraries permettent de placer ce code commun dans un dépôt Git séparé, chargé avec @Library('nom-de-la-lib') _ en tête du Jenkinsfile. Chaque projet garde alors un pipeline court et lisible.
Bonnes pratiques Jenkins
- Utiliser des agents Docker pour l'isolation
- Paralléliser les stages indépendants
- Stocker les credentials dans Jenkins Credentials
- Configurer les notifications pour les échecs
- Archiver les artefacts de build
- Versionner le Jenkinsfile et l'image de build avec le code
- Toujours définir un
timeoutet une rotation de l'historique
Quand ne pas choisir Jenkins
Si votre code est hébergé sur GitHub ou GitLab et que vous n'avez pas de contrainte d'hébergement particulière, les outils intégrés à ces plateformes demandent beaucoup moins de maintenance. Jenkins prend tout son sens lorsque les builds doivent rester sur votre réseau, accéder à des ressources internes, ou lorsque vous avez déjà une équipe capable d'administrer la plateforme dans la durée.