Blogartikel

26.08.2019

EcoSystem

Docs As Code – Continuous Delivery von Präsentationen mit reveal.js und Jenkins – Teil 1

Reveal.js ermöglicht es Softwareentwicklern Folien für Präsentationen mittels Web-Technologien (HTML, CSS, JavaScript) umzusetzen und im Browser anzuzeigen. Dadurch kann der von vielen Entwicklern gefürchtete Maus-getriebene Ausflug in die Welt von PowerPoint/Impress, mit Inkompatibilitäten zwischen Microsoft Office, Libre/Openoffice, Schwierigkeiten auf Linux und exklusiven Zugriff beim Bearbeiten entfallen.

Intro und Deployment nach GitHub Pages

Reveal.js ermöglicht Softwareentwicklern Folien für Präsentationen mittels Web-Technologien (HTML, CSS, JavaScript) umzusetzen und im Browser anzuzeigen. Dadurch kann der von vielen Entwicklern gefürchtete Maus-getriebene Ausflug in die Welt von PowerPoint/Impress, mit Inkompatibilitäten zwischen Microsoft Office, Libre/Openoffice, Schwierigkeiten auf Linux und exklusiven Zugriff beim Bearbeiten entfallen. Zum einen können dadurch alle Vorzüge des Webs (z.B. Multimedia, Hypermedia, UTF-8, Plattformunabhängigkeit, Verfügbarkeit im Internet, Plugins etc. – siehe auch die Beispiel Präsentation von reveal.js) genutzt werden.

Zum anderen sind Präsentationen damit auch "Documentation as Code", können also wie Code behandelt werden. Entwickler können dieselben Tools für das Erstellen von Präsentation nutzen wie für ihre tägliche Arbeit. Die Folien können beispielsweise in Markdown geschrieben und im Source Code Management (SCM) abgelegt werden (meist Git, z.B. bereitgestellt durch SCM-Manager oder GitHub).

Dadurch wird Revisionierung, Versionierung, gleichzeitiges Arbeiten, Branches, Merges, etc. möglich. Außerdem können Präsentationen von dort automatisiert bei jeder Änderung deployt werden. Damit hat man Continuous Delivery für seine Präsentationen! Das heißt, das bei jedem Git Push automatisch eine neue Version der Präsentation zur Verfügung gestellt wird.

Mit einem Git-basierten Wiki wie Smeagol können Änderungen sogar direkt im Browser durchgeführt werden. Dann führt ein Speichern im Wiki zum Deployment der Präsentation.

Diese Artikelserie zeigt am lebenden Beispiel wie man eine Continuous Delivery Pipeline für seine Präsentationen mit dem CI Server Jenkins realisiert. Exemplarisch werden folgende Zielumgebungen für das Deployment beschrieben:

Generell zeigt der Artikel damit, wie einfach sich mit Jenkins Pipelines Continuous Delivery leben lässt und bietet Beispiele für verschiedene Zielumgebungen.

Je nach vorhandener Infrastruktur und Zielumgebung ergibt sich ein unterschiedlicher Ablauf von der Änderung an den Folien bis zum Deployment:

  • Ausschließlich mit den Tools des Cloudogu EcoSystem, z.B. für interne Entwicklung (siehe Abbildung 1).
    • Textbearbeitung im Smeagol Wiki,
    • Versionsverwaltung mit SCM-Manager,
  • Mit GitHub und dem Cloudogu EcoSystem, z.B. für öffentliche Repositories (siehe Abbildung 2)
    • Versionsverwaltung mit GitHub,
    • Deployment nach GitHub Pages,
  • In beiden Fällen ist ein Deployment nach Nexus oder Kubernetes möglich.
  • Jenkins und Nexus können natürlich auch als einzelne Tools nach entsprechender Konfiguration betrieben werden.

Deployment mit Cloudogu EcoSystem Tools

Deployment mit GitHub

Verwendung bei Cloudogu

Die in dieser Artikelserie vorgestellten Zielumgebungen für das Deployment stammen direkt aus der Praxis. Bei Cloudogu werden reveal.js-Präsentationen unter anderem in folgenden Szenarien eingesetzt:

  • Unsere Kubernetes und Docker Schulungen werden aus dem Cloudogu EcoSytem heraus (natürlich) auf Kubernetes (K8s) deployt.
  • Öffentliche Präsentation auf Konferenzen teilen wir über GitHub Pages, z.B. '"3 things every developer should know about K8s security" auf der KubeCologne 2019'
  • Zusätzlich haben wir ein Repository in SCM-Manager, dessen Inhalt nach Nexus deployt wird, für interne Präsentationen. In diesem kann man durch ein einfaches git branch eine neue Präsentation erstellen, die ohne weiteren Aufwand Continuous Delivery durch eine Jenkins Multibranch-Pipeline hat.

Realisierung

Wie man dies umsetzen kann zeigt das upstream Projekt all unserer Projekte cloudogu/continuous-delivery-slides.

Es basiert direkt auf dem Repository von reveal.js und ergänzt dieses um

  • ein Jenkinsfile, in dem die Continuous Delivery Pipeline realisiert ist,
  • eine pom.xml für das Deployment nach Nexus,
  • Dockerfile und K8s.yaml für das Deployment auf Kubernetes,
  • weitere Features für reveal.js:
    • Font awesome Icons können verwendet werden,
    • Markdown Files werden aus dem Verzeichnis docs/slides geladen (weitere Files müssen in der <index.html im <div class="slides"> ergänzt werden).
    • Beispiel-Folien im Verzeichnis docs/slides, die die Möglichkeiten und Syntax zeigen.
  • <.smeagol.yml um die Bearbeitung in Smeagol zu ermöglichen und docs/Home.md, als Startseite des Smeagol-Wikis, die weitere Tipps zur Benutzung gibt (z.B. wie man effizient an den Folien arbeitet, die unterliegenden reveal-js Version upgradet oder die Folien als PDF exportiert).
  • Browser-unabhängige Darstellung von UTF-8 "emojis".
  • Styling der Folien in der Cloudogu Corporate Identity (im Ordner CSS).
  • Man darf auf weitere Feature gespannt sein, wie Continuous PDF delivery.

Im Folgenden werden die technischen Details der Continuous Delivery Pipeline beschrieben.

Continuous Delivery mit Jenkins

Die Continuous Delivery Pipeline ist im Jenkinsfile beschrieben, das in Jenkins durch das Pipeline Plugin ausgeführt wird (siehe auch unsere Artikelserie zum Thema).

Das folgende Listing zeigt das Skelett des Jenkinsfile. Aus Gründen der Lesbarkeit wurde es für den Artikel etwas vereinfacht und Stages und Steps werden in den folgenden Abschnitten gezeigt. Für die vollumfassende Version siehe <GitHub. Die fertige Präsentation kann man auf den GitHub Pages einsehen.

@Library('github.com/cloudogu/ces-build-lib@bc4b83b')
import com.cloudogu.ces.cesbuildlib.*

node('docker') {

    Git git = new Git(this, 'cesmarvin')

    catchError {

        Maven mvn = new MavenInDocker(this, "3.5.0-jdk-8")

        stage('Checkout') {
            checkout scm
            git.clean('')
        }

        String versionName = createVersion(mvn)

        stage('Build') {
            // ... Details siehe unten
        }

        stage('package') {
            // ... Details siehe unten
        }

        stage('Deploy GH Pages') {
              // ... Details siehe unten
        }

        stage('Deploy Nexus') {
            // ... Details siehe Teil 2
        }

        stage('Deploy Kubernetes') {
            // ... Details siehe Teil 2
        }
    }

    mailIfStatusChanged(git.commitAuthorEmail)
}

Voraussetzungen auf Jenkins

In der Pipeline wird massiv auf Docker gesetzt, um möglichst geringe Konfigurationsanforderungen an Jenkins zu stellen:

  • Außer den standardmäßig enthaltenen Plugins (z.B. Pipeline Plugin, GitHub Groovy Libraries für ces-build-lib – siehe unten) und einem Agent muss auf dem Jenkins Master nur wenig konfiguriert sein.
  • Ausnahme: Für das Deployment auf Kubernetes kommt das Kubernetes Continuous Deploy zum Einsatz.
  • Der Jenkins Agent (früher auch als Slave bezeichnet) muss ein funktionierendes docker CLI im PATH haben. Die Verbindung von Jenkins Agent zum Job wird über Labels realisiert. Hier fordert das Jenkinsfile einen Agent mit dem Label docker: node('docker').
  • Die für die jeweiligen Deployments notwendigen Schritte und Credentials sind in den jeweiligen Abschnitten aufgelistet.

Jenkins Pipeline Shared Library ces-build-lib

Die Pipeline verwendet die Shared Library ces-build-lib. Diese enthält u.a. Abstraktionen für Docker, Git, Maven und das Nexus-Repository. Dadurch ist vor allem das Deployment nach Nexus und GitHub Pages deutlich weniger komplex und an zentraler Stelle implementiert.

Komfortabel ist außerdem die Fehlerbehandlung: mailIfStatusChanged(git.commitAuthorEmail) sendet die typischen Jenkins Emails (fehlgeschlagen, instabil, wieder stabil) dynamisch an den Autor des letzten Commits statt einen manuell zu pflegenden Empfängerkreis. Dadurch, dass es nach dem catchError Block steht, wird dies auch im Fehlerfall ausgeführt.

Versionsname

String createVersion(Maven mvn) {
    String versionName = "${new Date().format('yyyyMMddHHmm')}-${new Git(this).commitHashShort}"

    if (env.BRANCH_NAME == "master") {
        mvn.additionalArgs = "-Drevision=${versionName} "
        currentBuild.description = versionName
    } else {
        versionName += '-SNAPSHOT'
    }
    return versionName
}

Eine Herausforderung bei Continuous Delivery Pipelines ist stets die automatische Vergabe von Versionsnamen. In diesem Beispiel haben wir uns für die Kombination <Datum>-<GitCommitHash> entschieden, z.B. (201904261520-077def6), weil dies einfach zu berechnen, eindeutig genug ist und sich bei Cloudogu in der Praxis bewährt hat. Dies ist in der Methode createVersion() realisiert. An dieser Stelle wird auch gezeigt, wie man ein Branch-basiertes Auslieferungsmodell realisieren kann: Commits auf dem Master Branch gehen in Produktion, alle anderen sind SNAPSHOT-Versionen und werden als solche gekennzeichnet. Außerdem wird der Versionsname als Beschreibung (currentBuild.description) in Jenkins gesetzt, die auf der Jenkins-Oberfläche neben der Build-Nummer angezeigt wird.

Struktur des Jenkinsfile

Das Jenkinsfile ist in Stages eingeteilt, die im Folgenden beschrieben werden:

  • Checkout – holt den Code aus dem Git Repository.
  • Build und Package – erzeugen die statische Webanwendung, die die Präsentation enthält.
  • Exemplarische Deploy Stages nach GitHub Pages, Nexus und Kubernetes.

Die folgende Abbildung zeigt die Stages im Jenkins BlueOcean Plugin.

Die Jenkins Pipeline in Aktion

Build und Package Stages

def introSlidePath = 'docs/slides/01-intro.md'
Docker docker = new Docker(this)

stage('Build') {
    docker.image('node:11.14.0-alpine')
      .mountJenkinsUser()
      .inside {
        sh 'npm install'
        sh 'node_modules/grunt/bin/grunt package --skipTests'
    }
}

stage('package') {
    docker.image('garthk/unzip').inside {
        sh 'unzip reveal-js-presentation.zip -d dist'
    }

    writeVersionNameToIntroSlide(versionName, introSlidePath)
}

// ...

private void writeVersionNameToIntroSlide(String versionName, String introSlidePath) {
    def distIntro = "dist/${introSlidePath}"
    String filteredIntro = filterFile(distIntro, "<!--VERSION-->", "Version: $versionName")
    sh "cp $filteredIntro $distIntro"
    sh "mv $filteredIntro $introSlidePath"
}

String filterFile(String filePath, String expression, String replace) {
    String filteredFilePath = filePath + ".filtered"
    // Fail command (and build) if file not present
    sh "test -e ${filePath} || (echo Title slide ${filePath} not found && return 1)"
    sh "cat ${filePath} | sed 's/${expression}/${replace}/g' > ${filteredFilePath}"
    return filteredFilePath
}

Zunächst wird der für reveal.js notwendigen Build ausgeführt. Hier wird das vom node Docker Container bereitgestellte npm genutzt, um Packages zu installieren und dann den grunt-Build von reveal.js auszuführen. Um Berechtigungsprobleme zu vermeiden, führt Jenkins generell docker.inside()-Steps mit der UserID (UID) aus, mit dem auch der Jenkins-Agent ausgeführt wird (docker run -u UID). Speziell npm kann allerdings offensichtlich nicht damit umgehen, wenn diese UID nicht in der /etc/passwd steht. Daher wird hier das von der Docker-Abstraktion aus der ces-build-lib bereitgestellte mountJenkinsUser() genutzt, um EACCES: permission denied-Fehler zu vermeiden.

Die Ausführung der reveal.js-Tests wird hier mittels --skipTests übersprungen, da das von reveal.js verwendete Puppeteer (aufgrund von Headless Chrome und X-Server) nur aufwändig in Docker ausführbar ist. Da es an dieser Stelle auch nicht um die Entwicklung von reveal.js geht, sondern um die Verwendung sollte das Überspringen der Tests hier keine Bad Practice sein.

Um nur die wirklich notwendigen Dateien aus dem Repository zu paketieren (Jenkinsfile, Dockerfile, etc. dürfen keinesfalls deployt werden, da hier sensible Daten über die Infrastruktur enthalten sind) wird ein Trick genutzt:

  • reveal.js hat bereits einen zip grunt-Task, der genau das Paket erstellt, das benötigt wird.
  • Dieses zip wird in der package-Stage einfach wieder entpackt.

Abschließend wird noch der Versionsname mittels sed auf die Titelfolie geschrieben, in dem der darin enthaltene Platzhalter <!--VERSION--> ersetzt wird.

Damit ist alles bereit für das Deployment, was im Folgenden exemplarisch für die Zielumgebung GitHub Pages beschrieben wird. Im zweiten Teil folgen dann Nexus und Kubernetes als weitere Beispiele.

Deployment auf GitHub Pages

Git git = new Git(this, 'cesmarvin')

stage('Deploy GH Pages') {
    git.pushGitHubPagesBranch('dist', versionName)
}

Technisch ist ein Deployment auf die GitHub pages ein git push auf den gh-pages-Branch. Dieser Branch muss im Repository existieren und in den Repository-Einstellungen aktiviert werden.

In der Pipeline ist das Deployment selbst nur ein einfacher Aufruf des Git.pushGitHubPagesBranch()-Steps aus der ces-build-lib. Wichtig ist an dieser Stelle noch die Authentifizierung und Autorisierung bei GitHub. Man benötigt:

  • einen GitHub-User,
  • der Schreibrechte auf das GitHub-Repo hat und
  • dessen Credentials als Username with password-Credentials in Jenkins hinterlegt sind.
  • Dann kann die ID dieser Credentials an das Git Objekt übergeben werden. Im Beispiel ist das cesmarvin: new Git(this, 'cesmarvin')
  • Hinweis: Es ist Good Practice einen Personal access token bei GitHub speziell für diesen Anwendungsfall zu generieren und anstelle des Passwortes zu verwenden. Dessen Berechtigungen können minimal und für diesen Anwendungsfall vergeben werden, er kann jederzeit gelöscht werden und wenn er in falsche Hände gerät, ist nicht das Passwort kompromittiert. Eine weitere Option wäre die Verwendung eines speziellen GitHub Deploy Keys für das Repository. Dies ist derzeit in der ces-build-lib noch nicht implementiert.

Das in unserem Beispiel deployte Ergebnis kann man direkt in den GitHub Pages des Beispiel Repositories sehen.

Ausblick

Im ersten Teil dieser Serie werden die Vorzüge des Themas Continuous Delivery von reveal.js-Präsentationen eingeleitet und Anwendungsbeispiele gezeigt. Die Umsetzung wird mittels Jenkins Pipeline detailliert beschrieben und endet mit einem exemplarischen Deployment auf GitHub Pages. Der zweite Teil zeigt wie man die Präsentationen alternativ auf Sonatype Nexus oder einem K8s Cluster deployt.

Blickt man über den Tellerrand des Nutzens als Browser-basierte Präsentation, erkennt man, dass hier konkret gezeigt wird, wie man Continuous Delivery von Webanwendungen realisieren kann und zwar gleich mit Beispielen für verschiedene Plattformen.

Tags