Step-by-Step Erklärung
Code Snippet #1
Step by Step erklärt
Einführung
Wir kennen alle CI - Continuous Integration. Und die nächste Stufe ist: CD - Continuous Delivery. Eine Kombination von vielen Schritten um den Build-Prozess, Automatische Tests, Release-Prozess und den Deploy-Prozess zu automatisieren.
Für die Umsetzung von CD gibt es gibt mehrere Möglichkeiten. Neben Jenkins (um den es in diesem Beitrag gehen soll) zum Beispiel auch GitLab Integration: https://stackoverflow.com/questions/37429453/gitlab-ci-vs-jenkins
Um mit Jenkins Continuous Delivery umzusetzen, verwenden wir eine sogenannte Pipeline (https://jenkins.io/doc/book/pipeline/). Allerdings sollte man beachten, dass man nicht ganz ohne Jenkins-Plugins auskommt und man schnell in der gefürchteten "Jenkins Plugin Hell" landen kann. Deswegen ist es Ratsam, sich Alteranativen wie z.B. GitLab Integration oder auch Bamboo anzuschauen und zu evaluieren https://de.atlassian.com/software/bamboo
Grundlagen
Wir möchten den CD-Prozess als Teil des Codes begreifen und nicht als Administrative Aufgabe. Deswegen werden die Pipeline-Skripte in unser Code-Repository commitet.
Somit haben wir den Vorteil, dass wir nicht die Jenkins-Job-Konfiguration anpassen müssen, wenn wir am Prozess etwas ändern. Außerdem können wir (theoretisch) unseren CD-Prozess einfach auf einen anderen Server übertragen, bzw. auch lokale Server damit bespielen.
Im Fall von Jenkins-Pipelines müssen wir ein Jenkinsfile erstellen. Diese kann in der Jenkins-Pipeline Skriptsprache, in Groovy oder in einem Mix aus beiden geschrieben sein.
Voraussetzungen
Ziele
Das ganze Jenkins-Pipeline Groovy-Skript
#!groovy
node {
def version
def webAppTarget = "xxx"
def sourceBranch = "develop"
def releaseBranch = "quality-assurance"
def nexusBaseRepoUrl = "http://xxx"
def repositoryUrl = "http://xxx"
def gitCredentialsId = "xxx"
def nexusRepositoryId = "xxx"
def configFileId = "xxx"
def mvnHome = tool 'M3'
def updateQAVersion = {
def split = version.split('\\.')
//always remove "-SNAPSHOT"
split[2] = split[2].split('-SNAPSHOT')[0]
//increment the middle number of version by 1
split[1] = Integer.parseInt(split[1]) + 1
//reset the last number to 0
split[2] = 0
version = split.join('.')
}
//FIXME: use SSH-Agent
//FIXME: use SSH-Agent
sh "git config --replace-all credential.helper cache"
sh "git config --global --replace-all user.email gituser@xxx.de; git config --global --replace-all user.name gituser"
configFileProvider([configFile(fileId: "${configFileId}", variable: "MAVEN_SETTINGS")]) {
stage('Clean') {
deleteDir()
}
dir('qa') {
stage('Checkout QA') {
echo 'Load from GIT'
git url: "${repositoryUrl}", credentialsId: "${gitCredentialsId}", branch: "${releaseBranch}"
}
stage('Increment QA version') {
version = sh(returnStdout: true, script: "${mvnHome}/bin/mvn -q -N org.codehaus.mojo:exec-maven-plugin:1.3.1:exec -Dexec.executable='echo' -Dexec.args='\${project.version}'").toString().trim()
echo 'Old Version:'
echo version
updateQAVersion()
echo 'New Version:'
echo version
}
stage('Set new QA version') {
echo 'Clean Maven'
sh "${mvnHome}/bin/mvn -B clean -s '$MAVEN_SETTINGS'"
echo 'Set new version'
sh "${mvnHome}/bin/mvn -B versions:set -DnewVersion=${version}"
}
stage('QA Build') {
echo 'Execute maven build'
sh "${mvnHome}/bin/mvn -B install -s '$MAVEN_SETTINGS'"
}
stage('Push new QA version') {
echo 'Commit and push branch'
sh "git commit -am \"New release candidate ${version}\""
sh "git push origin ${releaseBranch}"
}
stage('Push new tag') {
echo 'Tag and push'
sh "git tag -a ${version} -m 'release tag'"
sh "git push origin ${version}"
}
stage('QA artifact deploy') {
echo 'Deploy artifact to Nexus repository'
try {
sh "${mvnHome}/bin/mvn deploy:deploy-file -DpomFile=pom.xml -DrepositoryId=${nexusRepositoryId} -Durl=${nexusBaseRepoUrl} -Dfile=${webAppTarget}/target/${webAppTarget}-${version}.zip -Dpackaging=zip -s '$MAVEN_SETTINGS'"
} catch (ex) {
println("Artifact could not be deployed to the nexus!")
println(ex.getMessage())
}
}
stage('Deploy AEM Author') {
echo 'deploy on author'
withCredentials([usernamePassword(credentialsId: '6a613b0f-631b-453a-9f34-6a69e8676877', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
sh "curl -u ${USERNAME}:${PASSWORD} -F file=@\"${webAppTarget}/target/${webAppTarget}-${version}.zip\" -F force=true -F install=true http://doom.eggs.local:64592/crx/packmgr/service.jsp"
}
}
stage('Deploy AEM Publish') {
echo 'deploy on publish'
withCredentials([usernamePassword(credentialsId: '3a25eefc-d446-4793-a621-9f15e4774126', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
sh "curl -u ${USERNAME}:${PASSWORD} -F file=@\"${webAppTarget}/target/${webAppTarget}-${version}.zip\" -F force=true -F install=true http://doom.eggs.local:64594/crx/packmgr/service.jsp"
}
}
}
dir('develop') {
stage('Checkout develop') {
echo 'Load from GIT'
git url: "${repositoryUrl}", credentialsId: "${gitCredentialsId}", branch: "${sourceBranch}"
}
stage('Set new develop version') {
echo 'Clean Maven'
sh "${mvnHome}/bin/mvn -B clean -s '$MAVEN_SETTINGS'"
echo 'Set new version'
sh "${mvnHome}/bin/mvn -B versions:set -DnewVersion=${version}-SNAPSHOT"
}
stage('Develop Build') {
echo 'Execute maven build'
sh "${mvnHome}/bin/mvn -B install -s '$MAVEN_SETTINGS'"
}
stage('Push new develop version') {
echo 'Commit and push branch'
sh "git commit -am \"New QA release candidate ${version}\""
sh "git push origin ${sourceBranch}"
}
}
}
}
Step-by-Step Erklärung
Code Snippet #1
#!groovy
Das Pipeline Skript ist praktisch ein Groovy Skript, weswegen es auch als Groovy Skript annotiert wird.
Code Snippet #2
node {
Mit "node" deklarieren wir das Skript als Scripted Pipeline (Scripted vs. Declarative). Zusammengefasst kann man sagen, Scripted Pipelines ermöglichen mehr Flexibilität, da man alle Möglichkeiten von Groovy zur Verfügung hat.
Code Snippet #3
def version
def webAppTarget = "xxx"
def sourceBranch = "develop"
def releaseBranch = "quality-assurance"
def nexusBaseRepoUrl = "http://xxx"
def repositoryUrl = "http://xxx"
def gitCredentialsId = "xxx"
def nexusRepositoryId = "xxx"
def configFileId = "xxx"
def mvnHome = tool 'M3'
def updateQAVersion = {
def split = version.split('\\.')
//always remove "-SNAPSHOT"
split[2] = split[2].split('-SNAPSHOT')[0]
//increment the middle number of version by 1
split[1] = Integer.parseInt(split[1]) + 1
//reset the last number to 0
split[2] = 0
version = split.join('.')
}
Einige Variablen wie z.B. die Branchnamen und die Credential-Ids sind für den GIT Zugang da. Die Funktion "updateQAVersion" entfernt den Versions-Zusatz "-SNAPSHOT" und erhöht die mittlere Versionsnummer (2.1.12-SNAPSHOT → 2.2.0)
Code Snippet #4
//FIXME: use SSH-Agent
sh "git config --replace-all credential.helper cache"
sh "git config --global --replace-all user.email gituser@xxx.de; git config --global --replace-all user.name gituser"
configFileProvider([configFile(fileId: "${configFileId}", variable: "MAVEN_SETTINGS")]) {
stage('Clean') {
deleteDir()
}
dir('qa') {
stage('Checkout QA') {
echo 'Load from GIT'
git url: "${repositoryUrl}", credentialsId: "${gitCredentialsId}", branch: "${releaseBranch}"
}
Hier werden die GIT-Credentials mithilfe des "credential.helper" gesetzt und die Maven-Settings werden importiert.
Danach wird das aktuelle Verzeichnis gelöscht um auf einen sauberen Stand weiter zu arbeiten.
Im aktuellen Verzeichnis wird ein Verzeichnis "qa" angelegt und in diesem Verzeichnis wird der Sourcecode mit GIT ausgecheckt.
Wir legen ein extra Verzeichnis an, da wir später einen weitern branch auschecken.
Code Snippet #5
stage('Increment QA version') {
version = sh(returnStdout: true, script: "${mvnHome}/bin/mvn -q -N org.codehaus.mojo:exec-maven-plugin:1.3.1:exec -Dexec.executable='echo' -Dexec.args='\${project.version}'").toString().trim()
echo 'Old Version:'
echo version
updateQAVersion()
echo 'New Version:'
echo version
}
stage('Set new QA version') {
echo 'Clean Maven'
sh "${mvnHome}/bin/mvn -B clean -s '$MAVEN_SETTINGS'"
echo 'Set new version'
sh "${mvnHome}/bin/mvn -B versions:set -DnewVersion=${version}"
}
stage('QA Build') {
echo 'Execute maven build'
sh "${mvnHome}/bin/mvn -B install -s '$MAVEN_SETTINGS'"
}
Jetzt nutzen wir das exec-maven-plugin um die Versionsnummer aus der pom.xml auszulesen. Mit "returnStdout" wird der Terminal Output in die Variable "version" geschrieben.
Leider konnte ich zum Auslesen der Versionsnummer keine schönere Lösung finden.
Nachdem wir die aktuelle Versionsnummer haben, geben wir diese weiter an die Methode "updateQAVersion" um die nächst höhere Versionsnummer zu bekommen.
Diese setzen wir dann mit dem Maven Goal "versions:set" in das aktuell ausgecheckte Projekt. Dann bauen wir dieses um das fertige Package zu bekommen.
Code Snippet #6
stage('Push new QA version') {
echo 'Commit and push branch'
sh "git commit -am \"New release candidate ${version}\""
sh "git push origin ${releaseBranch}"
}
stage('Push new tag') {
echo 'Tag and push'
sh "git tag -a ${version} -m 'release tag'"
sh "git push origin ${version}"
}
Die nächsten beiden Stages werden genutzt um das gerade geänderte Projekt mit GIT zu pushen. Dies funktioniert nur, da wir im Code Snippet #4 den "credential.helper cache" gesetzt haben.
Eine Alternative, die auch zu bevorzugen ist, ist das ganze mit SSH zu machen: https://jenkins.io/doc/pipeline/examples/#push-git-repo
Code Snippet #7
stage('QA artifact deploy') {
echo 'Deploy artifact to Nexus repository'
try {
sh "${mvnHome}/bin/mvn deploy:deploy-file -DpomFile=pom.xml -DrepositoryId=${nexusRepositoryId} -Durl=${nexusBaseRepoUrl} -Dfile=${webAppTarget}/target/${webAppTarget}-${version}.zip -Dpackaging=zip -s '$MAVEN_SETTINGS'"
} catch (ex) {
println("Artifact could not be deployed to the nexus!")
println(ex.getMessage())
}
}
Diese Stage deployed das erstellte Artifakt (eine zip-Datei) in ein Nexus Repository mit der Hilfe des Maven Commandline-Befehls deploy:deploy-file. Hier benötigen wir den "configFileProvider", den wir zuvor gesetzt haben um die Maven-Properties in der settings.xml zur Verfügung zu haben (siehe Variable $MAVEN_SETTINGS)
Code Snippet #8
stage('Deploy AEM Author') {
echo 'deploy on author'
withCredentials([usernamePassword(credentialsId: 'xxx', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
sh "curl -u ${USERNAME}:${PASSWORD} -F file=@\"${webAppTarget}/target/${webAppTarget}-${version}.zip\" -F force=true -F install=true http://xxx/crx/packmgr/service.jsp"
}
}
stage('Deploy AEM Publish') {
echo 'deploy on publish'
withCredentials([usernamePassword(credentialsId: 'xxx', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
sh "curl -u ${USERNAME}:${PASSWORD} -F file=@\"${webAppTarget}/target/${webAppTarget}-${version}.zip\" -F force=true -F install=true http://xxx/crx/packmgr/service.jsp"
}
}
Danach deployen wir das zusammengebaute Projekt auf unsere AEM-Server mithilfe eines SH-Befehls. Die Anmeldeinformationen kommen wieder aus dem Jenkins Credentials-Plugin.
Code Snippet #9
dir('develop') {
stage('Checkout develop') {
echo 'Load from GIT'
git url: "${repositoryUrl}", credentialsId: "${gitCredentialsId}", branch: "${sourceBranch}"
}
stage('Set new develop version') {
echo 'Clean Maven'
sh "${mvnHome}/bin/mvn -B clean -s '$MAVEN_SETTINGS'"
echo 'Set new version'
sh "${mvnHome}/bin/mvn -B versions:set -DnewVersion=${version}-SNAPSHOT"
}
stage('Develop Build') {
echo 'Execute maven build'
sh "${mvnHome}/bin/mvn -B install -s '$MAVEN_SETTINGS'"
}
stage('Push new develop version') {
echo 'Commit and push branch'
sh "git commit -am \"New QA release candidate ${version}\""
sh "git push origin ${sourceBranch}"
}
}
Und weil wir die Version im Release/Ziel-Branch erhöht haben, müssen wir nun auch die Version im Develop/Source-Branch erhöhen. Dazu erstellen wir einen neuen Ordner "develop" und checken den Develop/Source-Branch aus.
Dann nehmen wir die neue Versionsnummer und hängen "-SNAPSHOT" hinten dran und setzen sie wie zuvor schon mithilfe des Maven Goals "versions:set".
Zusammenfassung
Wir haben nun einen funktionierenden Release-Prozess. Vom erhöhen der Versionsnummer, über GIT-Tagging, Nexus deployment und dem deployen auf unsere AEM-Server. Sogar in unserem Develop-Branch wird die Version erhöht. Und das alles in einem voll flexiblen Skript, bei dem man alle Änderungen im Code-Repository History nachverfolgen kann.
Das spart den Entwicklern/Dev-Ops jede Menge Arbeit und beschleunigt den Release-Prozess.
Aber natürlich ist der Prozess noch nicht perfekt