Aller au contenu principal

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é

← Sécurité | Exercices →