Aller au contenu principal

Jenkinsfile - Pipeline as Code


Introduction

Un Jenkinsfile est un fichier texte qui définit un pipeline Jenkins. Il est versionné avec le code source.

Avantages

  • ✅ Versionné avec le code
  • ✅ Code review des pipelines
  • ✅ Historique des modifications
  • ✅ Réutilisable entre projets
  • ✅ Reproductible

Emplacement

my-project/
├── src/
├── tests/
├── Jenkinsfile # Racine du projet
├── package.json
└── README.md

Ou dans un sous-dossier :

my-project/
├── ci/
│ └── Jenkinsfile
└── src/

Jenkinsfile complet

#!/usr/bin/env groovy

pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:18-alpine
command: ['sleep', 'infinity']
- name: docker
image: docker:24-dind
securityContext:
privileged: true
'''
}
}

environment {
APP_NAME = 'my-application'
DOCKER_REGISTRY = 'registry.example.com'
DOCKER_CREDENTIALS = credentials('docker-registry-creds')
}

options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 30, unit: 'MINUTES')
timestamps()
disableConcurrentBuilds()
}

parameters {
choice(
name: 'ENVIRONMENT',
choices: ['dev', 'staging', 'production'],
description: 'Target environment'
)
booleanParam(
name: 'SKIP_TESTS',
defaultValue: false,
description: 'Skip tests'
)
}

stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.VERSION = "${BUILD_NUMBER}-${GIT_COMMIT_SHORT}"
}
}
}

stage('Install Dependencies') {
steps {
container('node') {
sh 'npm ci'
}
}
}

stage('Lint') {
steps {
container('node') {
sh 'npm run lint'
}
}
}

stage('Test') {
when {
expression { return !params.SKIP_TESTS }
}
parallel {
stage('Unit Tests') {
steps {
container('node') {
sh 'npm run test:unit'
}
}
post {
always {
junit 'coverage/junit.xml'
}
}
}
stage('Integration Tests') {
steps {
container('node') {
sh 'npm run test:integration'
}
}
}
}
}

stage('Build') {
steps {
container('node') {
sh 'npm run build'
}
}
}

stage('Build Docker Image') {
steps {
container('docker') {
sh """
docker build \
-t ${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} \
-t ${DOCKER_REGISTRY}/${APP_NAME}:latest \
.
"""
}
}
}

stage('Push Docker Image') {
when {
anyOf {
branch 'main'
branch 'develop'
}
}
steps {
container('docker') {
sh """
echo ${DOCKER_CREDENTIALS_PSW} | docker login \
-u ${DOCKER_CREDENTIALS_USR} \
--password-stdin ${DOCKER_REGISTRY}
docker push ${DOCKER_REGISTRY}/${APP_NAME}:${VERSION}
docker push ${DOCKER_REGISTRY}/${APP_NAME}:latest
"""
}
}
}

stage('Deploy') {
when {
branch 'main'
}
stages {
stage('Deploy to Staging') {
steps {
sh """
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} \
-n staging
"""
}
}

stage('Smoke Tests') {
steps {
sh 'curl -f https://staging.example.com/health'
}
}

stage('Deploy to Production') {
when {
expression { return params.ENVIRONMENT == 'production' }
}
input {
message 'Deploy to production?'
ok 'Deploy'
submitter 'admin,deployers'
}
steps {
sh """
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${VERSION} \
-n production
"""
}
}
}
}
}

post {
always {
cleanWs()
}
success {
slackSend(
color: 'good',
message: "✅ ${JOB_NAME} #${BUILD_NUMBER} succeeded\n${BUILD_URL}"
)
}
failure {
slackSend(
color: 'danger',
message: "❌ ${JOB_NAME} #${BUILD_NUMBER} failed\n${BUILD_URL}"
)
}
}
}

Credentials

Types de credentials

environment {
// Username/Password
DOCKER_CREDS = credentials('docker-credentials')
// Génère: DOCKER_CREDS_USR et DOCKER_CREDS_PSW

// Secret text
API_KEY = credentials('api-key')

// Secret file
KUBECONFIG = credentials('kubeconfig-file')

// SSH key
SSH_KEY = credentials('ssh-key')
}

Utilisation

stages {
stage('Deploy') {
steps {
// Username/Password
sh '''
echo ${DOCKER_CREDS_PSW} | docker login \
-u ${DOCKER_CREDS_USR} \
--password-stdin
'''

// Secret text
sh 'curl -H "Authorization: Bearer ${API_KEY}" https://api.example.com'

// Secret file
sh 'kubectl --kubeconfig=${KUBECONFIG} apply -f k8s/'

// SSH
sshagent(['ssh-key']) {
sh 'ssh user@server "deploy.sh"'
}
}
}
}

withCredentials

steps {
withCredentials([
usernamePassword(
credentialsId: 'github-creds',
usernameVariable: 'GIT_USER',
passwordVariable: 'GIT_TOKEN'
),
string(
credentialsId: 'sonar-token',
variable: 'SONAR_TOKEN'
),
file(
credentialsId: 'gcp-key',
variable: 'GOOGLE_APPLICATION_CREDENTIALS'
)
]) {
sh '''
git clone https://${GIT_USER}:${GIT_TOKEN}@github.com/org/repo.git
sonar-scanner -Dsonar.login=${SONAR_TOKEN}
gcloud auth activate-service-account --key-file=${GOOGLE_APPLICATION_CREDENTIALS}
'''
}
}

Shared Libraries

Structure

jenkins-shared-library/
├── vars/
│ ├── buildApp.groovy
│ ├── deployToK8s.groovy
│ └── notifySlack.groovy
├── src/
│ └── com/
│ └── example/
│ └── Utils.groovy
└── resources/
└── templates/
└── deployment.yaml

vars/buildApp.groovy

def call(Map config = [:]) {
def appName = config.appName ?: 'app'
def nodeVersion = config.nodeVersion ?: '18'

pipeline {
agent {
docker { image "node:${nodeVersion}" }
}

stages {
stage('Install') {
steps {
sh 'npm ci'
}
}

stage('Test') {
steps {
sh 'npm test'
}
}

stage('Build') {
steps {
sh 'npm run build'
}
}
}
}
}

vars/deployToK8s.groovy

def call(String namespace, String image) {
sh """
kubectl set image deployment/app app=${image} -n ${namespace}
kubectl rollout status deployment/app -n ${namespace}
"""
}

vars/notifySlack.groovy

def call(String status, String channel = '#builds') {
def color = status == 'SUCCESS' ? 'good' : 'danger'
def emoji = status == 'SUCCESS' ? '✅' : '❌'

slackSend(
channel: channel,
color: color,
message: "${emoji} ${env.JOB_NAME} #${env.BUILD_NUMBER} - ${status}\n${env.BUILD_URL}"
)
}

Utilisation

// Jenkinsfile
@Library('jenkins-shared-library') _

// Utiliser le pipeline complet
buildApp(appName: 'my-app', nodeVersion: '20')

// Ou utiliser des fonctions individuelles
pipeline {
agent any
stages {
stage('Deploy') {
steps {
deployToK8s('production', 'my-app:1.0.0')
}
}
}
post {
always {
notifySlack(currentBuild.result)
}
}
}

Configurer la library

Dans Jenkins > Manage Jenkins > System Configuration > Global Pipeline Libraries :

Name: jenkins-shared-library
Default version: main
Retrieval method: Modern SCM
- Git
- Project Repository: https://github.com/org/jenkins-shared-library.git

Snippets utiles

Checkout avec options

checkout([
$class: 'GitSCM',
branches: [[name: '*/main']],
extensions: [
[$class: 'CloneOption', depth: 1, shallow: true],
[$class: 'CleanBeforeCheckout'],
[$class: 'SubmoduleOption', recursiveSubmodules: true]
],
userRemoteConfigs: [[
url: 'https://github.com/org/repo.git',
credentialsId: 'github-creds'
]]
])

Stash/Unstash

stages {
stage('Build') {
steps {
sh 'npm run build'
stash includes: 'dist/**', name: 'build-output'
}
}

stage('Deploy') {
agent { label 'deploy-agent' }
steps {
unstash 'build-output'
sh 'rsync -av dist/ server:/var/www/'
}
}
}

Artifacts

post {
success {
archiveArtifacts(
artifacts: 'target/*.jar, target/*.war',
fingerprint: true,
onlyIfSuccessful: true
)
}
}

Input avec timeout

stage('Approval') {
steps {
timeout(time: 1, unit: 'HOURS') {
input(
message: 'Deploy to production?',
ok: 'Deploy',
submitter: 'admin,deployers',
parameters: [
choice(name: 'VERSION', choices: ['v1.0', 'v1.1', 'v2.0'])
]
)
}
}
}

Multibranch Pipeline

Configuration

  1. New Item > Multibranch Pipeline
  2. Branch Sources > GitHub
  3. Behaviors:
    • Discover branches
    • Discover pull requests

Jenkinsfile pour Multibranch

pipeline {
agent any

stages {
stage('Build') {
steps {
sh 'npm ci && npm run build'
}
}

stage('Test') {
steps {
sh 'npm test'
}
}

stage('Deploy to Dev') {
when {
branch 'develop'
}
steps {
sh 'kubectl apply -f k8s/dev/'
}
}

stage('Deploy to Staging') {
when {
branch 'main'
}
steps {
sh 'kubectl apply -f k8s/staging/'
}
}

stage('Deploy to Prod') {
when {
tag 'v*'
}
steps {
input 'Deploy to production?'
sh 'kubectl apply -f k8s/prod/'
}
}
}
}

Résumé

ConceptDescription
JenkinsfilePipeline versionné
credentials()Accès aux secrets
Shared LibraryCode réutilisable
MultibranchPipeline par branche
stash/unstashPartage de fichiers

← Pipelines | Plugins →