Bonnes Pratiques Jenkins
Organisation des jobs
Structure en folders
jenkins-jobs/
├── frontend/
│ ├── web-app/
│ │ ├── main # Pipeline principal
│ │ └── pr # Pull Requests
│ └── mobile-app/
├── backend/
│ ├── api/
│ └── workers/
├── infrastructure/
│ ├── terraform/
│ └── kubernetes/
└── shared/
└── deploy-templates/
Convention de nommage
// ✅ Bon
"frontend-webapp-build"
"backend-api-deploy-staging"
"infra-terraform-plan"
// ❌ Mauvais
"job1"
"test"
"my-pipeline"
Pipeline as Code
Toujours utiliser un Jenkinsfile
// ✅ Jenkinsfile versionné dans le repo
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'npm run build'
}
}
}
}
// ❌ Pipeline configuré dans l'UI Jenkins
// Difficile à versionner et auditer
Shared Libraries
// vars/standardPipeline.groovy
def call(Map config) {
pipeline {
agent {
kubernetes {
yaml libraryResource('pod-templates/node.yaml')
}
}
environment {
APP_NAME = config.appName
DOCKER_REGISTRY = 'registry.example.com'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Test') {
when { expression { return config.runTests != false } }
steps {
sh 'npm test'
}
}
stage('Docker') {
steps {
script {
docker.build("${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER}")
}
}
}
}
post {
always {
cleanWs()
}
}
}
}
// Jenkinsfile simplifié
@Library('jenkins-shared-lib') _
standardPipeline(
appName: 'my-app',
runTests: true
)
Optimisation des builds
Cache des dépendances
pipeline {
agent {
kubernetes {
yaml '''
spec:
containers:
- name: node
image: node:18
volumeMounts:
- name: npm-cache
mountPath: /root/.npm
volumes:
- name: npm-cache
persistentVolumeClaim:
claimName: npm-cache-pvc
'''
}
}
stages {
stage('Build') {
steps {
container('node') {
sh 'npm ci --cache /root/.npm'
}
}
}
}
}
Parallélisation
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'npm run build'
}
}
stage('Tests') {
parallel {
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
}
stage('Integration Tests') {
steps {
sh 'npm run test:integration'
}
}
stage('E2E Tests') {
steps {
sh 'npm run test:e2e'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
}
}
}
}
Builds incrémentaux
pipeline {
agent any
stages {
stage('Check Changes') {
steps {
script {
def changes = sh(
script: 'git diff --name-only HEAD~1',
returnStdout: true
).trim()
env.FRONTEND_CHANGED = changes.contains('frontend/')
env.BACKEND_CHANGED = changes.contains('backend/')
}
}
}
stage('Build Frontend') {
when {
expression { return env.FRONTEND_CHANGED == 'true' }
}
steps {
dir('frontend') {
sh 'npm run build'
}
}
}
stage('Build Backend') {
when {
expression { return env.BACKEND_CHANGED == 'true' }
}
steps {
dir('backend') {
sh 'mvn package'
}
}
}
}
}
Gestion des erreurs
Retry avec backoff
def deployWithRetry(String environment) {
def maxRetries = 3
def retryDelay = 30
for (int i = 0; i < maxRetries; i++) {
try {
sh "kubectl apply -f k8s/${environment}/"
return true
} catch (Exception e) {
if (i < maxRetries - 1) {
echo "Deploy failed, retry in ${retryDelay}s (attempt ${i + 1}/${maxRetries})"
sleep(retryDelay)
retryDelay *= 2 // Exponential backoff
} else {
throw e
}
}
}
}
pipeline {
agent any
stages {
stage('Deploy') {
steps {
script {
deployWithRetry('staging')
}
}
}
}
}
Timeout et fallback
stage('Deploy') {
steps {
timeout(time: 10, unit: 'MINUTES') {
script {
try {
sh 'kubectl apply -f k8s/'
sh 'kubectl rollout status deployment/app --timeout=5m'
} catch (Exception e) {
echo "Deploy failed, rolling back..."
sh 'kubectl rollout undo deployment/app'
throw e
}
}
}
}
}
Notifications intelligentes
Notifier uniquement si changement
post {
success {
script {
if (currentBuild.previousBuild?.result == 'FAILURE') {
slackSend(
color: 'good',
message: "✅ ${JOB_NAME} is back to normal!\n${BUILD_URL}"
)
}
}
}
failure {
script {
// Ne pas spammer si déjà en échec
if (currentBuild.previousBuild?.result != 'FAILURE') {
slackSend(
color: 'danger',
message: "❌ ${JOB_NAME} #${BUILD_NUMBER} failed\n${BUILD_URL}"
)
}
}
}
}
Notifications riches
def notifySlack(String status) {
def color = [
'SUCCESS': 'good',
'FAILURE': 'danger',
'UNSTABLE': 'warning'
][status] ?: 'warning'
def duration = currentBuild.durationString.replace(' and counting', '')
def commit = env.GIT_COMMIT?.take(7)
def author = sh(script: 'git log -1 --format="%an"', returnStdout: true).trim()
slackSend(
color: color,
attachments: [[
title: "${JOB_NAME} #${BUILD_NUMBER}",
title_link: BUILD_URL,
color: color,
fields: [
[title: 'Status', value: status, short: true],
[title: 'Duration', value: duration, short: true],
[title: 'Commit', value: commit, short: true],
[title: 'Author', value: author, short: true]
]
]]
)
}
post {
always {
notifySlack(currentBuild.currentResult)
}
}
Maintenance
Rotation des builds
# jenkins.yaml
jobs:
- script: >
pipelineJob('my-job') {
properties {
buildDiscarder {
strategy {
logRotator {
numToKeepStr('20')
daysToKeepStr('30')
artifactNumToKeepStr('5')
}
}
}
}
}
// Dans le pipeline
options {
buildDiscarder(logRotator(
numToKeepStr: '20',
daysToKeepStr: '30',
artifactNumToKeepStr: '5'
))
}
Nettoyage des workspaces
pipeline {
agent any
options {
skipDefaultCheckout()
}
stages {
stage('Checkout') {
steps {
cleanWs()
checkout scm
}
}
// ...
}
post {
always {
cleanWs(
cleanWhenSuccess: true,
cleanWhenFailure: false, // Garder pour debug
cleanWhenAborted: true,
deleteDirs: true,
patterns: [
[pattern: 'node_modules', type: 'INCLUDE'],
[pattern: '.git', type: 'EXCLUDE']
]
)
}
}
}
Anti-patterns à éviter
1. Scripts shell inline trop longs
// ❌ Mauvais
stage('Build') {
steps {
sh '''
echo "Starting build"
npm ci
npm run lint
npm run test
npm run build
docker build -t app .
docker push registry/app
kubectl apply -f k8s/
kubectl rollout status deployment/app
'''
}
}
// ✅ Bon - Étapes séparées
stage('Install') {
steps { sh 'npm ci' }
}
stage('Lint') {
steps { sh 'npm run lint' }
}
stage('Test') {
steps { sh 'npm run test' }
}
stage('Build') {
steps { sh 'npm run build' }
}
stage('Deploy') {
steps { sh './scripts/deploy.sh' }
}
2. Credentials en clair
// ❌ JAMAIS
environment {
DB_PASSWORD = "mysecretpassword"
}
// ✅ Toujours via credentials
environment {
DB_PASSWORD = credentials('db-password')
}
3. Pas de cleanup
// ❌ Laisse des containers/volumes orphelins
stage('Test') {
steps {
sh 'docker-compose up -d'
sh 'npm test'
// Pas de cleanup!
}
}
// ✅ Toujours nettoyer
stage('Test') {
steps {
sh 'docker-compose up -d'
sh 'npm test'
}
post {
always {
sh 'docker-compose down -v --remove-orphans'
}
}
}
4. Builds sur le controller
# ❌ Builds sur le controller
jenkins:
numExecutors: 4
# ✅ Builds uniquement sur agents
jenkins:
numExecutors: 0
mode: EXCLUSIVE
Checklist
Pipeline:
□ Jenkinsfile dans le repo
□ Shared library pour code commun
□ Stages atomiques et clairs
□ Parallélisation où possible
□ Cache des dépendances
Sécurité:
□ Credentials via Jenkins Credentials
□ Pas de secrets en clair
□ Principe du moindre privilège
Maintenance:
□ Build discarder configuré
□ Cleanup des workspaces
□ Monitoring des agents
Notifications:
□ Notifications pertinentes (pas de spam)
□ Informations utiles dans les messages
□ Canaux appropriés selon la criticité