Continuous Delivery mit Jenkins und AEM

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

  1. Hochzählen der Projekt/Maven/Pom Version im Master-Branch
  2. Erstellen eines GIT-Tags mit der neuen Version
  3. Push dieses Tags
  4. Hochladen des gebauten Artifakts in ein Nexus Repsitory
  5. Neue Version auch im Child/Develp-Branch eintragen
  6. Gebautes Projekt auf die AEM Instanzen deployen

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

  • Das Error-Handling existiert praktisch nicht
  • Wir benötigen einen speraten Jenkins-Trigger-Job, der unsere Pipeline aufruft. Ansonsten würde sich die Pipeline bei jedem commit selbst aufrufen. Man müsste so etwas wie "ci-skip" implementieren, was aber zum derzeitigen Zeitpunkt noch fehlerhaft ist.
  • Man muss die Anmeldeninformationen und Maven-Settings in Jenkins konfigurieren. Wünschenswert wäre, wenn alle Konfigurationen im Skript wären, so dass am Jenkins-Server nichts verändert werden muss