Construisez un pipeline CI-CD avec Jenkins
Jusqu'à présent, nous avons utilisé Maven pour construire notre application Spring Boot, Docker pour la containeriser dans un environnement isolé, SonarQube pour tester la qualité de notre code, et Nexus comme registre privé pour stocker et partager les images Docker. De plus, nous avons mis en place deux environnements, staging
et production
, pour déployer l'application avec Ansible.
Ces étapes sont nécessaires à chaque modification du code. Afin d'automatiser ce processus, nous allons mettre en œuvre un pipeline CI/CD avec Jenkins.
Installez Jenkins
Il existe plusieurs manière d'installer Jenkins, vous pouvez consulter la documentation officielle, dans ce cours je vais l'installer directement sur le serveur masterserver
.
Jenkins a besoin de Java pour fonctionner, installez openjdk 17
:
sudo apt-get update && apt-get -y install openjdk-17-jdk
mossaabfr@masterserver:~$ java --version
openjdk 17.0.9 2023-10-17
OpenJDK Runtime Environment (build 17.0.9+9-Ubuntu-122.04)
OpenJDK 64-Bit Server VM (build 17.0.9+9-Ubuntu-122.04, mixed mode, sharing)
Installez ensuite la version LTS de Jenkins :
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update
sudo apt-get install jenkins
Activez et démarrez le service Jenkins :
sudo systemctl enable jenkins
sudo systemctl start jenkins
Par défaut Jenkins est accessible par le port 8080
, pour moi c'est prod.local:8080
.
Lors de la première connexion à Jenkins, un mot de passe administrateur vous sera demandé. Vous pouvez le récupérer en consultant le contenu du fichier initialAdminPassword
:
cat /var/lib/jenkins/secrets/initialAdminPassword
Après avoir fourni le mot de passe, suivez les étapes d'initialisation suivantes :
- Choisissez l'option "install suggested plugins" pour installer les plugins recommandés.
- Créez un utilisateur administrateur lorsque cela vous est demandé.
- Enfin, configurez l'URL de votre serveur Jenkins. Dans mon cas, l'URL serait
http://prod.local:8080
.
Qu'est-ce qu'un pipeline Jenkins ?
Un pipeline dans Jenkins est un ensemble d'étapes organisées de manière séquentielle, que le serveur Jenkins exécute pour accomplir les différentes tâches nécessaires au processus CI/CD telles que la compilation du code source, les tests, l'analyse statique du code, la construction d'artefacts, le déploiement sur des environnements spécifiques, etc.
Jenkins propose deux méthodes pour définir des pipelines : la méthode script et la méthode déclarative. Dans ce cours, nous allons utiliser la méthode déclarative qui offre une approche plus structurée et lisible pour définir les pipelines. Elle permet aux utilisateurs de décrire le flux de travail global de manière déclarative, en spécifiant les étapes, les paramètres et les conditions d'exécution sans la nécessité d'écrire un script complet.
Structure de Base
La méthode déclarative permet de définir les étapes du pipeline en créant un fichier texte appelé Jenkinsfile
et en utilisant une syntaxe compatible avec Groovy
. Il commence généralement par une déclaration du pipeline, suivie d'une section "agent" spécifiant l'agent sur lequel les étapes seront exécutés. Voici un exemple de structure de base :
pipeline {
agent any
stages {
stage('Etape 1') {
steps {
// Actions spécifiques de l'étape 1
}
}
stage('Etape 2') {
steps {
// Actions spécifiques de l'étape 2
}
}
}
post {
// Actions à effectuer après l'exécution des étapes principales
}
}
Vous pouvez tester la création d'un pipeline avec un exemple classique 'Hello World' :
- Sur le tableau de bord, cliquez sur 'Nouvel item', choisissez un nom, par exemple, "demoPipeline", et dans le type, choisissez
Pipeline
.
- Ensuite, dans la section "Pipeline", choisissez l'exemple de 'Hello World'.
- Sauvegardez le pipeline et cliquez sur 'Lancer un build' pour exécuter le pipeline manuellement. Après le build, vous pouvez consulter l'historique. Jenkins attribue un numéro unique, s'auto-incrémentant à chaque nouveau build.
Pour notre application Spring Boot, nous allons inclure directement le fichier Jenkinsfile
avec le code source, ce qui est d'ailleurs une meilleure pratique. De plus, vous allez configurer Jenkins pour détecter automatiquement les modifications du code source et déclencher le build.
Versionnez votre code source
Nous allons utiliser Git et GitHub pour versionner le code source de notre petite application Spring Boot.
- Créez un fichier vide pour le moment nommé
Jenkinsfile
à la racine du projet - Poussez le projet sur GitHub :
git init
git add .
git commit -m "initial commit"
git remote add origin git@github.com:MossaabFrifita/devops-jenkins-ci-cd.git
git branch -M main
git push -u origin main
Créez un pipeline Jenkins
- Créez un nouveau pipeline pour notre application, choisissez le type
pipeline
et spécifiez le nom, par exempledevops-project-samples
.
- Dans la définition du pipeline, choisissez
Pipeline script from SCM
et entrez l'URL de votre dépôt GitHub.
- Ensuite, dans les 'branches à construire', ajoutez deux branches :
main
etdevelop
. À chaque modification du code source, nous allons déployer la branchemain
sur le serveur de production (prodserver
) et la branchedevelop
sur le serveur de test (testserver
).
- Enfin, sauvegardez le pipeline.
Créez la branche de développement (develop)
Vous allez créez la branche develop
à partir de votre branche principe main
pour commencer à écrire le pipeline :
git checkout -b develop
Définissez les étapes du pipeline
Les étapes du pipeline doivent être écrites dans le fichier Jenkinsfile
que vous avez créé. Si vous utilisez IntelliJ comme IDE, vous pouvez le configurer pour qu'il reconnaisse le type de fichier. Cela peut inclure la coloration syntaxique, l'auto-complétion et d'autres fonctionnalités pour faciliter l'écriture et la gestion du pipeline.
Dans l'onglet 'File '=> 'settings '=> 'editor '=> 'file type' => 'groovy ': ajoutez le pattern 'Jenkinsfile'
Définissez les variables d'environnement
Nous aurons besoin de définir des variables d'environnement spécifiques qui seront utilisées dans le pipeline :
BRANCHE_DEV
définit le nom de la branche de développement qui sera déployée sur letestserver
.BRANCHE_PROD
définit le nom de la branche de production qui sera déployée sur leprodserver
.NEXUS_DOCKER_REGISTRY
définit l'URL du registre Docker privé.DOCKER_IMAGE_NAME
définit le nom de l'image Docker à construire.DOCKER_IMAGE_TAG
définit le tag de l'image Docker.
Remarque : Jenkins expose des variables d'environnement accessibles via la variable globale
env
, telles queBUILD_ID
etJOB_NAME
. Vous pouvez en savoir plus dans la documentation officielle sur l'utilisation des variables d'environnement.
Ajoutez le bloc environment
à votre pipeline :
pipeline {
agent any
environment {
BRANCHE_DEV = 'origin/develop'
BRANCHE_PROD = 'origin/main'
NEXUS_DOCKER_REGISTRY = "http://prod.local:5003"
DOCKER_IMAGE_NAME = "api-demo"
DOCKER_IMAGE_TAG = "prod.local:5003"
}
}
Récupérez le code source
Ce stage sert à récupérer le code source du projet depuis le référentiel Git :
stage('Checkout') {
steps {
checkout scm
echo 'Pulling... ' + env.GIT_BRANCH
}
}
La variable environnement env.GIT_BRANCH
contient le nom de la branche à partir de laquelle le code est récupéré.
Exécutez les tests unitaires
Ce stage permet d'exécuter les tests unitaires avec Maven, il faut que Maven soit installé sur le serveur masterserver
Pour ce cours je l'ai installé directement depuis le terminal avec la commande sudo apt-get install maven
, vous pouvez aussi l'installer automatiquement via l'interface graphique de Jenkins, en accédant à 'Administrer Jenkins > Tools'
Si vous installez Maven depuis Jenkins, vous devrez spécifier la version de Maven que vous souhaitez utiliser dans votre pipeline dans la section Tools. Vous pouvez suivre ce lien dans la documentation officielle pour plus d'informations à ce sujet.
Ajoutez le stage des tests unitaires à votre pipeline :
stage('Tests') {
steps {
sh 'mvn test'
}
}
Testez la qualité de votre code avec SonarQube
A chaque exécution du pipeline, nous voulons nous assurer que le code respecte bien les règles de qualité définies dans SonarQube. Vous avez déjà testé l'analyse de code. Maintenant, dans votre pipeline, vous allez intégrer SonarQube.
Intégrez SonarQube avec Jenkins
- Installez le plugin
SonarQube Scanner
depuis 'Administrer Jenkins > Plugins' :
Comme indiqué dans la section de SonarQube, pour pouvoir envoyer le rapport d'analyse au serveur, il faut un token d'autorisation. Vous pouvez utiliser celui que vous avez généré, ou en générer un nouveau si vous ne l'avez pas noté.
- Dans 'Administrer Jenkins > Credentials,' cliquez sur 'Domains (global)' ensuite 'Add credentials'
- Ensuite, ajoutez un credentials de type
Secret text
et donnez-lui un ID, par exemplesonar-token
-
Pour pouvoir communiquer avec le serveur SonarQube dans le pipeline, vous devez le configurer dans 'Administrer Jenkins > System' :
-
Choisissez un nom de votre choix, par exemple
sonar-server
. -
Ajoutez l'URL du serveur. Dans mon cas, il est installé sur le
masterserver
et donc accessible viahttp://prod.local:9000
. -
Sélectionnez le token d'authentification que vous avez créé.
-
SonarQube Webhooks
Les webhooks de SonarQube permettent de notifier des services externes lorsque l'analyse d'un projet est terminée. Une requête HTTP POST, contenant un payload JSON, est envoyée à chaque URL configurée comme webhook. Cela permet à Jenkins de recevoir le résultat de l'analyse et de mettre fin au build si les règles de qualité du code ne sont pas respectées.
SonarQube Scanner pour Jenkins
Après avoir configuré le serveur SonarQube, le plugin offre deux fonctionnalités que vous allez utiliser dans votre pipeline :
withSonarQubeEnv
: Permet de configurer l'environnement du pipeline pour utiliser les paramètres de connexion au serveur SonarQube que vous avez configuré.waitForQualityGate
: Permet de mettre en pause l'exécution du pipeline jusqu'à ce que SonarQube termine l'analyse du projet. Elle écoute sur l'URLJenkins_instance_url/sonarqube-webhook
pour recevoir le résultat de l'analyse du code.
Du côté de SonarQube, pour le projet devops-project-samples
, vous allez configurer un webhook qui pointe vers /sonarqube-webhook
:
- Dans le serveur SonarQube, sélectionnez le projet
devops-project-samples
, puis allez dans 'Project Settings > Webhooks'
-
Cliquez sur 'Create' pour créer un nouveau Webhook :
- Entrez un nom, par exemple
JenkinsWebhooks
. - Entrez l'URL du serveur Jenkins. J'ai utilisé directement l'adresse IP du serveur
masterserver
au lieu du nom de domaineprod.local
, car le serveur SonarQube s'exécute dans un conteneur Docker et ne peut pas résoudre le nom de domaine que j'ai configuré manuellement sur le serveur, à moins que je ne le configure également. J'ai donc simplement utilisé l'adresse IP pour la démo.
- Entrez un nom, par exemple
-
Cliquez sur 'Create' pour sauvgarder.
Voici un exemple d'une requête POST envoyée au serveur Jenkins contenant les informations sur le résultat de l'analyse, y compris le statut
Il est maintenant temps d'ajouter le stage SonarQube dans votre pipeline :
stage('Sonarqube Analysis') {
steps {
script {
withSonarQubeEnv('sonar-server') {
sh "mvn sonar:sonar -Dintegration-tests.skip=true -Dmaven.test.failure.ignore=true"
}
timeout(time: 1, unit: 'MINUTES') {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "Pipeline aborted due to quality gate failure: ${qg.status}"
}
}
}
}
}
La fonction timeout
permet de définir une limite de temps pour l'exécution du bloc de code qui suit. Si le serveur SonarQube ne répond pas dans le délai spécifié, le pipeline ne reste pas bloqué dans la fonction waitForQualityGate()
et passe à l'étape suivante. Ensuite, si le statut retourné par la fonction waitForQualityGate()
n'est pas "OK", une erreur est déclenchée pour interrompre le pipeline.
Packagez le projet avec Maven
Dans ce stage, vous allez simplement packager l'application Spring Boot en un Jar avec Maven et archiver le Jar généré :
stage('Maven Build and Package') {
steps {
script {
sh 'mvn clean package -DskipTests'
}
}
post {
success {
archiveArtifacts 'target/*.jar'
}
}
}
Générez la version du projet à chaque build
Pour mieux gérer le versioning lors de la livraison continue, nous devons générer la version du projet à chaque exécution du pipeline.
Petit rappel : la version correspond à :
MAJEUR.MINEUR.CORRECTIF-ENV.NumeroBuild.CommitGit-Id
MAJEUR.MINEUR.CORRECTIF
: Cette partie sera récupérée à partir du fichier POM.xml.ENV
est soitprod
oudev
, en fonction de la branche GIT courante à partir de laquelle Jenkins récupère le code.NumeroBuild
peut être obtenu avecenv.BUILD_NUMBER
.- L'ID du commit le plus récent sur la branche actuelle peut être obtenu avec
git rev-parse HEAD
.
Pour lire le fichier pom.xml du projet, nous avons besoin de la fonction readMavenPom
du plugin Pipeline Utility Steps
. Elle retourne un objet de type Model
du package org.apache.maven.model.Model
(Documentation Maven Model).
En utilisant cet objet, nous pouvons manipuler diverses propriétés du projet Maven. Voici quelques-unes des méthodes disponibles :
- getVersion() : Retourne la version actuelle de l'artefact produit par le projet.
- setVersion(String version) : Permet de définir la version courante de l'artefact produit par le projet.
- getArtifactId() : Retourne l'identifiant de l'artefact.
- setArtifactId(String artifactId) : Permet de définir l'identifiant de l'artefact.
- getName() : Retourne le nom du projet.
- setName(String name) : Permet de définir le nom du projet.
-
Installez le plugin
Pipeline Utility Steps
-
Dans le fichier Jenkins et après la section
pipeline { }
, ajoutez la fonction suivante qui prend en paramètre le nom de l'environnement et retourne la version :
def getEnvVersion(envName) {
def pom = readMavenPom file: 'pom.xml'
// get the current development version
artifactVersion = "${pom.version}"
def gitCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
def versionNumber;
if (gitCommit == null) {
versionNumber =artifactVersion+"-${envName}."+env.BUILD_NUMBER;
} else {
versionNumber =artifactVersion+"-${envName}."+env.BUILD_NUMBER+'.'+gitCommit.take(8);
}
print 'build ${environnement} versions...'
print versionNumber
return versionNumber
}
Conteneurisez l'application et envoyez l'image Docker vers Nexus
Dans ce stage, vous allez créer une image docker de votre application Spring Boot, la tager avec la version et l'enregistrer dans le registre docker privé de Nexus
Pour pouvoir se connecter au registre Nexus, ajoutez un nouveau Credentials de type Nom d'utilisateur et mot de passe.
Donne lui un ID, par exemple nexus-credentials
Dans le bloc environnement
, ajoutez la variable suivante :
NEXUS_CREDENTIALS_ID = "nexus-credentials"
Ensuite, ajoutez le stage suivant :
stage('Docker Build and Push to Nexus') {
steps {
script {
envName = "dev"
if(env.GIT_BRANCH == BRANCHE_PROD) {
envName = "prod"
}
envVersion = getEnvVersion(envName)
withCredentials([usernamePassword(credentialsId: "${NEXUS_CREDENTIALS_ID}", usernameVariable: 'USER', passwordVariable: 'PASSWORD')]){
sh 'echo $PASSWORD | docker login -u $USER --password-stdin $NEXUS_DOCKER_REGISTRY'
sh 'docker system prune -af'
sh "docker build -t $DOCKER_IMAGE_TAG/$DOCKER_IMAGE_NAME:$envVersion --no-cache --pull ."
sh "docker push $DOCKER_IMAGE_TAG/$DOCKER_IMAGE_NAME:$envVersion"
}
}
}
}
La condition if
permet de déterminer l'environnement cible en fonction de la branche GIT.
envVersion
stocke la version obtenue en appelant la fonction getEnvVersion()
.
withCredentials([...]) { ... }
utilise les identifiants fournis pour se connecter au registre Docker.
Les instructions suivantes sont de simples commandes shell pour se connecter au registre, nettoyer les éléments inutilisés dans le système Docker comme les images précédemment construites, construire la nouvelle image Docker et la pousser vers le registre Nexus.
Finalement, pour pouvoir utiliser docker dans le pipeline jenkins, installez le plugin Docker Pipeline
Ensuite, ajoutez l'utilisateur Jenkins au groupe Docker
Le groupe Docker est un groupe d'utilisateurs sur un système Linux qui a le droit d'interagir avec le démon Docker. Lorsqu'un utilisateur est ajouté au groupe Docker, cela lui donne la permission d'exécuter des commandes Docker sans avoir à utiliser sudo
:
Dans le serveur masterserver
:
sudo usermod -aG docker jenkins
Redémarrez Jenkins :
sudo service jenkins restart
Déployez l'application avec Ansible
Pour déployer l'application, il suffit de se connecter au serveur Ansible nodemanager
et lancer le playbook deploy_playbook.yml
que vous avez créé.
Vous allez donc se connecter en SSH au nodemanager
.
Dans le masterserver
, générez une paire de clés SSH :
ssh-keygen -o -t rsa
Vous aurez donc la clé publique et privée :
mossaabfr@masterserver:~$ ls .ssh/
id_rsa id_rsa.pub
Utilisez ssh-copy-id
pour copier la clé publique sur le nodemanager
, puis testez que vous pouvez bien vous connecter en SSH :
mossaabfr@masterserver:~/.ssh$ ssh-copy-id -i id_rsa.pub user-ansible@192.168.1.173
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "id_rsa.pub"
The authenticity of host '192.168.1.173 (192.168.1.173)' can't be established.
ED25519 key fingerprint is SHA256:of8XJ1A+0vYWv+GUwrLlTLo+Gw3WIBmX3ejD5N32eqw.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
user-ansible@192.168.1.173's password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'user-ansible@192.168.1.173'"
and check to make sure that only the key(s) you wanted were added.
mossaabfr@masterserver:~/.ssh$ ssh user-ansible@192.168.1.173
Last login: Sat Jan 13 22:47:34 2024 from 192.168.1.196
[user-ansible@nodemanager ~]$
Installez le plugin SSH Agent Plugin pour pouvoir établir une connexion SSH au nodemanager
dans votre pipeline.
Dans 'Admintrer Jenkins > Credentials', ajoutez un nouveau credentials de type SSH Username with private key
Donnez-lui un ID, par exemple ansible-node-manager
, et copiez la clé privée en utilisant la commande cat id_rsa
dans le champ 'Private Key'
Pour tester la connexion SSH au nodemanager
avant de modifier votre pipeline, vous pouvez créer un autre pipeline :
pipeline {
agent any
stages {
stage('ssh cnx') {
steps {
sshagent(credentials: ['ansible-node-manager']) {
sh '[ -d ~/.ssh ] || mkdir ~/.ssh && chmod 0700 ~/.ssh'
sh 'ssh-keyscan -t rsa,dsa 192.168.1.173 >> ~/.ssh/known_hosts'
sh 'ssh user-ansible@192.168.1.173'
}
}
}
}
}
La commande ssh-keyscan
permet d'ajouter automatiquement le fingerprint
au fichier known_hosts
de l'utilisateur Jenkins afin de faciliter les connexions SSH ultérieures sans être invité à valider manuellement la clé chaque fois.
Exécutez le pipeline pour tester la connexion SSH :
Maintenant, vous allez ajouter deux étapes : une pour déployer l'application sur le prodserveur
uniquement si la branche actuelle est main
, et l'autre pour déployer l'application sur le testserver
uniquement si la branche actuelle est develop
.
Pour l'environnent de test :
stage('Ansible job staging') {
when {
expression { env.GIT_BRANCH == BRANCHE_DEV }
}
steps {
script {
def targetVersion = getEnvVersion("dev")
sshagent(credentials: ['ansible-node-manager']) {
sh "ssh user-ansible@192.168.1.173 'cd ansible-projects/devops-ansible-deployment && ansible-playbook -i 00_inventory.yml -l staging deploy_playbook.yml --vault-password-file ~/.passvault.txt -e \"docker_image_tag=${targetVersion}\"'"
}
}
}
}
Dans cette étape, après la connexion au nodemanager
, nous nous déplaçons dans le dossier ansible-projects/devops-ansible-deployment
, qui est l'emplacement de notre projet Ansible, et nous exécutons simplement le playbook deploy_playbook.yml
.
L'option -l
permet de spécifier le groupe d'hôtes cible. Dans cette étape, c'est le groupe staging
qui contient le serveur testserver
.
L'option -e
permet de passer la valeur de la variable docker_image_tag
. Par défaut dans Ansible, dans le fichier all.yml
, nous l'avons définie sur latest
. Dans le pipeline, il faut passer le tag de l'image Docker construite.
Pour l'environnement de production :
En général, effectuer un merge sur la branche principale "main" ou "master" correspond à une nouvelle version ou une release du logiciel. Dans le contexte du CI/CD, il est nécessaire de pousser un tag sur Git afin de marquer cette version spécifique. Cela permet de garder une trace claire des versions publiées et facilite le déploiement automatique ou la gestion des versions ultérieures.
En plus, en cas de problème ou de besoin de revenir à une version antérieure, le fait d'avoir des tags pour chaque version publiée facilite grandement le rollback. Vous pouvez simplement revenir au tag de la version précédente pour restaurer l'état du code à ce moment-là.
Pour pouvoir pousser le tag sur GitHub, une authentification est nécessaire. La manière la plus simple pour notre pipeline est d'établir une liaison SSH entre Jenkins et GitHub.
Dans Jenkins, comme pour Ansible, créez un Credentials de type SSH Username with private key
, en lui donnant un ID tel que github-credentials
, par exemple. Copiez ensuite la clé privée que vous avez déjà générée et utilisée pour Ansible
Dans GitHub, ajoutez la clé publique dans 'Settings > SSH and GPG keys'
Finalement, dans votre pipeline ajoutez le stage nécessaire pour déployer l'application sur la production :
stage('Ansible job production') {
when {
expression { env.GIT_BRANCH == BRANCHE_PROD }
}
steps {
script {
def targetVersion = getEnvVersion("prod")
sshagent(credentials: ['github-credentials']) {
sh "git tag -f v${targetVersion}"
sh "git push origin --tags HEAD:develop"
}
sshagent(credentials: ['ansible-node-manager']) {
sh "ssh user-ansible@192.168.1.173 'cd ansible-projects/devops-ansible-deployment && ansible-playbook -i 00_inventory.yml -l production deploy_playbook.yml --vault-password-file ~/.passvault.txt -e \"docker_image_tag=${targetVersion}\"'"
}
}
}
}
Pendant cette étape, si la branche actuelle est la branche de production, nous récupérons la version, créons un tag et le poussons vers GitHub. Ensuite, nous lançons le playbook Ansible sur le groupe production
, en spécifiant le tag de l'image Docker créée.
Voici le contenu du fichier Jenkins final pour notre pipeline :
pipeline {
agent any
environment {
BRANCHE_DEV = 'origin/develop'
BRANCHE_PROD = 'origin/main'
NEXUS_DOCKER_REGISTRY = "http://prod.local:5003"
NEXUS_CREDENTIALS_ID = "nexus-credentials"
DOCKER_IMAGE_NAME = "devops-project-samples"
DOCKER_IMAGE_TAG = "prod.local:5003"
}
stages {
stage('Checkout') {
steps {
checkout scm
echo 'Pulling... ' + env.GIT_BRANCH
}
}
stage('Tests') {
steps {
sh 'mvn test'
}
}
stage('Sonarqube Analysis') {
steps {
script {
withSonarQubeEnv('sonar-server') {
sh "mvn sonar:sonar -Dintegration-tests.skip=true -Dmaven.test.failure.ignore=true"
}
timeout(time: 1, unit: 'MINUTES') {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "Pipeline aborted due to quality gate failure: ${qg.status}"
}
}
}
}
}
stage('Maven Build and Package') {
steps {
script {
sh 'mvn clean package -DskipTests'
}
}
post {
success {
archiveArtifacts 'target/*.jar'
}
}
}
stage('Docker Build and Push to Nexus') {
steps {
script {
envName = "dev"
if(env.GIT_BRANCH == BRANCHE_PROD) {
envName = "prod"
}
envVersion = getEnvVersion(envName)
withCredentials([usernamePassword(credentialsId: "${NEXUS_CREDENTIALS_ID}", usernameVariable: 'USER', passwordVariable: 'PASSWORD')]){
sh 'echo $PASSWORD | docker login -u $USER --password-stdin $NEXUS_DOCKER_REGISTRY'
sh 'docker system prune -af'
sh "docker build -t $DOCKER_IMAGE_TAG/$DOCKER_IMAGE_NAME:$envVersion --no-cache --pull ."
sh "docker push $DOCKER_IMAGE_TAG/$DOCKER_IMAGE_NAME:$envVersion"
}
}
}
}
stage('Ansible job staging') {
when {
expression { env.GIT_BRANCH == BRANCHE_DEV }
}
steps {
script {
def targetVersion = getEnvVersion("dev")
sshagent(credentials: ['ansible-node-manager']) {
sh "ssh user-ansible@192.168.1.173 'cd ansible-projects/devops-ansible-deployment && ansible-playbook -i 00_inventory.yml -l staging deploy_playbook.yml --vault-password-file ~/.passvault.txt -e \"docker_image_tag=${targetVersion}\"'"
}
}
}
}
stage('Ansible job production') {
when {
expression { env.GIT_BRANCH == BRANCHE_PROD }
}
steps {
script {
def targetVersion = getEnvVersion("prod")
sshagent(credentials: ['github-credentials']) {
sh "git tag -f v${targetVersion}"
sh "git push origin --tags HEAD:develop"
}
sshagent(credentials: ['ansible-node-manager']) {
sh "ssh user-ansible@192.168.1.173 'cd ansible-projects/devops-ansible-deployment && ansible-playbook -i 00_inventory.yml -l production deploy_playbook.yml --vault-password-file ~/.passvault.txt -e \"docker_image_tag=${targetVersion}\"'"
}
}
}
}
}
}
def getEnvVersion(envName) {
def pom = readMavenPom file: 'pom.xml'
// get the current development version
artifactVersion = "${pom.version}"
def gitCommit = sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
def versionNumber;
if (gitCommit == null) {
versionNumber =artifactVersion+"-${envName}."+env.BUILD_NUMBER;
} else {
versionNumber =artifactVersion+"-${envName}."+env.BUILD_NUMBER+'.'+gitCommit.take(8);
}
print 'build ${environnement} versions...'
print versionNumber
return versionNumber
}
Pour que le pipeline se lance automatiquement, vous devez choisir un mécanisme de déclenchement. Vous allez configurer le pipeline Jenkins pour surveiller en continu les changements sur le dépôt GitHub. Lorsqu'un changement est détecté, Jenkins démarre automatiquement le pipeline.
Pour ce faire, sélectionnez le projet dans Jenkins, puis accédez à Configuration. Dans la section 'Déclencheurs de build', cochez 'Scrutation de l'outil de gestion de version' et saisissez * * * * *
. Cette configuration signifie que Jenkins vérifie chaque minute s'il y a de nouvelles modifications. Vous pouvez cliquer sur le point d'interrogation pour obtenir plus d'informations.
Après avoir sauvegardé la modification, vous remarquerez que Jenkins lance un premier build. Cependant, étant donné que le fichier Jenkinsfile est vide pour le moment, il n'exécutera aucune action.
Maintenant, effectuez un git commit
et un push
de la branche develop
. Ensuite, modifiez la version du projet dans le fichier pom.xml
, par exemple en la mettant à 1.1.1-RELEASE
.
Dans Jenkins, vous pouvez désormais constater que le pipeline a été automatiquement lancé. De plus, comme vous avez poussé la modification uniquement sur la branche develop
, le stage Ansible job production
ne sera pas déclenché.
Si vous n'avez pas autorisé l'utilisation de la méthode getVersion()
, elle pourrait nécessiter une approbation explicite dans Jenkins. En effet, si ces méthodes ne sont pas approuvées au préalable, des problèmes tels que org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException
[Pipeline] End of Pipeline
Also: org.jenkinsci.plugins.workflow.actions.ErrorAction$ErrorId: 7d88c948-32d7-4fa8-be0f-343fd56935b6
org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use method org.apache.maven.model.Model getVersion
at org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectMethod(StaticWhitelist.java:229)
Pour résoudre ce problème, vous pouvez autoriser l'utilisation de cette méthode dans Jenkins. Vous pouvez le faire en suivant ces étapes :
- Allez dans la configuration Jenkins ('Administrer Jenkins').
- Recherchez la section "In-process Script Approval".
- Vous devriez voir une liste d'approbations en attente. Recherchez l'approbation liée à la méthode
getVersion
de la classeorg.apache.maven.model.Model
et approuvez-la.
L'approbation de scripts dans Jenkins est une mesure de sécurité mise en place pour empêcher l'exécution involontaire ou malveillante de certaines méthodes ou classes Java dans les scripts Groovy utilisés dans Jenkins Pipelines. Cela permet de protéger l'environnement Jenkins contre l'exécution de scripts potentiellement dangereux.
Voici le résultat du lancement du pipeline :
Sur le serveur testserver
, vérifiez que l'application a été correctement déployée :
[root@testserver ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
519efeb67f07 prod.local:5003/devops-project-samples:1.1.1-RELEASE-dev.3.b712e91b "java -jar /opt/app/…" 2 hours ago Up 2 hours 0.0.0.0:7070->7070/tcp springbootapp
Vous pouvez également visualiser les images Docker téléchargées :
[root@testserver ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
prod.local:5003/devops-project-samples 1.1.1-RELEASE-dev.3.b712e91b 996e0754f64b Less than a second ago 309MB
prod.local:5003/devops-project-samples 0.0.1-SNAPSHOT-dev.2.79a67f5f 64091d71adc5 Less than a second ago 309MB
Maintenant, dans GitHub, créez un pull request pour merger la branche develop
dans main
. Cette fois-ci, Jenkins doit déployer l'application en production et pousser le tag sur GitHub.
Vérifiez également sur le prodserver
que l'application a été déployée :
[root@prodserver ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b881d0705103 prod.local:5003/devops-project-samples:1.1.1-RELEASE-prod.4.40e193ff "java -jar /opt/app/…" 2 hours ago Up 2 hours 0.0.0.0:7070->7070/tcp springbootapp
[root@prodserver ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
prod.local:5003/devops-project-samples 1.1.1-RELEASE-prod.4.40e193ff b5ce181d1c22 Less than a second ago 309MB
Sur GitHub, vous pouvez constater qu'un nouveau tag a été ajouté, faisant référence exactement à la version déployée en production.