Warum arbeiten wir mit Jenkins Pipeline? Rund 50 Entwickler arbeiten bei Ackee daran, Apps für unsere anspruchsvollen Kunden zu entwickeln, zu testen und auszuliefern. Dafür nutzen wir verschiedene Methoden. Bei mehreren Git Pushes und Merge Requests pro Stunde benötigen wir einen schnellen und optimierten Prozess. Die CI/CD mittels vielfältiger Methoden und Clouds bzw. anderenEinsatzzielenzu automatisieren ist sehr wichtig für uns. Deshalb nutzen wir die leistungsstarke Jenkins Pipeline mit der Shared Pipeline Libraryzusammenmit Jenkins und Gitlab.

Unser Backendbasierthauptsächlich auf Kubernetes Cluster sowie auf NodeJS, MongoDB, MySQL und einigen PHP Containern. Das Frontend mit React , Middleman und weiteren Methoden muss schnell getestet und auf den entsprechenden Webservern bereitgestellt werden. Das kann Google Storage Buckets oder FTP Webhosting sein – je nachdem wie der Kunde es haben will.

Auch das Android- und das iOS-Team benötigen eine schnelle und zuverlässigeCI/CD. Generell brauchen alle Teams einen Weg um Merge Requestsaufzubauenund zu testen. Zudem gibt es ständig den Bedarf, etwas an der Infrastruktur zu automatisieren. Deshalb hat für uns diegeskriptete Automatisierungsplattformfür das DevOps-Team eine hohe Priorität.

Jenkins Pipeline ist die bessere Alternative

Vor einiger Zeit haben wir Jenkins Freestyle Jobs genutzt, um ein Repository auszuprobieren. Wir haben es mit einfachenBash Snippets indieKonsoleeingebaut und es anderen Servern mittels SSH, Rsync, etc. zur Verfügung gestellt.

Dann kam Jenkins Pipeline. Jenkins Pipeline ist eine Sammlung von Plugins, die das Einbinden und Integrieren von ständig aktiven Liefer-Pipelines zu Jenkins unterstützen. Also eine Alternative zu den alten Freestyle Jobs.
Mit Jenkins Pipeline muss man nur noch eine Pipeline oder einen Multibranch Pipeline Job erstellen unddie Pipeline als Code definieren. Der Code kann Teil der Job Definition oder auch direkt als Jenkins File im Projekt Repository enthalten sein.

Beispiel Jenkinsfile:

#!groovy node('nodejs') { currentBuild.result = "SUCCESS" try { stage('Checkout'){ checkout scm } stage('Build'){ sh 'npm install' } stage('Test'){ env.NODE_ENV = "test" sh 'npm run ci-test' } stage('Deploy'){ sh 'docker build -t myapp . && docker run -d myapp' } stage('Cleanup'){ sh 'npm prune' sh 'rm node_modules -rf' slackNotify channel: '#ci-nodejs', message: 'pipeline successful: myapp' } } catch (err) { currentBuild.result = "FAILURE" slackNotify channel: '#ci-nodejs', message: 'pipeline failed: myapp' throw err } }

Was Jenkins Pipeline leistet

Die Jenkins Pipeline hat verschiedeneLevels, die dank des Visualizer Plugins sichtbar sind. Was die Jenkins Pipeline kann, ist Folgendes:

– Sie baut die Node App mitNPM(“sh” bedeutetShell Call),
– Sie testet das Ganze
– Sie baut einDocker Imageund verwaltet es
– Sie räumt denWorkspaceauf und
– sie benachrichtigt Slack über das Pipeline Ergebnis.

slackNotify ist einSlack Plugin Call. Das bedeutet, dass Plugins die Syntax der Jenkins Pipeline unterstützen müssen, um als hilfreiche Funktion mit einer Auswahl von Parametern zu gelten.

jenkins pipeline
Jenkins pipeline – stages visualizer

Jenkins Pipeline ist überaus leistungsstark. Sie macht mehr oder weniger alles, was man von ihr will. Da sie die Groovy Sprache nutzt, kann man Java Libraries in der Pipeline verwenden.

Jenkins Shared Pipeline Library

Da wir an vielen Projekten gleichzeitig arbeiten, hätten wir so eine Menge ähnlichen Codes oder das exakt gleiche Jenkinsfile in allen NodeJS und andere Repositorys.

Man stelle sich vor, dass eine einfache Änderung in derslackNotify Funktion Call Syntaxoder einer anderenCIi Client Syntaxbedeuten würde, dass wir den Pipeline Code in allen Repositorys undBranchesändern müssen!
Das ist übrigens neulich passiert beiGCloud cli API Client Syntax. Die alte Syntax wurde abgelehnt und hatGCloud Docker Push zuGCloud Docker — push geändert. In unserem Fall wollen wir die Pipeline Logik nicht im Projekt Repository vorfinden. Das einzige, was wir dort haben wollen, ist die Pipeline Konfiguration. Das kann man ganz einfach erreichen, in dem manJenkins Shared Pipeline Library verwendet. Das ist ein Groovy Git Repository mit der folgendenOrdnerStruktur. 

(root) +- src # Groovy source files | +- org | +- foo | +- Bar.groovy # for org.foo.Bar class +- vars | +- foo.groovy # for global 'foo' variable | +- foo.txt # help for 'foo' variable +- resources # resource files (external libraries only) | +- org | +- foo | +- bar.json # static helper data for org.foo.Bar

Dieses shared Pipeline Repository wird jedes Mal überprüft, wenn ein Pipeline Job startet. Man kann es in sein Jenkinsfile einfügen und dort nutzen. Man kann sogar Branches definieren.

@Library('[email protected]') _

Oder man lädt esimplizit. Das kann man in den Jenkins Konfigurationseinstellungen festlegen.

Konfiguration von Jenkinsfile

Wenn man die shared Pipeline Library nutzt, kann das Jenkinsfile im Projekt Repository folgendermaßen aussehen:

PipelineNodejs{ // MODIFY projectName = 'node-template' slackChannel = '#ci-nodejs' appName = 'api' // microservice name, unique in project cloudProject = [development: 'kube-dev-cluster', master: 'kube-prod-cluster'] buildCommand = 'npm install && npm run postinstall' // MODIFY ONLY IF YOU KNOW WHAT YOU ARE DOING nodeImage = 'node:5.12.0' nodeEnv = "-e NODE_PATH=./app:./config" nodeTestEnv = '-e NODE_ENV=test -e NODE_PATH=./app:./config' namespace = [development: "${projectName}-development", master: "${projectName}-master"] }

Beispiel für eine Jenkins Pipeline

The Jenkinsfile im vorhergehenden Abschnitt ruft eine Funktion PipelineNodeJS auf. Das ist eine globale Funktion, die in der Shared Pipeline Library in vars/PipelineNodeJS.groovy definiert ist.

def call(body) { // evaluate the body block, and collect configuration into the object def config = [:] body.resolveStrategy = Closure.DELEGATE_FIRST body.delegate = config body() properties([disableConcurrentBuilds()]) def agent = config.agent ?: 'nodejs' node(agent) { def workspace = pwd() ... code omitted for brevity ... stage('Test') { if (config.runTests){ docker.image(nodeImage).inside(nodeTestEnv) { sh "npm run ci-test" } echo "npm run ci-test finished. currentBuild.result=${currentBuild.result}" ... code omitted for brevity ... if (currentBuild.result == 'UNSTABLE') { throw new RuntimeException("Tests failed") } } else { echo 'Tests skipped' } ... code omitted for brevity ...

FürAndroid,iOS,React,middleman, etc. gibt es auch andere Pipelines.

Merge Request Builder

Für den Merge Request Flow mit Gitlab skripten wir einen Jenkins Pipeline Job, der das Gitlab Integration Plugin nutzt.
Hier kommt das Merge Request Builder Job Beispiel für iOS:

env.CHANGELOG_PATH = "outputs/changelog.txt" env.SLACK_CHANNEL = "ci-merge-requests" env.FASTLANE_SKIP_UPDATE_CHECK = 1 env.FASTLANE_DISABLE_COLORS = 1 env.CHANGELOG = "" node('ios') { try { gitlabBuilds(builds: ["carthage", "pods", "test", "build ipa"]) { def gemfileExists = fileExists 'Gemfile' fastlane = "fastlane" if (gemfileExists) { fastlane = "bundle exec fastlane" } stage('Checkout') { println env.dump() withCredentials([string(credentialsId: 'jenkins-gitlab-credentials', variable: 'credentials')]) { checkout changelog: true, poll: true, scm: [$class: 'GitSCM', branches: [[name: "origin/${env.gitlabSourceBranch}"]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'WipeWorkspace'], [$class: 'PreBuildMerge', options: [fastForwardMode: 'FF', mergeRemote: 'origin', mergeStrategy: 'default', mergeTarget: "${env.gitlabTargetBranch}"] ] ], submoduleCfg: [], userRemoteConfigs: [[name: 'origin', credentialsId: credentials, url: env.gitlabSourceRepoSshUrl ]]] } } stage('Prepare') { sh("security unlock -p ${MACHINE_PASSWORD} ~/Library/Keychains/login.keychain") if (gemfileExists) { sh "bundle install --path ~/.bundle" } } gitlabCommitStatus("carthage") { stage('Carthage') { sh(fastlane + " cart") } } gitlabCommitStatus("pods") { stage('Pods') { sh(fastlane + " pods") } } gitlabCommitStatus("test") { stage('Test') { sh(fastlane + ' test type:unit') junit allowEmptyResults: true, testResults: 'fastlane/test_output/report.junit' } } gitlabCommitStatus("build ipa") { stage('Build IPA') { sh(fastlane + " beta") } } } currentBuild.result = 'SUCCESS' } catch (e) { currentBuild.result = "FAILURE" throw e } finally { notifyBuild(currentBuild.result,reason) } }

Nutze GitLabBuild und GitlabCommitStatus, um die Jenkins Pipeline in Gitlab zu visualisieren und das mergen des MR in Gitlab zu erlauben bzw. zu verbieten. Dieser Job kann global für alle iOS Merge Requests verwendet werden. Setze den WebHook für diesen Jenkins Job von deinem Repository aus – und fertig.

Ein nerviger aber wichtiger Schritt ist derSCM Checkout, bei dem Source undTarget Branchzusammengeführt werden. Die ENV Daten (env.gitlabSourceBranch und env.gitlabTargetBranch) werden vom Gitlab Webhook geparst.
Man kann das Rebuilding des Merge Requests leicht wieder einrichten, wenn ein neuer Push auf Source oder Target Branch erfolgt ist.

Ein weiteres cooles Feature ist, dass man die Jenkins Pipeline durch einen Kommentar im MR nochmals ablaufen lassen kann. Wir nutzen die Phrase “rebuild pls” und Jenkins baut den Job automatisch neu auf!

Job DSL und Seed Jobs

Dank des Job DSL Plugins können wir dynamisch Jenkins Pipeline Jobs via Jenkins DSL API generieren.
Einen Job, der andere Jobs generiert, nennt man Seed Job.
Hier kommt ein Beispiel für das Generieren eines iOS Merge Request Jobs:

String scriptPath = "jobs/gitlab" String jobSuffix = "merge-request-builder" String mCommentTrigger = "rebuild pls" def gitlabOn = { it / 'properties' / 'com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty' { 'gitLabConnection'('gitlab') } } def platform = 'ios' def jobName = "$platform-$jobSuffix" pipelineJob(jobName) { concurrentBuild(false) configure gitlabOn definition { cps { script(readFileFromWorkspace("${scriptPath}/${jobName}.groovy")) sandbox() } } triggers { gitlabPush { buildOnMergeRequestEvents(true) buildOnPushEvents(false) enableCiSkip(true) setBuildDescription(true) commentTrigger(mCommentTrigger) rebuildOpenMergeRequest('both') skipWorkInProgressMergeRequest(false) } } }

Die Produktions-Pipeline testen

Jetzt kommt der Moment, in dem wir unsere Jenkins Pipeline aufbauen, testen und ausliefern müssen. CI/CD für die CI/CD-Pipeline.

Yo dawg, I heard you like Inception.

Wir platzieren ein einfaches Jenkinsfile in der Shared Pipeline Library und nutzen es, um die Jobs im vorigen Kapitel “zu säen”.

// use a Pipeline class src/cz/ackee/Pipeline.groovy rather than the global func def pipeline = new cz.ackee.Pipeline() node { properties([ disableConcurrentBuilds() ]) pipeline.checkoutScm() // set additional envvars and config pipeline.setEnv() stage('seed jobs') { withCredentials([string(credentialsId: 'jenkins-gitlab-credentials', variable: 'gitlabCredentials')]) { jobDsl targets: 'jobs/**/seed.groovy', additionalParameters: [credentials: gitlabCredentials] } } stage('test') { // run basic tests on pipeline like mysql reporting, slack, gcloud, kubectl and gitlab integrations checking pipeline.envTest() // test Node.js Pipeline def obj = build job: '../node-template/master/', propagate: false if(obj.result == 'FAILURE') throw new RuntimeException("Node.js Pipeline test failed.") // test React Pipeline obj = build job: '../react-template/master', propagate: false if(obj.result == 'FAILURE') throw new RuntimeException("React Pipeline test failed.") // test Middleman Pipeline obj = build job: '../middleman-template/master', propagate: false if(obj.result == 'FAILURE') throw new RuntimeException("Middleman Pipeline test failed.") } stage('lint') { //TODO: add groovy lint } }

Weitere großartige Dinge, die man mit der Jenkins Shared Pipeline Library anstellen kann

Alles, was du willst. Wirklich!

Links

Für NodeJS , Android , iOS , React , middleman, etc. gibt es noch weitere Pipelines.

https://github.com/AckeeDevOps/jenkins-pipeline-library

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.