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
- New Item > Multibranch Pipeline
- Branch Sources > GitHub
- 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é
| Concept | Description |
|---|---|
| Jenkinsfile | Pipeline versionné |
| credentials() | Accès aux secrets |
| Shared Library | Code réutilisable |
| Multibranch | Pipeline par branche |
| stash/unstash | Partage de fichiers |