Blog

Wie setzen wir die Gitlab-CI ein?

26.11.2020 // Gregor Goldbach

In meinem Artikel über die Optimierung von Docker-Images habe ich erwähnt, dass wir die »Gitlab-CI« einsetzen. In diesem Artikel beschreibe ich nun näher, was das eigentlich ist und welche Erfahrungen wir gemacht haben.

Was bedeutet CI?

Mit »CI« wird in der Software-Entwicklung »continuous integration« bezeichnet, also die fortlaufende Integration von Software-Komponenten. Dies ist ein Vorgehen im Team und weniger ein Werkzeug, bei dem Änderungen an der gemeinsamen Code-Basis fortlaufend an einer definierten Stelle zusammengeführt und auf ihre Qualität hin untersucht werden.

Diese Untersuchung besteht in der Regel aus einer Reihe von automatisierten Tests, die Abweichungen von gewünschten Eigenschaften aufzeigen sollen. Hierzu kann auch das Einhalten von Codier-Richtlinien zählen.

Durch die häufige Integration und einem hohen Grad an Automatisierung möchte das Team sicherstellen, dass Fehler und Abweichungen schneller gefunden und behoben werden können.

Die fortlaufende Integration ist also ein Baustein, um qualitätssichernde Maßnahmen im Softwareentwicklungsprozess umzusetzen.

Gitlabs CI-Umsetzung

Das Produkt Gitlab unterstützt fortlaufende Integration durch eine textuelle Beschreibung von »Pipelines«, in denen konfigurierbare Befehle ausgeführt werden.

Nach einem Push von Änderungen in einem Repository wird ein Docker-Container gestartet, in dem dann die beschriebenen Befehle ausgeführt werden.

Das folgende Beispiel zeigt eine hypothetische Distribution Foo::Bar::Baz, die mit Dist::Zilla in Ubuntu gebaut und getestet wird.

image: ubuntu

stages:
  - build

build:
  stage: build
  script:
    - dzil build
    - dzil cover
    - dzil test
  coverage: '/^Total.+\s+(\d+.\d)$/'
  artifacts:
    when: always
    name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
    paths:
      - foo-bar-baz-*.tar.gz

Diese Befehle werden typischerweise das Compilieren und Linken von Software sein, an die sich Testautomatisierung und weitere Prüfungen anschließen.

Wenn alle Befehle erfolgreich ausgeführt wurden, ist der Build erfolgreich. Erfolg oder Misserfolg eines Builds werden dem Entwickler, dessen Änderungen die Pipeline ausgelöst haben, per E-Mail mitgeteilt. Der Zustand eines Builds ist zusätzlich im Gitlab-Projekt sichtbar.

Welche Arten von Pipelines es gibt und wie dies in Gitlabs Oberfläche dargestellt wird, ist in Gitlabs Dokumentation nachzulesen.

Wie nutzen wir die Gitlab-CI?

Eine Pipeline wird wie oben beschrieben nach einem Push der Änderungen gestartet. Bei der Konfiguration dieser Pipelines legen wir auf zwei Aspekte großen Wert: Geschwindigkeit und Reproduzierbarkeit des Builds.

Geschwindigkeit ist für uns wichtig, da wir binnen weniger Minuten Rückmeldung über die Qualität der Änderungen erhalten wollen. Wenn hier etwas bricht, wollen wir den Fehler sofort korrigieren, da diese Behebungen meist einfacher sind als bei großen Änderungen.

Geschwindigkeit erreichen wir durch konfektionierte Docker-Images. Außerdem stimmen wir die Reihenfolge der Tests aufeinander ab und verfolgen beim Schreiben der Tests einen »fail fast«-Ansatz. Wenn ein Build fehlschlägt, bekommen wir so früh Rückmeldung.

Die Reproduzierbarkeit eines Builds ist für uns wichtig, damit wir Builds nachvollziehen können, indem wir die Docker-Images auf beliebigen Systemen bauen. In der Vergangenheit hatte uns Gitlab öfter Fehler in Builds gemeldet, die wir dann lokal auf unseren Entwicklungsrechnern nicht nachvollziehen konnten. Grund hierfür war die Installation von Abhängigkeiten und dadurch entstandene Versionsunterschiede.

Die Reproduzierbarkeit erhalten wir, indem wir so gut es geht die Versionen der Abhängigkeiten angeben und während des Builds keine Komponenten nachinstallieren. Da Installationen entfallen, erhöht sich als Nebenwirkung auch die Build-Geschwindigkeit.

Dist::Zilla

Für uns hat sich Dist::Zilla als sehr hilfreich erwiesen. Einerseits ermöglichen uns die Kommandos dieses Werkzeugs, die Konfiguration einer Pipeline übersichtlich zu halten. Andererseits bietet die Funktionalität von dzil vieles, was wir sinnvoll einsetzen können.

Kommandos, die wir nutzen, sind zum Beispiel »test« für das Ausführen der Tests, »build« zum Erstellen der Distribution und »cover« zum Messen der Testabdeckung.

Wir setzen Dist::Zilla ein, um am Ende eines Builds die Distribution als Artefakt »an den Build zu hängen«. Das von dzil erstellte Archiv wird einem Build zugeordnet; wir können es dann im Browser, in anderen Projekten oder über die API übertragen.

Auch bei der Versionsverwaltung ist Dist::Zilla nützlich: Sein git-Bundle hilft dabei, Branches nach dem Veröffentlichen einer Distribution zu taggen. Somit ist automatisiert und nachvollziehbar im Git-Repository eine Versionshistorie abrufbar.

Zusammenfassung

Das Produkt Gitlab hilft einem Team beim fortlaufenden Integrieren von Software-Komponenten. Entwickler können »Pipelines« über Textdateien beschreiben, in denen Befehle nach Änderungen einer gemeinsamen Code-Basis ausgeführt werden. Wir können damit schnelle Rückmeldungen über Code-Änderungen und reproduzierbare Builds erhalten. Dist::Zilla hilft uns, einen Build übersichtlich zu beschreiben und effizient durchzuführen.

Weiterlesen


Permalink:

Perl-Schulungen vor dem Deutschen Perl-/Raku-Workshop 2021 in Leipzig

10.11.2020 // Renée Bäcker

Auch während der aktuell hohen Infektionszahlen in der Corona-Pandemie schauen wir nach vorne. Nach aktuellem Stand findet der Deutsche Perl-/Raku-Workshop 2021 Ende März in Leipzig statt (sollte die Corona-Situation das nicht hergeben, wird da mit Sicherheit reagiert).

Wir wollen den Workshop zum Anlass nehmen, am Tag vorher (23. März 2021) zwei Halbtagesschulungen anzubieten:

Den Tag beginnen wird Gregor mit Gitlab und Perl. In dem Kurs, der von 08:00 Uhr bis 12:00 Uhr stattfinden wird, geht es um die Entwicklung von Perl-Distributionen mit Hilfe der Plattform Gitlab. Gregor wird anhand einer CPAN-Distribution kurz die hierfür relevanten integrierten Komponenten der Plattform vorstellen und insbesondere auf die continuous integration eingehen. Ein Schwerpunkt liegt hier auf der schnellen Rückmeldung an Entwickler nach einem Commit.

Am Nachmittag (13:30 Uhr bis 17:30 Uhr) geht es dann um REST-APIs mit Mojolicious. Ich werde hierbei kurz auf REST an sich eingehen, bevor eine Schnittstelle definiert und dann als Mojolicious-Anwendung mit Leben gefüllt wird. Dabei werden dann auch Themen wie Sicherheit etc. angesprochen. Zum Abschluss wird die Schnittstelle noch getestet.

Als Veranstaltungsort ist das Hotel Michaelis geplant und die Teilnehmerzahl ist auf 10 Personen pro Schulung beschränkt.

Wer Interesse an einer der Schulungen hat, kann sich gerne bei uns melden. Die Teilnahme an einer Schulung kostet 200,00 € netto (238,00 € inkl. MwSt), ein Kombiticket für beide Schulungen kostet 350,00 € netto (416,50 € inkl. MwSt).

Wir werden natürlich die Entwicklungen bzgl. Corona weiter im Auge behalten. Sollte eine Präsenzveranstaltung nicht möglich sein, werden wir die Schulungen als Online-Veranstaltung anbieten.


Permalink:

Optimierung von Docker-Images

14.10.2020 // Gregor Goldbach

Wenn man als Entwickler mit der Erstellung von Docker-Images anfängt, dann ist das Image zunächst auf Funktionalität optimiert. Mit fortschreitender Entwicklung wird eine schnelle Rückmeldung über das Build-Ergebnis an die Entwickler wichtiger. Zudem soll ein Build reproduzierbar sein, damit von der CI-Umgebung gemeldete Fehler nachvollzogen und Änderungen für den Betrieb einplant werden können. Dieser Artikel zeigt, worauf man bei achten sollte, damit das klappt.

Zuerst: Funktionalität

Wir nutzen Gitlab und haben uns zu Beginn der Nutzung dieses Dienstes darauf konzentriert, überhaupt einen Build nach einem Commit durchlaufen zu lassen. Das ist manchmal gar nicht so einfach, da diverse Abhängigkeiten benötigt werden, die vor Ausführung von Tests in einem laufenden Container installiert werden müssen.

Wir bei Perl-Services haben uns zu Beginn auf die CI-Stufe von Gitlab verlassen. Da es hier aber mitunter etwas dauern kann, bis ein Build nach einem Commit startet, haben wir schnell gemerkt, dass lokales Bauen von Images sinnvoller ist. Dafür haben wir dann ein Makefile erstellt:

#!/bin/make -f
#
# Makefile pointers...
# Self-documenting:
#    https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
# Argument passing:
#    https://stackoverflow.com/questions/2826029/passing-additional-variables-from-command-line-to-make

#
# To add a new option, follow the format below, e.g.:
#     make command: ## Documentation of command
#             ./the-command-to-run.sh

# Default to displaying the help
.DEFAULT_GOAL := help

.PHONY: help

REGISTRY=registry.gitlab.com
IMAGE_NAME=perlservices/groupname/projectname-docker
IMAGE_TAG=latest
FULL_IMAGE_NAME=${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}

# Displays all the make options and their descriptions
help:
    @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

build: ## Build Docker image
    docker build -t ${FULL_IMAGE_NAME} .

shell: ## Start a container from the image built and run a shell in it
    docker run --rm -it --entrypoint /bin/bash \
        -v $$PWD/src:/src \
        -p 8080:8080 \
        ${FULL_IMAGE_NAME}

Dieses Makefile erstellt beim Aufruf von make build ein Docker-Image anhand des im gleichen Verzeichnisses liegenden Dockerfiles. Der Aufruf von make shell startet einen Container mit diesem Image und öffnet eine Shell darin, wobei hier das lokale Verzeichnis src im Container unter /src zur Verfügung steht.

Mit Hilfe dieses Makefiles können wir recht einfach ein Docker-Image erstellen und dieses lokal weiterentwickeln.

Schnelle Rückmeldung durch kurze Durchlaufzeit

Damit eine Perl-Distribution im Rahmen eines Docker-Images erstellt werden kann, werden viele Bausteine benötigt.

Einerseits benötigen wir auf Systemebene einige Werkzeuge. Dazu gehört zum Beispiel curl, um andere Builds auf Gitlab anzustoßen. Diese installieren wir über den Paketmanager des Betriebssystems.

Anderseits brauchen wir auf Anwendungsebene Module, die wir installieren müssen. Wenn sie in der geeigneten Version als Paket des Betriebssystems vorliegen, installieren wir sie ebenfalls mit dem Paketmanager des Betriebssystems. Wenn dies nicht der Fall ist, installieren wir sie von CPAN.

Diese Installationen dauern entsprechend lange und greifen auf externe Quellen zu. Dadurch dauert der Build lange und kann schlimmstenfalls fehlschlagen.

Daher haben wir hier in zwei Schritten optimiert: Wir erstellen für eine zu entwickelnde Perl-Distribution (oder Anwendung) ein spezifisches Image, das alle benötigten Abhängigkeiten enthält.

Im Dockerfile installieren wir zunächst mit apt-get install die benötigten Pakete des Betriebssystems:

FROM registry.gitlab.com/perlservices/groupname/projectname-docker:latest

RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
  lsb-core \
  wget

# Add PostgreSQL repo for the matching release of the OS
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | tee  /etc/apt/sources.list.d/pgdg.list

RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
  postgresql \
  postgresql-client \
  git \
  apache2 \
  curl \
  phppgadmin \
  php-pgsql \
  cpanminus \
  libdbd-pg-perl \
  libnet-ssleay-perl \
  libxml-parser-perl \
  gosu \
  ...

Die Perl-Abhängigkeiten legen wir in einem cpanfile fest:

...
requires 'Mojolicious' => 8;
requires 'Mojolicious::Plugin::Bcrypt' => 0;
requires 'Mojolicious::Plugin::Status' => 0;
requires 'Moo' => 0;
requires 'MySQL::Workbench::DBIC', '>= 1.13';
requires 'MySQL::Workbench::Parser', '>= 1.06';
...

Das so erstellte Image verwenden wir dann in der .gitlab-ci.yaml unseres Projektes. So können dann sofort nach einem Commit in der CI-Stufe die konfigurierten Befehle ausgeführt werden, denn alle Abhängigkeiten sind bereits im Image vorhanden.

Reproduzierbare Builds durch genaue Versionsangaben

Gerade zu Beginn der Arbeit mit den oben beschriebenen projektspezifischen Docker-Images haben wir öfter Probleme mit der Nachvollziehbarkeit von Fehlern im Build auf Gitlab gehabt. Ursache dafür war, dass sich Abhängigkeiten in jedem Build geändert haben.

Beispiele für diese Änderungen:

  • Eine Perl-Distribution wird auf CPAN zwischen zwei Builds aktualisiert.
  • Lokal ist eine indirekte Abhängigkeit in einer anderen Version installiert.

Es wird dann unter Umständen auf Gitlab ein Fehler im Build angezeigt, der lokal beim Entwickler nicht mehr oder anders auftritt.

Durch das Erstellen von projektspezifischen Docker-Images werden schon viele Abhängigkeiten festgezurrt, so dass hier weitestgehend reproduzierbare Builds umgesetzt werden konnten.

Eine wesentliche Abhängigkeit ist jedoch noch nicht festgezurrt: Die des grundlegenden Images für alle Builds – Das verwendete Betriebssystem.

Vielfach wird in Beispielen für ein Dockerfile entweder kein Tag bei einem verwendeten Image angegeben oder eines mit dem Namen »latest«. Beide führen dazu, dass jeweils die letzte Fassung eines Images verwendet wird:

FROM debian:latest

Im schlimmsten Fall kann es dadurch dazu kommen, dass zwischen zwei Builds ein Versionssprung des Betriebssystems stattfindet und alle abhängigen Images neu gebaut werden. In der Regel läuft dies nicht ohne Probleme ab und zieht weitere Arbeiten nach sich.

Die dann auftretenden Probleme im Build können im günstigsten Fall eine kurze Verzögerung bedeuten, im schlimmsten Fall ungeplante Wartungsarbeiten nach sich ziehen.

Außerdem bedeuten gerade bei Betriebssystemen neue Versionen größere Änderungen im Betrieb, die sorgfältig vorbereitet und eingeplant werden müssen.

Diese Art von Überraschung kann umgangen werden, indem in Dockerfiles bei jedem verwendeten Image stets ein Tag mit angegeben wird, das eine feste Version spezifiziert.

Es sollte also auf Angaben wie »rolling« und »latest« verzichtet werden.

FROM ubuntu:20.04

Zusammenfassung

Während Entwickler zunächst auf Funktionalität bedacht sind, wenn sie in CI-Umgebungen mit dem Erstellen von Docker-Images beginnen, zeigt sich sehr schnell in der täglichen Arbeit, dass weitere Faktoren wichtig sind. Schnelle Rückmeldung durch kurze Durchlaufzeiten und stabiler Betrieb durch reproduzierbare Builds werden mit zunehmender Zeit wichtiger.

Dieser Artikel hat beschrieben, wie durch projektspezifische Images die Durchlaufzeit verringert werden kann. Außerdem wurde gezeigt, wie durch das ausdrückliche Konfigurieren von Softwareversionen planvolle Updates ohne große Überraschungen im Betrieb möglich werden.


Permalink:

Wie nutzen wir Gitlab?

01.09.2020 // Gregor Goldbach

Gitlab ist eine webbasierte Plattform für die kontinuierliche Auslieferung von Software. Diese Plattform bietet viele Funktionen, die über die reine Softwareentwicklung und das Deployment hinausgehen. Wir erstellen damit Perl-Distributionen, Docker-Images sowie Dokumentation.

Hauptteil

Gitlab ist eine webbasierte Plattform für die kontinuierliche Auslieferung von Software. Unter anderem integriert es Komponenten wie ein Wiki, ein Ticket-System und die Unterstützung des Versionskontrollsystems Git. Außerdem bietet es eine automatisierte Ausführung von beliebigem Code in Docker-Images an, die von Commits ausgelöst werden können.

Welche Befehle ausgeführt werden, kann man in einer YAML-Datei beschreiben. Dort wird auch beschrieben, welches Docker-Image dafür verwendet werden soll und unter welchen Bedingungen die Befehle ausgeführt werden sollen.

Wie wir bei Perl-Services diese Möglichkeiten nutzen, beschreibe ich in diesem Artikel.

Automatisiertes Erstellen von Perl-Distributionen

Als wir begonnen haben, Gitlab zu evaluieren, haben wir zunächst kleinere Perl-Distributionen als Projekt angelegt und die Git-Repositorys eingebunden. In den YAML-Dateien dieser Projekte haben wir dann konfiguriert, dass nach jedem Commit die Unit-Tests ausgeführt werden und zum Abschluss die Perl-Distribution paketiert werden sollen. Hier ein Auszug aus einem mittlerweile so nicht mehr existenten Projekt:

image: ubuntu:rolling

build:
  script:
    - apt-get update -qq
    - cpanm --installdeps .
    - dzil authordeps | cpanm
    - dzil listdeps | cpanm
    - dzil build

artifacts:
    when: always
    name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
    paths:
      - *.tar.gz

Unser Ansatz war zunächst, Fehler durch Tests zu finden und bei erfolgreichem Testdurchlauf die Perl-Distribution zu erstellen. Die benötigten Abhängigkeiten haben wir mit cpanm installiert. Dabei haben wir festgestellt, dass wir gerne schneller Rückmeldung erhalten würden. Dies führte dann dazu, dass wir die Build-Pipeline optimiert haben.

Verkürzte Durchlaufzeiten erreichen wir einerseits durch eine Anpassung der Befehle, andererseits aber auch dadurch, dass wir ein Docker-Image verwenden, das für diese Distribution optimiert ist und bereits alle Abhängigkeiten enthält.

Was sind die nächsten Schritte? Wir prüfen, ob wir die erfolgreich gebauten Distributionen automatisiert in einen Pinto-Server laden oder auf CPAN veröffentlichen.

Erzeugen von Docker-Images

Im vorherigen Abschnitt habe ich erwähnt, dass wir Docker-Images für die Entwicklung von Perl-Distributionen optimieren. Wir wollen dafür alle Abhängigkeiten einer Perl-Distribution in einem Docker-Image zur Verfügung stellen, damit wir die Durchlaufzeit der Perl-Distribution klein halten und dann schnell Rückmeldung nach einem Commit bekommen.

Wie haben wir dies mit Gitlab erreicht? Wir setzen Dist::Zilla ein und beschreiben Abhängigkeiten über dessen Konfiguration dist.ini und das cpanfile. Im verwendeten Docker-Image werden diese darüber beschriebenen Abhängigkeiten mit cpanm installiert.

Dafür haben wir Gitlab so konfiguriert, dass bei einer Änderung dieser Dateien im Master-Branch der Perl-Distribution das verwendete Docker-Image neu erstellt wird. Hier ein beispielhafter Auszug aus einer Gitlab-Konfiguration:

trigger_image_build:
  stage: trigger
  script:
    - curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master https://gitlab.com/api/v4/projects/9398220/trigger/pipeline
  only:
    refs:
      - /^master$/
    changes:
      - cpanfile
      - dist.ini

Die Beschreibungen der Abhängigkeiten werden beim Erstellen des Docker-Images aus dem abhängigen Projekt gelesen. Hier ein beispielhafter Auszug aus einem Dockerfile:

RUN cd /home/mydocker/src && \
  git clone https://gitlab-ci-token:${CI_TOKEN}@gitlab.com/perlservices/project.git && \

[...]
  
RUN cd /home/mydocker/src/project && \
   cpanm --skip-satisfied --installdeps . && \
   dzil authordeps --missing | cpanm --skip-satisfied && \
   dzil listdeps | cpanm --skip-satisfied

Die Gitlab-Dokumentation hat uns hier teilweise eher verwirrt als geholfen, aber letzten Endes haben wir ein perfekt abgestimmtes Image erhalten, in dem alle Abhängigkeiten enthalten sind. Dadurch können in dem Projekt, das auf diesem Image basiert, nach einem Commit sofort die Unit-Tests ausgeführt werden können.

Die Durchlaufzeiten sind daher sehr gering und liegen bei einem gehosteten Gitlab-Repository bei etwa drei Minuten. Der Löwenanteil dieser drei Minuten liegt darin begründet, dass wir auf einen freien Gitlab-Runner warten müssen.

Um die Durchlaufzeiten weiter zu verringern, migrieren wir die Repositorys derzeit auf einen eigenen Gitlab-Server.

Hinweis: Das müssen wir wegen dem Fall des Privacy Shields ohnehin machen 🙂

Dokumentation erstellen

Die in Projekten zu erstellende Dokumentation reicht je nach Wunsch des Kunden von kurzen Beschreibungen über HTML-Dateien bis zu sauber mit LaTeX gesetzten Dokumenten. Das Ausgangsformat der Dokumente ist für uns stets Markdown, da dies einfach zu schreiben ist, wir uns auf das Wesentliche konzentrieren können und alle gewünschten Zielformate erzeugt werden können.

In Gitlab erzeugen wir mit pandoc aus den Quelldateien das Zielformat. Gegebenenfalls verarbeiten wir das Resultat automatisiert weiter. Wenn als Ergebnis beispielsweise ein PDF erzeugt werden soll, dann bedeutet dies, dass wir aus Markdown LaTeX-Dokumente erzeugen und diese im Anschluss mit lualatex in ein PDF wandeln. Damit dies möglich ist, haben wir uns ein Docker-Image erstellt, in dem TeX Live installiert ist.

Da wir lualatex aus TeX Live einsetzen, ist unser Docker-Image mehrere Gigabyte groß. Diese Kröte müssen wir wohl schlucken. Pandoc leistet sehr gute Dienste und die Entscheidung für Markdown als Ausgangsformat hat sich bewährt.

Wir werden diese Kombination beibehalten, jedoch gelegentlich prüfen, ob wir das Docker-Image etwas verkleinern können 🙂

Zusammenfassung

Wir erstellen mit Gitlab automatisiert Perl-Distributionen, Docker-Images und Dokumente in unterschiedlichen Formaten. Gitlab als Plattform hat sich hierfür bewährt. Durch den Einsatz einer selbst betriebenen Gitlab-Instanz erhoffen wir uns geringere Durchlaufzeiten.

Links


Permalink: