Wie bereits in einem früheren Beitrag erwähnt, nutze ich für meinen Blog mittlerweile Hugo, einen Static Site Generator, anstelle einer Blog-Engine oder eines Content Management Systems. Bisher habe ich die Entscheidung nicht bereut, musste aber feststellen, dass sich mein Workflow im ersten Schritt nicht wirklich verbessert hat. Um das zu beheben, sollte man zunächst zwei Dinge betrachten: Wie hat es bisher funktioniert und wo will ich eigentlich hin? Dieser Beitrag soll eine Art Retrospektive sein und meinen Weg hin zu einer Continuous Deployment Strategie mit Hugo und Drone CI veranschaulichen.

Den notwendigen Neuaufbau meiner Infrastruktur habe ich nicht nur zum Anlass genommen, um alles vollständig auf Ansible umzustellen, sondern auch um meinen Softwarestack kritisch zu hinterfragen und aufzuräumen. Jede Software erfüllt natürlich einen Zweck, entscheidend für mich war aber: Wie wartungsintensiv ist die Lösung, wie leicht lässt sie sich deployen und rechtfertigt der Aufwand den Zweck. Im Fall von meiner Blog-Engine Serendipity (s9y) bin ich zu dem Schluss gekommen, dass ich darauf eigentlich ganz gut verzichten kann. Nebenbei empfand ich die Weboberfläche zur Verwaltung meiner Beiträge als nicht optimal für mich. Ich wollte lieber in einem Editor meiner Wahl, am besten auch Offline, arbeiten können anstatt in einer Weboberfläche. Außerdem musste man für etwas komplexere Formatierungen dann doch wieder zu HTML und CSS wechseln, weil der WYSIWYG-Editor das nicht hergab und Markdown im Standard da auch relativ beschränkt ist. Damit war die Entscheidung auch schon getroffen, es soll eine neue Lösung her.

Der zweite Schritt war die “Suche” nach einer Alternative. Zugegeben, da habe ich es mir tatsächlich etwas einfach gemacht und keinen großen Softwareauswahlprozess durchgeführt. Dass es ein Static Site Generator werden soll, stand für mich fest und Hugo war mir bereits bekannt. Nach den ersten Experimenten mit Hugo, die ohne große Zwischenfälle verliefen, fing ich an ein paar Punkte zu sammeln, die ich mir für die konkrete Umsetzung gewünscht habe:

  • Keine Serverkomponente mehr, die gewartet werden muss, nur noch good old HTML
  • Artikel können in Markdown geschrieben werden
  • Artikel lassen sich bequem über Git verwalten
  • Design und Inhalt sind voneinander getrennt und können separat verwaltet werden

Zur Trennung des Seitendesigns und des Inhaltes verwende ich zwei Git Repositories. Das erste Repository beinhaltet tatsächlich nur das reine Theme. Im zweiten Repository befinden sich aber nicht nur die Markdown Dateien für die Artikel, sondern eigentlich die komplette Ordnerstruktur und Seitenkonfiguration für Hugo. Soweit alles nach meinen Vorstellungen. Aber wie bringe ich jetzt beides zusammen, damit Hugo daraus meinen Blog generieren kann? Theoretisch gibt es dafür aus meiner Sicht zwei Ansätze:

Methode 1: Unabhängige Repositories

Diese Methode ist auf den ersten Blick die leichtere. Beide Repositories bleiben komplett unabhängig voneinander und werden einzeln verwendet. Führt aber dazu, dass ich vor jeder Generierung beide irgendwie manuell zusammenführen muss.

  • Content-Repository klonen und in den Themes Order wechseln
  • Eventuell zum produktiven Branch wechseln
  • In Theme Verzeichnis wechseln
  • Theme-Repository klonen
  • Eventuell zum produktiven Branch wechseln
  • Seite generieren

Methode 2: Integrierte Repositories

Alternativ lassen sich die Repositories auch bereits im Vorfeld, beispielsweise mit Git Subtree oder Git Submodule, miteinander verbinden. Spart vor dem Generieren erstmal nicht viel, außer das separate klonen vom Theme.

  • Content-Repository klonen und in den Themes Order wechseln
  • Eventuell zum produktiven Branch wechseln
  • Seite generieren

Ich habe mich für die zweite Methode entschieden und arbeite mit Git Subtree. Dafür gibt es im Content-Repository einen separaten Branch der nur das Theme beinhaltet. Bei Bedarf hole ich mir per Git Pull die Änderungen aus dem Theme-Repository und überführe diese anschließend in den master Branch. Ich kann also im Theme-Repository ungestört arbeiten und die Änderungen bei Bedarf kontrolliert überführen. Nach Änderungen reicht es jetzt also das Repoitory zu klonen, anschließend die Seite zu generieren und das Ergebnis auf den Webserver zu übertragen. Soweit so gut. Gehen wir einen Schritt weiter und betrachten die offenen Punkte die noch nicht gelöst sind:

  • CSS Minify/Beautify für das Theme sicherstellen
  • Automatisierung des Deployments bei neuen Artikeln oder Änderungen am Theme

Um Bandbreite bei der Übertragung zu sparen, kann man CSS oder auch JavaScript Dateien verkleinern. In der Praxis geschieht das im Wesentlichen durch das Entfernen nicht benötigter Zeichen wie Leerzeichen, Leerzeilen und Zeilenumbrüchen. Automatisieren lassen sich solche Tasks beispielsweise mit gulp.js. Mithilfe von Plugins können so beliebige Tasks gruppiert werden. Ein Gulp Task kann beispielsweise so aussehen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const gulp = require('gulp');
const minifyCSS = require('gulp-csso');
const concat = require('gulp-concat');
const rename = require('gulp-rename');
const cleanCSS = require('gulp-clean-css');
const autoprefixer = require('gulp-autoprefixer');

var CSSDEST = 'static/'

gulp.task('default', defaultTask);

function defaultTask(done) {
  return gulp.src('src/css/*.css')
              .pipe(concat('default.css'))
              .pipe(cleanCSS({format: 'beautify'}))
              .pipe(autoprefixer({
                browsers: ['last 4 versions'],
                cascade: false
              }))
              .pipe(gulp.dest(CSSDEST))
              .pipe(cleanCSS())
              .pipe(rename({ extname: '.min.css' }))
              .pipe(gulp.dest(CSSDEST))
}

Der hier zusammengestellte defaultTask macht der Reihe nach folgendes:

  • alle CSS Dateien aus dem Ordner src/css/ in eine einzelne Datei default.css zusammenführen
  • die Formatierungen korrigieren und vereinheitlichen (beautify)
  • fehlende CSS Präfixes ergänzen
  • die Datei in den Ordner static/ speichern
  • CSS verkleinern (minify) und als default.min.css ebenfalls in den Ordner static/ speichern

Die aufbereitete Datei default.min.css wird dann als CSS Datei für die fertige Seite verwendet. Wie gesagt, die Tasks lassen sich beliebig erweitern und bei Bedarf unterschiedlich gruppieren. Denkbar ist auch das Kompilieren von SASS oder SCSS Dateien.

Bleibt zum Schluss nur noch eines. Bei Änderungen sollen automatisch meine Gulp Tasks ausgeführt, die Seite generiert und auf meinen Webspace deployed werden. Am besten eignet sich dazu ein Continuous Integration System wie beispielsweise Travis CI. Ich habe mich allerdings für Drone CI entschieden. Drone selbst wird als Docker Image bereitgestellt und kann auf dem eigenen Server betrieben werden. Wem das zuviel Aufwand ist, für den gibt es auch eine Cloud Version. Wie bei den meisten CI Lösungen wird für jedes Repository eine Pipeline definiert, die beschreibt, welche Schritte bei welcher Aktion (Push, Tag, Pull Request) ausgeführt werden sollen. Drone führt dabei jeden Schritt der Pipeline in einem eigenen Docker Container aus. Meine definierten Pipelines für das Theme-Repository und das Content-Repository findet ihr auf meiner Gitea Instanz. Ich beschreibe an dieser Stelle nur kurz den groben Ablauf für beide Repositories.

Drone Pipeline bei Änderungen am Theme-Repository
Drone Pipeline bei Änderungen am Theme-Repository

Die Pipeline für das Theme-Repository ist relativ einfach gehalten. Gulp Tasks ausführen, bei Bedarf geänderte und bereinigte CSS Dateien (Assets) mittels eines Bots zurück in das Repo pushen und eine Benachrichtigung mit dem Status der Pipeline in einen privaten Matrix Channel senden. Fertig. Änderungen an dem Theme werden derzeit bewusst nicht automatisch durch die CI in das Content-Repository übernommen. Möglich wäre das durchaus, ich mache diesen Schritt aber vorerst manuell um Änderungen besser steuern zu können.

Drone Pipeline bei Änderungen am Content-Repository
Drone Pipeline bei Änderungen am Content-Repository

Auch die Pipeline bei Änderungen am Content-Repository ist nicht wirklich kompliziert, hat aber ein paar mehr Schritte. Zuerst wird die neue Version der Seite generiert, die verwendete Hugo Version kann hierbei über das Drone Plugin gesteuert werden. Als Nächstes wird dann der aktuelle Stand der Seite auf dem Webserver “eingefroren”. Dazu wird der aktuelle Ordner kopiert und ein Symlink auf die Kopie erstellt. Anschließend löscht Drone den Ordner der alten Version auf dem Webserver und erstellt ihn wieder mit der neuen Version. Zum Abschluss wird der Symlink wieder umgelenkt und die Kopie gelöscht. Warum das ganze? Ich habe das Drone Plugin so konfiguriert, dass die Dateien beim Deployment nicht einfach über den alten Stand drüber kopiert werden, sondern die alte Version vorher gelöscht wird. Dadurch kann ich sicherstellen, dass Überreste von alten Artikeln oder im Theme tatsächlich auch entfernt werden, wenn sie nicht mehr benötigt werden. Das führt beim Deployment allerdings zu einer kurzen Downtime meines Blogs, da zwischen dem löschen und dem kopieren nichts von der Seite mehr da ist. Um das zu vermeiden, der der beschrieben Workaround für in Zero-Downtime-Deployment.

Mit diesem Workflow bin ich aktuell ganz zufrieden, ich kann offline in einem Editor meiner Wahl arbeiten, ich kann meine Arbeit mittels Git synchronisieren und am Handy oder einem anderen PC weiter schreiben und das Veröffentlichen geht bequem mit einem einfachen Push von der Kommandozeile aus.