Docs As Code – Continuous Delivery von Präsentationen mit reveal.js und Jenkins – Teil 1
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 (siehe Blog Post) 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:
- GitHub Pages (Teil 1)
- Sonatype Nexus (Teil 2) und
- Kubernetes (Teil 2).
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.
Cloudogu EcoSystem Quickstart Guide
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.
Cloudogu EcoSystem
Überzeugen Sie sich von den Vorteilen des Cloudogu Ecosystem. Nutzen Sie jetzt kostenlos die moderne DevOps Plattform.
Zur PlattformContinuous 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.
Auf unserer Open Source Jenkins-Instanz kann man den Build in Aktion erleben.
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 imPATH
haben. Die Verbindung von Jenkins Agent zum Job wird über Labels realisiert. Hier fordert dasJenkinsfile
einen Agent mit dem Labeldocker
: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
undPackage
– 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.
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.