Blogartikel

11.02.2020

EcoSystem

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

Der erste Teil dieser Artikelserie zeigt Anwendungsfälle und Vorteile, die Präsentationen mit reveal.js haben – sie sind Docs As Code und können deshalb unter Versionsverwaltung gestellt und natürlich auch per Continuous Delivery ausgeliefert werden. Weiterhin wird in einer beispielhaften konkreten Umsetzung gezeigt, wie man mit Jenkins Pipelines auf GitHub Pages deployt. Dieser Teil zeigt weitere Alternativen für das Deployment (Sonatype Nexus und Kubernetes), wobei die generelle Struktur des Jenkinsfile die gleiche bleibt.

Der erste Teil dieser Artikelserie zeigt Anwendungsfälle und Vorteile, die Präsentationen mit reveal.js haben – sie sind Docs As Code und können deshalb unter Versionsverwaltung gestellt und natürlich auch per Continuous Delivery ausgeliefert werden. Weiterhin wird in einer beispielhaften konkreten Umsetzung gezeigt, wie man mit Jenkins Pipelines auf GitHub Pages deployt. Dieser Teil zeigt weitere Alternativen für das Deployment (Sonatype Nexus und Kubernetes), wobei die generelle Struktur des Jenkinsfile die gleiche bleibt.

Deployment auf Nexus

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

stage('Deploy Nexus') {
    mvn.useDeploymentRepository([
            id: 'ecosystem.cloudogu.com',
            CredentialsId: 'ces-nexus'
    ])

    mvn.deploySiteToNexus("-Dartifact=${env.BRANCH_NAME} ")
}

In nicht-öffentlichen Kontexten (z.B. bei Firmen-internen Präsentationen) ist das öffentliche Deployment auf GitHub keine Option. Wenn intern ohnehin ein Nexus-Repository bereitsteht, kann der Maven Site-Mechanismus verwendet werden, um die Präsentation dort hochzuladen.

Dazu benötigt man

  • ein Nexus ein Repository im raw Format (im Bsp. Cloudogu-Docs),
  • eine pom.xml, in der im Wesentlichen konfiguriert wird, wohin die Site deployed wird und
  • einen User Account, der Schreibrechte auf das Nexus-Repository hat.

Generell deployt man eine Maven Site mittels mvn site:deploy. User Account und Passwort definiert man in der settings.xml. Erfahrungsgemäß ist letzteres auf CI-Servern umständlich. Auch hier werden die Implementierungs-Details der ces-build-lib überlassen und einfach der Step Maven.deploySiteToNexus() aufgerufen. Dabei wird wieder Docker verwendet, um Maven bereitzustellen, mittels der MavenInDocker-Klasse aus der ces-build-lib.

Damit dieser funktioniert, muss vorher mittels Maven.deploySiteToNexus() folgendes übergeben werden:

  • die ID des Repositories (verweist auf die Definition in der pom.xml, siehe unten) und
  • die ID von Username with password Credentials, die in Jenkins gepflegt werden. Im Beispiel ist das ces-nexus. CredentialsId: 'ces-nexus'. Diese gehören zu einem User in Nexus, der mittels einer Role autorisiert, ist auf das Repository zu schreiben. In Nexus 3 benötigt die Role konkret die Rechte nx-repository-view-raw-<RepoName>-add und -edit (z.B. nx-repository-view-raw-Cloudogu-Docs-add).

Die wesentlichen Punkte in der pom.xml sind die folgenden (siehe GitHub für das vollständige Beispiel):

<groupId>com.cloudogu.slides</groupId>
<artifactId>${artifact}</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>

<url>https://ecosystem.cloudogu.com/nexus/repository/Cloudogu-Docs/${project.groupId}/${project.artifactId}/${project.version}/</url>

<distributionManagement>
    <site>
        <id>ecosystem.cloudogu.com</id>
        <name>site repository ecosystem.cloudogu.com</name>
        <url>dav:https://ecosystem.cloudogu.com/nexus/repository/Cloudogu-Docs/${project.groupId}/${project.artifactId}/${project.version}/</url>
    </site>
</distributionManagement>

<properties>
    <revision>-SNAPSHOT</revision>
    <artifact>template</artifact>
</properties>

Die Maven Koordinaten (groupId, artifactId und version) werden hier verwendet, um die URL der Präsentation im Nexus-Repository zu definieren. Die URL einer vom master Branch aus deployten Version sieht beispielsweise so aus: https://ecosystem.cloudogu.com/nexus/repository/Cloudogu-Docs/com.cloudogu.slides/master/201904291351-dd1df3d7/

Dabei wird das Maven-Feature "CI Friendly Versions"< verwendet, um die Koordinaten dynamisch während des Builds mittels System Properties (auf der Kommandozeile z.B. mittels -Dartifact=abc) zu setzen:

  • artifactId wird verwendet, um den Namen des aktuellen Branches in der URL abzubilden (dadurch bekommt man für jeden Branch eine eigene URL) und
  • revision setzt die Version, die in jedem Build neu berechnet wird. Der Wert wird bereits in createVersion() übergeben (siehe ersten Teil der Artikelserie): mvn.additionalArgs = "-Drevision=${versionName} "

Deployment auf Kubernetes

Eine weitere Alternative ist das Deployment als Container auf Kubernetes. Wer bereits einen Cluster konfiguriert hat, kann die Präsentation ohne großen Aufwand als zusätzliche Applikation deployen. Für alle anderen kann dieses Beispiel als erste, einfache Beispielanwendung für Continuous Delivery mit Jenkins und Kubernetes dienen.

Da der Build bereits im Jenkinsfile definiert ist, fällt das Dockerfile (also der Bauplan für das Docker Image) überschaubar aus:

FROM bitnami/nginx:1.14.2
COPY . /app/

Hier kommt Nginx als bewährter Webserver (bedient fast die Hälfte des Internet Traffics) zum Einsatz. Als Basis-Image kommt allerdings nicht das offizielle zum Einsatz, sondern, das von bitnami, das auf IT-Sicherheit spezialisiert ist. Im Gegensatz zum offiziellen Image wird das von bitnami nicht als root-User ausgeführt und ist damit z.B. nicht für die erste ernsthafte Sicherheitslücke in Kubernetes (CVE-2019-5736) anfällig.

Im Dockerfile gilt es zu beachten, dass COPY . / den gesamten Workspace in das Image kopieren würde, was dann zur Laufzeit durch Nginx bereitgestellt würde. Dadurch wäre Beispielsweise auch das Jenkinsfile und die k8s.yaml zum Download verfügbar – ein Sicherheitsrisiko! Daher wird zusätzlich eine .dockerignore Datei gepflegt:

**
!index.html
#...
  • Beginn mit ** (alles ignorieren)
  • dann Freigabe der gewünschten Dateien und Ordner mittels Negierung.
  • Dies führt effektiv zu einem Whitelisting.

Damit aus dem Image auf Kubernetes ein Container gestartet werden kann, der von außen erreichbar ist, wird außerdem folgendes benötigt:

  • ein Deployment (Template für Pods, in denen Container ausgeführt werden),
  • einen Service (fester Endpunkt (IP-Adresse/DNS-Name) für potenziell kurzlebige Pods) und
  • einen Ingress (Mapping von Hostname auf Service für eingehende Requests) – dies setzt natürlich einen konfigurierten Ingress Controller (wie z.B. Træfik) voraus.

Diese sind alle in der Datei k8s.yaml definiert. Interessant daran ist, dass im Deployment als Image-Name ein Platzhalter verwendet wird: image: $IMAGE_NAME.Dies wird in der Pipeline genutzt, um die aktuelle Version des Images einzutragen.

String versionName = createVersion(mvn)
//...
stage('Deploy Kubernetes') {
    deployToKubernetes(versionName)
}

// ...
void deployToKubernetes(String versionName) {

    String imageName = "cloudogu/continuous-delivery-slides-example:${versionName}"
    def image = docker.build imageName
    docker.withRegistry('', 'hub.docker.com-cesmarvin') {
        image.push()
        image.push('latest')
    }

    withCredentials([file(CredentialsId: 'kubeconfig-oss-deployer', variable: 'kubeconfig')]) {

        withEnv(["IMAGE_NAME=$imageName"]) {

            kubernetesDeploy(
                    CredentialsType: 'KubeConfig',
                    kubeConfig: [path: kubeconfig],

                    configs: 'k8s.yaml',
                    enableConfigSubstitution: true
            )
        }
    }
}

Im Jenkinsfile wird zunächst mit Jenkins-Bordmitteln das Image gebaut und in eine Registry gepusht (im Beispiel ist das DockerHub). Das fertige Image kann hier eingesehen werden. Dabei wird sowohl der aktuelle Versionsname als Docker-Tag gesetzt als auch latest. Zweiteres ist nicht zwingend notwendig aber good Practice in Docker Registries. Den notwendigen User Account legt man in Jenkins als Username with password-Credentials an und übergibt deren ID (im Beispiel ist das hub.docker.com-cesmarvin) an den docker.withRegistry()-Step. Der User Account benötigt in der Docker Registry Schreibrechte für das Image (im Bsp cloudogu/continuous-delivery-slides-example).

Jetzt muss der Image-Name noch in das Kubernetes-Deployment eingetragen und dieses an den Cluster übergeben werden. Diese Schritte erledigt beide der kubernetDeploy()-Step, der vom Kubernetes Continuous Deploy Plugin bereitgestellt wird. Mittels enableConfigSubstitution wird aktiviert, dass alle Einträge mit der $VARIABLE Syntax in den YAML-Dateien durch entsprechende Environment-Variablen aus der Jenkins Pipeline ersetzt werden (im Beispiel IMAGE_NAME). Auch beim Cluster muss man sich authentifizieren. Hier wird die vom CLI Tool kubectl bekannte kubeconfig-Datei verwendet, die als Secret file-Credentials im Jenkins hinterlegt ist (im Beispiel mit der ID kubeconfig-oss-deployer).

Mehr Details zum Erstellen der Credentials für die Container Registry und Kubernetes in Jenkins finden sich im 4. Teil unserer Artikelserie zu Jenkins Pipelines.

So hat man in wenigen Zeilen Jenkinsfile-Code vermeintlich komplizierte Themen wie Docker-Image-Bauen und Kubernetes-Deployment automatisiert.

Fazit

Zum Abschluss dieser Artikelserie werden in diesem Teil weitere Beispiele für Zielumgebungen des Deployments von reveal.js-Präsentation mittels Continuous Delivery Pipeline in Jenkins gezeigt. Konkret wird beschrieben, wie man auf Sonatype Nexus und Kubernetes deployt.

Blickt man über den Tellerand des Nutzen als Browser-basierte Präsentation, erkennt man, dass hier konkret gezeigt wird, wie man Continuous Delivery von Webanwendungen realisieren kann. Dabei bietet der Artikel eine Auswahl, bei der man sich je nach Anwendungsfall und verfügbarer Infrastruktur auch für Produktivsysteme bedienen kann.

  • Wer im Enterprise-Umfeld einfach eine nur intern verfügbare statische Website deployen will, kann hier einfach zu Nexus greifen.
  • Wenn statischer Content öffentlich sein kann oder soll, bietet sich das Deployment auf GitHub Pages an.
  • Kubernetes bietet die größte Flexibilität – hier kann man sowohl intern als auch extern statischen oder auch dynamischen Content hosten. Allerdings ist das Betreiben des Clusters aufwändiger. Wer hier mehr wissen will, wird in unserem Schulungsbereich fündig.

Tags