Hugo Site mit Gitlab deployen

Wie ich in einem Blog schrieb, wird diese Seite nun mit dem statischen Site-Generierer Hugo erstellt.

Das Schreiben geht leicht von der Hand, aber vor der Veröffentlichung sind noch einige Schritte nötig. Diese habe ich nun mit gitlab-ci auf gitlab.com automatisiert. Jeder Push ins Repository baut die Seite neu und überträgt die Änderungen auf meinen Webspace bei uberspace.

Die Vorlage für meine Umsetzung fand ich in diesem Blog der aber schon ein bisschen älter ist, so dass ich einige Anpassungen vornehmen musste. Genug Vorgeplänkel. Los geht’s.

Gitlab.com ist das Clound-Angebot der gleichnamigen Firma. Dort kann sich jede® einen Account anlegen. Im Gegensatz zu ähnlichen Anbietern wie Microsoft Github, kann man auch im freien Paket private Repositories anlegen. Außerdem ist Gitab Community-Edition Freie Software. Alle diese Schritte lassen sich also auch dort nachvollziehen.

Gitlab-ci ist ein Build-Runner, der Jobs ausführt, die jeweils in einem Docker-Container isoliert ausgeführt werden. Die Jobs werden von einer Pipeline koordiniert, die durch einen Push ausgelöst wird.

Die Hugo-Site wird in ein neues Repository eingecheckt. Das Theme sollte als git-Modul eingebunden werden.

.gitlab-ci.yml

Zusätzlich wird die Datei .gitlab-ci.yml auf der obersten Ebene des Repositories benötigt:

stages:
  - build
  - deploy
build:
  stage: build
  image: registry.gitlab.com/softmetz/softmetz.de-ci-build-hugo
  script:
  - git submodule update --init --recursive
  - hugo -b "${BLOG_URL}"
  artifacts:
    paths:
    - public
  expire_in: 1 hour
  only:
  - master
deploy:
  stage: deploy
  image: registry.gitlab.com/softmetz/softmetz.de-ci-deploy-rsync-ssh
  script:
  - echo "${SSH_PRIVATE_KEY}" > id_rsa
  - chmod 700 id_rsa
  - mkdir "${HOME}/.ssh"
  - echo "${SSH_HOST_KEY}" > "${HOME}/.ssh/known_hosts"
  - rsync -at --quiet --delete --delete-delay --delay-updates --exclude=_ --include=.well-known -e 'ssh -i id_rsa' public/ "${SSH_USER_HOST_LOCATION}"
  variables:
    GIT_STRATEGY: none
  only:
  - master

Der Abschnitt

stages:
  - build
  - deploy

bestimmt, dass die Pipeline aus zwei Stages besteht, build und deploy. Den Stages können dann Jobs zugeordnet werden, die stage-weise nacheinander, aber in der Stage parallel laufen. In diesem Fall gibt es zwei Stages und zwei Jobs, je stage einer.

Der Job build

Das Bauen der Hugo-Site übernimmt der Job namens “build”:

build:
  stage: build
  image: registry.gitlab.com/softmetz/softmetz.de-ci-build-hugo
  script:
  - git submodule update --init --recursive
  - hugo -b "${BLOG_URL}"
  artifacts:
    paths:
    - public
    expire_in: 1 hour
  only:
  - master

Die beschriebenen Schritte werden in einem Docker-Container namens registry.gitlab.com/softmetz/softmetz.de-ci-build-hugo ausgeführt.

Gitlab-ci checkt im Standard das Repository an einem festen Pfad aus und wechselt dann in diesem Verzeichnis. Im ersten Schritt werden dann die git-Submodule aktualisiert. Dadurch wird das Theme initial geklont oder aktualisiert.

Der zweite Schritt ist dann der eigentliche Hugo-Lauf. In diesem Fall wird mit -b ein Base-URL für die Seiten angegeben. Das Ergebnis wird in das Verzeichnis public geschrieben.

Der Abschnitt

  artifacts:
    paths:
    - public
    expire_in: 1 hour

sorgt nun für den Austausch der Daten zwischen den beiden Jobs build und deploy. Dabei muss bedacht werden, dass die Jobs wirklich von zwei verschiedenen Docker-Instanzen ausgeführt werden, die im Normalfall auch auf verschiedenen Host-Systemen laufen. Die Brücke stellt ein Artifact-Server dar, der die Daten entgegen nimmt.

Abschließend sagt

  only:
  - master

noch, dass der Job nur für den Branch master ausgeführt wird. Andere Tags oder Branches werden ignoriert.

Der Job deploy

Nach dem Ende des Jobs Build liegt die generierte Seite im Cache. Im nächsten Schritt muss sie auf den Webspace gebracht werden. Wieder der Code im Überblick:

deploy:
  stage: deploy
  image: registry.gitlab.com/softmetz/softmetz.de-ci-deploy-rsync-ssh
  script:
  - echo "${SSH_PRIVATE_KEY}" > id_rsa
  - chmod 700 id_rsa
  - mkdir "${HOME}/.ssh"
  - echo "${SSH_HOST_KEY}" > "${HOME}/.ssh/known_hosts"
  - rsync -at --quiet --delete --delete-delay --delay-updates --exclude=_ --include=.well-known -e 'ssh -i id_rsa' public/ "${SSH_USER_HOST_LOCATION}" 
  variables:
    GIT_STRATEGY: none
  only:
  - master

Wieder wird ein Docker-Image referenziert, diesmal registry.gitlab.com/softmetz/softmetz.de-ci-deploy-rsync-ssh. Dieses stellt das Programm rsync und ssh zur Verfügung.

Da der Container immer wieder neu erstellt wird, müssen einige Vorarbeiten gemacht werden.

Der Abschnitt

  - echo "${SSH_PRIVATE_KEY}" > id_rsa
  - chmod 700 id_rsa
  - mkdir "${HOME}/.ssh"
  - echo "${SSH_HOST_KEY}" > "${HOME}/.ssh/known_hosts"

kopiert einen SSH-Private-Key (auf keinen Fall einen dedizierten nehmen, nicht den eigenen der noch außerhalb des Webspace verwendet wird) in die Datei id_rsa und passt die Berechtigungen an. Dann wird noch der Host-Key des Webspace-Servers in die Datei `${HOME}/.ssh/known_hosts eingefügt. Dies ist nötig, damit ssh nicht mit der Meldung abbricht, dass der Benutzer den Host-Key bestätigen muss.

Die eigentliche Arbeit passiert dann in

  - rsync -at --quiet --delete --delete-delay --delay-updates --exclude=_ --include=.well-known -e 'ssh -i id_rsa' public/ "${SSH_USER_HOST_LOCATION}"

-atz verarbeitet den Dateibaum rekursiv im Archiv-Modus. --delete löscht auf der Gegenseite Dateien, die an der Quelle existieren. --delete-delay und --delay-updates optimieren die Reihenfolge der Operationen und beschleunigen so den Transfer. -e 'ssh -i id_rsa' legt fest, dass der SSH-Private-Key von eben verwendet wird. Im Kern wird dann das Verzeichnis public/ nach ${SSH_USER_HOST_LOCATION} übertragen.

Da der Source-Code nicht mehr benötigt wird, sparen wir uns mit dem folgenden Ausdruck den Checkout:

  variables:
    GIT_STRATEGY: none

Die Repository-Konfiguration

Gerade im zweiten Job wird viel mit Variablen gemacht. Wo kommen diese Variablen her? Sie werden im Gitlab-Frontend hinterlegt.

Wenn man sich auf der Hauptseite des Projekts befindet, wechselt man in Settings und dann nach CI/CD.

In der Sektion Variables können schließlich die Variablen hinterlegt werden:

Gitlab Variablen

Wenn die Variablen gespeichert wurden und ein Push ins Repo gemacht wird, sollte jetzt eigentlich alles funktionieren. Das liegt daran, dass ich die Docker-Images bereits gebaut habe. Aber natürlich kann man sich die selbst erstellen, Sicherheit, Vertrauen und so.

Das Hugo-Docker-Image

Das Docker-Image für Hugo wird mit folgendem Dockerfile erstellt:

FROM alpine:3.7

RUN apk add --update \
      git && \
    rm -rf /var/cache/apk/*

ENV HUGO_VERSION 0.42.2
ENV HUGO_RESOURCE hugo_${HUGO_VERSION}_Linux-64bit

ADD https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${HUGO_RESOURCE}.tar.gz /tmp/

RUN mkdir /tmp/hugo && \
    tar -xvzf /tmp/${HUGO_RESOURCE}.tar.gz -C /tmp/hugo/ && \
    mv /tmp/hugo/hugo /usr/bin/hugo && \ 
    rm -rf /tmp/hugo*

Das ganze basiert auf alpine-Linux, einer sehr kleinen Linux-Distribution.

RUN apk add --update \
      git && \
    rm -rf /var/cache/apk/*

installiert git aus den Paketquelle von alpine.

ENV HUGO_VERSION 0.54.0
ENV HUGO_RESOURCE hugo_${HUGO_VERSION}_Linux-64bit

ADD https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${HUGO_RESOURCE}.tar.gz /tmp/

Legt fest, welche Version von Hugo verwendet werden soll und lädt die von Microsoft Github herunter. Das Archiv landet in /tmp.

RUN mkdir /tmp/hugo && \
    tar -xvzf /tmp/${HUGO_RESOURCE}.tar.gz -C /tmp/hugo/ && \
    mv /tmp/hugo/hugo /usr/bin/hugo && \ 
    rm -rf /tmp/hugo*

schließlich kopiert hugo an die richtige Stelle und löscht Altlasten.

Das Image wird mit

docker build -t repository/image:latest 

gebaut und mit

docker push repository/image:latest

veröffentlich.

Wie ich inzwischen herausgefunden habe, kann man auf Gitlab.com sogar Docker-in-Docker verwenden, um Docker-Images zu bauen. Dafür verwende ich folgendes .gitlab-ci.yml:

image: docker:latest

services:
  - docker:dind

stages:
- build

variables:
  DOCKER_IMAGE_TAG: registry.gitlab.com/softmetz/softmetz.de-ci-build-hugo

before_script:
  - echo $CI_BUILD_TOKEN | docker login --username gitlab-ci-token --password-stdin registry.gitlab.com

build:
  stage: build
  script:
    - docker build --pull -t $DOCKER_IMAGE_TAG .
    - docker push $DOCKER_IMAGE_TAG

Das rsync-Docker-Image

Analog wird mit dem zweiten Images verfahren. Hier das Dockerfile:

FROM alpine:3.7

RUN apk add --update \
      openssh \
      rsync && \
    rm -rf /var/cache/apk/*

Analog gibt es nun folgendes .gitlab-ci.yml:

image: docker:latest

services:
  - docker:dind

stages:
- build

variables:
  DOCKER_IMAGE_TAG: registry.gitlab.com/softmetz/softmetz.de-ci-deploy-rsync-ssh

before_script:
  - echo $CI_BUILD_TOKEN | docker login --username gitlab-ci-token --password-stdin registry.gitlab.com

build:
  stage: build
  script:
    - docker build --pull -t $DOCKER_IMAGE_TAG .
    - docker push $DOCKER_IMAGE_TAG

Das ganze sollte nach dem vorherigen Abschnitt selbsterklärend sein.

Fazit

Gitlab und Gitlab-ci sind richtig cool und nach etwas Lernen geht die Benutzung ganz einfach von der Hand. Die Automatisierung spart pro Änderung an der Seite 5 bis 10 Minuten manuelle Arbeit, Zeit die mir zum Schreiben von solchen schönen Anleitungen bleibt. :-)

Updates:

2019-02-01