Aus Linux-Magazin 01/2018

Continuous Integration mit Docker und Gitlab

© hamik, 123RF

Mit Containern lässt sich kritische Infrastruktur zuverlässig und reproduzierbar betreiben. Gitlab steuert die perfekte Umgebung bei, um Docker-Container zu produzieren.

Docker mit seinem fulminanten Aufstieg steht stellvertretend für den Aufschwung der Containertechnik unter Linux. Denn erst der Erfolg von Docker machte den Erfolg von Kubernetes überhaupt möglich, also jener von Google angebotenen Orchestrierung für Container, die geradezu als eine Art Allheilmittel für alle Herausforderungen der modernen IT vermarktet wird.

Docker ist längst zum Quasi-Standard geworden, wenn es darum geht, wie sich Anwendungen an Anwender ausliefern lassen. Dabei wildern Container sogar im Metier der klassischen Paketverwaltungen, so zum Beispiel Canonicals Snap, dem der Durchbruch bisher allerdings noch nicht gelungen ist.

Wenn es um das effiziente Verteilen von Anwendungen geht, also klassisches Platform as a Service (PaaS) oder Software as a Service (SaaS), gehört Docker mittlerweile zum Standardrepertoire. Wer den Nutzen von Docker-Containern aber auf diese Anwendungsgebiete beschränkt, tut Docker und Containern allgemein Unrecht. Denn sinnvolle Einsatzszenarien für Container-Virtualisierung ergeben sich regelmäßig auch an Stellen, an denen man eigentlich gar nicht mit ihnen rechnet. Ein Beispiel, das heraussticht, ist die Deep Infrastructure.

Was es am Laufen hält

Unter Deep Infrastructure verstehen Admins in der Regel die Komponenten eines Setups, die sich um den Betrieb absolut grundlegender Dienste kümmern. Als Beispiel stelle man sich eine Cloudinstallation vor, die auf Open Stack aufbaut. Damit der Anwender per API-Aufruf virtuelle Maschinen starten kann, müssen im Hintergrund viele verschiedene Dienste zusammenarbeiten. Jeder Server benötigt etwa eine passende Netzwerkkonfiguration samt richtiger IP-Adressen, die gerade in großen Setups realistisch nur per DHCP machbar ist. Das impliziert aber den Betrieb eines DHCP-Servers.

Auch NTP ist eine klassische Standardanforderung, weil die Uhren aller Server bis auf Bruchteile einer Sekunde synchron sein sollten. Diverse Dienste wie auf AMQP aufbauende Message-Queues oder verteile Storagesysteme quittieren sonst in Windeseile den Dienst.

Hinzu kommt, dass riesige Cloudinstallation ein valides Konzept brauchen, um in kurzer Zeit schnell in die Breite skalieren zu können. Das funktioniert nur, wenn sich der manuelle Aufwand für die Installation neuer Server auf ein Minimum beschränkt. Ist ein Server samt der benötigten IP-Adressen sowie eventuell relevanter Credentials im Inventarisierungssystem eingetragen, muss es genügen, ihn im Rack zu montieren und einzuschalten.

Der ganze Rest, angefangen bei der Durchführung eventueller Wartungsaufgaben über die nötige Installation eines Betriebssystems bis hin zum Ausrollen der Dienste für die eigentliche Cloud, muss komplett automatisch ablaufen. Für diesen Workflow sind aber weitere Dienste aus der Deep-Infrastructure-Kategorie nötig, etwa TFTP. Gern genutzt auf solchen Systemen wird zudem ein lokaler Spiegelserver der Distribution, die zum Einsatz kommt: Er ist schneller und verursacht in Summe viel weniger externen Traffic als ein externer Server.

Der konventionelle Ansatz

Der klassische Ansatz wäre, alle genannten Dienste auf zentralen Servern zu installieren, mit den entsprechenden Konfigurationsdateien zu versehen und sie dort – womöglich mit Pacemaker & Co. für Hochverfügbarkeit – zu betreiben. Sonderlich elegant ist das aber nicht: Wer in der Lage sein will, den Ausfall eines solchen Knotens schnell zu kompensieren, müsste zunächst viel Zeit in die Automatisierung mit Ansible oder anderen Tools stecken.

Zudem müsste er den Wartungszyklen des Distributors folgen, dessen Linux-Produkt auf den eigenen Servern läuft. Geht ein Update schief, zieht das im schlimmste Fall die gesamte Boot-Infrastruktur in Mitleidenschaft.

Wie es auch anders geht

Ein anderer Ansatz besteht darin, die einzelnen Dienste wie NTP oder »dhcpd« in Form einzelner Container auf einem simplen Basis-Linux auszurollen, etwa auf Container Linux [1], dem ehemaligen Core OS. Auf der Boot-Infrastruktur läuft dann ein minimales Grundsystem, das im Betrieb Container aus einer zentralen Registry herunterlädt und diese einfach ausführt. Will der Admin einzelne Dienste aktualisieren, installiert er einen neuen Container für das jeweilige Programm.

Will der Admin die Boot-Infrastruktur komplett neu aufsetzen, weil es beispielsweise wichtige Updates für Container Linux gibt, installiert er ein neues Container Linux, das nach dem ersten Start automatisch alle relevanten Container erneut herunterlädt und wieder ausführt. Container Linux bietet mit Ignition [2] sogar ein Konfigurationsframework, über das sich entsprechende Anweisungen automatisch erteilen lassen.

Die Container freilich müssen so vorbereitet sein, dass sie die benötigte Konfiguration sowie alle relevanten Daten bereits enthalten. Für Updates soll es zudem möglich sein, einen Container jederzeit und ohne viel Aufwand neu zu bauen. Das allerdings ist leichter gesagt als getan: De facto muss der Admin für diesen Workflow nämlich ein umfassendes System für Continuous Delivery und Continuous Integration (CI) bauen, mit dem er seine Docker-Container zuverlässig regeneriert.

Dass es nur eine mittelgute Idee ist, auf fertige Docker-Container etwa von Docker Hub zu setzen, hat das Linux-Magazin schon mehrere Male erklärt: Die dort zu findenden Container sind meist Blackboxes, deren Entstehungsprozess komplett im Dunkeln liegt. Im Herzen seiner Infrastruktur möchte man das nicht haben.

Null Problemo

Die gute Nachricht: Es ist leicht, auf Basis der offiziellen Container der verschiedenen Hersteller eigene Container für Docker zu bauen. Praktisch jede gängige Linux-Distribution bietet offizielle Docker-Abbilder an, solche gibt es etwa für Ubuntu, Debian oder Centos. Die Images finden sich im Docker Hub, ihre Quellen sind frei verfügbar, sodass jeder Nutzer sie herunterladen kann.

Baut der Admin mit seinen Containern auf diesen Vorgaben auf, ist der Rest ein leichtes Unterfangen. In Form eines Dockerfile genannten Drehbuchs legt er fest, welche Änderungen das Basis-Image braucht. Das so erstellte Abbild lässt sich danach in einer privaten Docker-Registry hinterlegen, von wo die Ziel-Hosts es abrufen und nutzen.

Als ausgesprochen nützlich erweist sich in diesem Kontext Gitlab: Es bringt eine eigene Docker-Registry mit und bietet dem Admin darüber hinaus viele Werkzeuge an, um echte Continuous Integration zu erreichen. Ein Beispiel sind die Pipelines: Wer im Verzeichnis seines Docker-Containers entsprechende Tests hinterlegt, sorgt dafür, dass Gitlab diese Tests bei jedem Commit in das Git-Verzeichnis automatisch aufruft, nachdem es den Docker-Container erneut zusammengestellt hat.

Verlaufen die Tests erfolgreich, kopiert Gitlab den neuen Container gleich in die hauseigene Docker-Registry, von wo aus er sich unmittelbar nutzen lässt. Zwischen dem Commit des Entwicklers und der Verfügbarkeit des Containers vergehen also im Idealfall nur wenige Sekunden – und händisches Eingreifen ist auch nicht nötig.

Im Folgenden stellt dieser Artikel den kompletten Docker-Workflow auf Basis von Gitlab im Detail vor. Neben den notwendigen Anpassungen bei Gitlab geht es um die benötigten Dateien in einem Docker-Container-Verzeichnis in Gitlab.

Schritt 1: Docker-Registry in Gitlab

Ein funktionierender Continuous-Integration-Ablauf für Docker-Container in Gitlab setzt zwei funktionierende Komponenten voraus. Das Gitlab-CI-System gehört zwar zum Standardumfang von Gitlab, benötigt jedoch einen so genannten Runner, damit es CI-Aufgaben tatsächlich ausführen kann. Hinzu kommt die Docker-Registry. Sie ist das Ziel des CI-Vorgangs, denn in ihr landet zum Schluss des Prozesses das fertige Docker-Abbild.

Schritt 1 auf dem Weg zu Docker-CI mit Gitlab besteht deshalb darin, die Docker-Registry in Gitlab zu aktivieren. Dabei geht das folgende Beispiel davon aus, dass Gitlab in der Community-Edition und auf Basis der Omnibus-Variante von Gitlab zum Einsatz kommt. Um die Registry zu aktivieren, genügen drei Zeilen in »gitlab.rb« im Verzeichnis »/etc/gitlab«:

registry_external_url 'https://gitlab.example.com:5000'
registry_nginx['ssl_certificate'] = /Pfad/zu/certificate.pem
registry_nginx['ssl_certificate_key'] = /Pfad/zu/certificate.key

Hat der Admin das Zertifikat für Gitlab und den SSL-Schlüssel bereits in »/etc/gitlab/ssl/hostname.crt« und »/etc/gitlab/ssl/hostname.key« hinterlegt, entfällt die Pfadangabe für Nginx im Beispiel, sodass nur die erste Zeile des Codeblocks übrig bleibt. Das Beispiel geht davon aus, dass die Docker-Registry von Gitlab denselben Hostnamen wie Gitlab selbst, aber einen anderen Port benutzt. Außerdem ist die Gitlab-Docker-Registry ab Werk per SSL abgesichert.

Ist »gitlab.rb« um die entsprechenden Zeilen erweitert, stößt der Admin das erneute Generieren der Gitlab-Konfiguration mittels Chef an, das bei Gitlab in der Omnibus-Edition zum Lieferumfang gehört: »sudo gitlab-ctl reconfigure« gefolgt von »sudo gitlab-ctl restart« erledigen das. Mittels »netstat -nltp« lässt sich anschließend überprüfen, ob auf dem Port 5000 nun Nginx lauscht. Auf einem Host, auf dem Docker installiert ist, lässt sich außerdem mit »docker login gitlab.example.com:5000« versuchsweise eine Verbindung mit der Docker-Registry von Gitlab herstellen.

Übrigens: Gitlab speichert die Docker-Abbilder in der Standardkonfiguration im Verzeichnis »/var/opt/gitlab/gitlab-rails/shared/registry«. Möchte der Admin das ändern, genügt der Parameter »gitlab_rails[‘registry_path’] = “/Pfad/zum/Ordner”« in »gitlab.rb«, gefolgt von einer erneuten Generierung der Gitlab-Konfiguration und einem Neustart.

Schritt 2: Runner aktivieren

Ohne zumindest einen Runner geht bei Gitlab nichts: Ein Runner ist bei Gitlab so etwas wie ein entfernter Befehlsempfänger, der auf einem System läuft und auf Anweisungen aus der Gitlab-CI-Suite wartet. Er ist dafür verantwortlich, den gesamten CI-Prozess abzubilden, baut also auch das Image und führt die definierten Tests aus. Wer das Gitlab-CI noch nicht genutzt hat, hat meist keinen aktiven Runner – das Erstellen eines solchen Runners ist folglich der nächste Schritt auf dem Weg zu automatischen Docker-Containern.

Der Runner darf durchaus auf demselben Host laufen wie Gitlab selbst, eleganter ist freilich, wenn der Runner über ein eigenes System verfügt und sich seine Ressourcen nicht mit anderen Diensten teilen muss. Zudem muss auf dem Runner-Host die Community-Edition von Docker installiert sein, sodass etwa das Kommando »docker« funktioniert. Danach besorgt der Admin sich im ersten Schritt ein Authentifizierungstoken für den neuen Runner: In Gitlab klickt er dazu in der Admin-Seite auf »Overview | Runners«, wo er ein entsprechendes Token findet (Abbildung 1).

Abbildung 1: Im Admin-Abschnitt von Gitlab organisiert sich der Admin ein Token für neue Runner und verwaltet die bestehenden.

Abbildung 1: Im Admin-Abschnitt von Gitlab organisiert sich der Admin ein Token für neue Runner und verwaltet die bestehenden.

Danach erfolgt die Paketinstallation: Die Gitlab-Entwickler stellen dafür ein Skript online zur Verfügung, das unter [3] für Debian-basierte Systeme und unter [4] für RPM-basierte Systeme mit Ausnahme von Open Suse zu finden ist. Wer mutig ist, lädt das Skript mittels »curl -L« herunter und führt es per »| sudo bash« gleich aus. Wer in das Skript zunächst hineinschauen will, lädt es herunter und führt es danach als Root händisch aus. Am Ende des Vorgangs ist das Paket »gitlab-runner« installiert.

Schließlich steht noch die Registrierung des Runners in Gitlab an: Der Befehl »sudo gitlab-runner register« leitet den Prozess ein. Zunächst fragt der Assistent des Programms nach der Gitlab-URL, danach nach dem Token aus dem vorangegangenen Schritt. Den Hostnamen trägt der Admin entsprechend ein, die Frage nach eventuellen Tags beantwortet der Admin mit »docker« und anschließend die nach der Ausführung von »Untagged jobs« mit »true«.

Die Frage nach dem Festnageln auf das aktuelle Projekt hingegen quittiert der Admin mit »false«. Als Executor gibt er schließlich noch »shell« ein – kompliziertere Setups würden den Gitlab-Runner hier direkt mit Docker über den eigens dafür vorgesehenen Executor verbinden, das ist in diesem Basisbeispiel jedoch nicht nötig.

Am Ende des Vorgangs taucht der neue Runner in Gitlab auf: Dort, wo sich der Admin zuvor das Token besorgt hat, ist nun ein neuer Eintrag zu finden. Hier lassen sich diverse Parameter des Runners übrigens im Nachhinein auch zu jedem Zeitpunkt noch verändern. Geht also bei der initialen Einrichtung des Runners etwas schief, ist die falsche Konfiguration nicht in Stein gemeißelt.

Schritt 3: Gitlab-CI für Projekt aktivieren

Der letzte Schritt der Gitlab-Vorbereitung besteht nun darin, das CI-Framework für das Beispielprojekt zu aktivieren. Dazu legt der Admin zunächst in Gitlab ein neues Projekt an und navigiert anschließend zu dessen Einstellungen. Dort findet sich der Menüpunkt »Settings | CI/CD«. Ein Klick auf »Expand« neben »General Pipeline Settings« sorgt dafür, dass sich die wichtigsten Konfigurationseinstellungen öffnen. Hier setzt der Admin den Wert »Git strategy for pipelines« auf »git clone«.

Danach klickt er auf »Settings« und anschließend rechts auf »Permissions«. Ein Klick auf »Expand« macht die Projekteinstellungen sichtbar – hier setzt er den Wert »Container Registry« auf »Enabled« (Abbildung 2). Damit ist das Gitlab-CI-Framework für das Beispielprojekt scharfgeschaltet. Noch gibt es allerdings keinen Trigger, der eventuelle CI-Automatismen auslöst. Das ändert sich im letzten Schritt.

Abbildung 2: Wenn die Gitlab-Docker-Registry aktiviert ist, setzt der Admin sie für spezifische Projekte innerhalb von Gitlab ein.

Abbildung 2: Wenn die Gitlab-Docker-Registry aktiviert ist, setzt der Admin sie für spezifische Projekte innerhalb von Gitlab ein.

Schritt 4: CI-Pipelines definieren

Gitlab erwartet die CI-Konfiguration für ein Projekt als Yaml-Datei namens ».gitlab-ci.yml« im Hauptverzeichnis des Projekts. Inhaltlich und auch vom Prinzip her ähnelt die gewünschte Yaml-Datei einem Makefile: In ihr legt der Admin fest, aus welchen Teilen (Stages) der CI-Prozess besteht und welche Jobs zu diesen Stages jeweils gehören. Praktisch: Weil Gitlab für die Durchführung der einzelnen Arbeitsschritte in den Stages extern einfach nur eine Shell aufruft, lassen sich die einzelnen Stages in Form von Shellskripten definieren, auf die der Admin in ».gitlab-ci.yml« dann einfach verweist. Das hilft dabei, die Yaml-Datei selbst übersichtlich zu halten.

Grundsätzlich lassen sich die Namen der einzelnen Jobs frei wählen, es gibt allerdings ein paar reservierte Keywords in der Yaml-Hierarchie von Gitlab-CI. Unter »stages« etwa erwartet Gitlab eine Liste der CI-Stages des Projekts. Mittels »before_script« und »after_script« akzeptiert Gitlab Befehle, die vor dem und nach dem Ausführen einzelner Jobs definiert sind. Variablen lassen sich mit der Direktive »variables« festlegen. Genauere Details sind der Gitlab-Dokumentation [5] zu entnehmen.

Im Beispiel besteht ».gitlab-ci.yml« aus den Stages: »pre_cleanup«, »build«, »test«, »upload« und »post_cleanup«, die auch in »stages« definiert sind. Danach erfolgt die Definition der Jobs:

Name:

script: <§§I>Befehl<§§I>

stage: <§§I>Abschnitt<§§I>

tags:

– dock

Der Name eines Jobs kann, muss aber nicht identisch sein mit dem Abschnitt des Build-Vorgangs, zu dem der Job gehört. Es empfiehlt sich allerdings, denn es trägt zur Übersichtlichkeit ganz erheblich bei. Der komplette »build«-Job könnte wie folgt aussehen:

build:

script: “bash -x build.sh”

stage: build tags: – docker

Dieser Eintrag in ».gitlab-ci.yml« führt dazu, dass Gitlab-CI jedes Mal den Befehl »bash -x build.sh« aufruft, wenn es den »build«-Abschnitt im CI-Prozess abarbeitet. Die eigentlichen Befehle, die zum Anlegen des Image für Docker nötig sind – ganz konkret also »docker build …« –, stehen dann in »build.sh«, was dieser Lösung Eleganz verleiht und dabei hilft, Chaos in ».gitlab-ci.yml« zu vermeiden. Die Umgebung eines Jobs lässt sich übrigens dynamisch festlegen, was weitere Vereinheitlichung ermöglicht.

Es ist beispielsweise sinnvoll, ein Shellskript namens ».gitlab-ci.config« anzulegen, das die Umgebungsvariablen »IMAGE«, »IMAGE_NAME« sowie »VERSION« in die Shell-Umgebung exportiert. Holt sich der Admin diese Umgebungsvariablen anschließend mittels »./.gitlab-ci.config « im ersten Stage des CI-Prozesses in die Shell, haben alle weiteren Befehle dieses Jobs Zugriff auf sie. Dabei ist es wichtig, dass eine »script«-Direktive in einer Job-Beschreibung auch mehrere Befehle aufrufen kann, wenn das in Yaml-Notation passiert.

Analog zu dem Beispiel für »build« fügt der Admin in ».gitlab-ci.yml« weitere Jobs für die anderen Stages ein, wobei jeder Abschnitt ein eigenes Shellskript bekommt (»pre_cleanup.sh«, »test.sh«, »upload.sh« und schließlich »post_cleanup.sh«). Die letzte interessante Frage ist vor diesem Hintergrund, was der Inhalt eben jener Shellskripte sein soll.

Die Arbeit mit den Shellskripten

Grundsätzlich gilt, dass jedes Shellskript exakt jene Befehle aufrufen soll, die den entsprechend benannten Build-Vorgang abarbeiten. Im »pre_cleanup«-Schritt sollte »pre_cleanup.sh« etwa dafür sorgen, dass auf dem Runner nicht schon ein Docker-Container läuft, der den gleichen Namen wie der zu bauende trägt – dann ließe sich beim Ausführen von »test.sh« kein neuer Container mehr starten.

Analog sollte »pre_cleanup.sh« auch sicherstellen, dass in der lokalen Image-Liste des Runners nicht schon ein Docker-Abbild mit dem gleichen Namen und der gleichen Version vorliegt, weil sonst das Bauen ebenfalls schiefginge.

In »build.sh« gehören alle Befehle, die den Container bauen – allen voran »docker build« und eventuell nötige Zusatzbefehle (Abbildung 3). Der interessanteste Teil ist sicherlich »test.sh«: Hierin hat der Admin die Möglichkeit, beliebige Tests in Shellsyntax zu definieren, mit denen sich prüfen lässt, ob der automatisch gebaute Container wirklich wie gewünscht funktioniert.

Abbildung 3: <code>build.sh</code> im Beispiel: Diese Variante des Skripts baut das Abbild und versieht es mit einem Tag.

Abbildung 3: »build.sh« im Beispiel: Diese Variante des Skripts baut das Abbild und versieht es mit einem Tag.

Soll der Container zum Beispiel Nginx auf Port 80 und Port 443 starten, könnte eine »for«-Schleife in Kombination mit »netstat« oder »ss« prüfen, ob nach dem »docker run«-Aufruf innerhalb von 30 Sekunden tatsächlich ein Nginx einen TCP/IP-Socket öffnet. Die IP-Adressen des Loopback-Interface, etwa »127.127.127.127«, eignen sich dafür hervorragend. Der »docker run«-Aufruf muss dafür freilich mit entsprechenden Parametern gespickt und der Docker-Container selbst muss in der Lage sein, diese Umgebungsvariablen zu nutzen.

Große Bedeutung kommt aber auch dem Ausgabeverhalten des Shellskripts zu: Es muss umgehend den Rückgabewert »1« liefern, wenn es unterwegs auf einen Fehler stößt. Laufen die Tests erfolgreich durch, sollte es am Ende den Wert »0« zurückgeben, sodass Gitlab-CI den Test als “erfolgreich” in der eigenen Datenbank vermerken kann. Dem Upload des Image in die Docker-Registry per »upload.sh« steht danach nichts mehr im Wege. Dabei sollte der Admin aber darauf achten, dass er das Image nicht nur hochlädt, sondern auch ein entsprechendes Tag setzt.

Übrigens: Die CI/CD-Jobs von Gitlab-CI kann der Admin live mitverfolgen, indem er das Webinterface öffnet, zum jeweiligen Projekt navigiert und dort auf »CI/CD« klickt. Sowohl die Pipelines (Abbildung 4) als auch die Jobs (Abbildung 5) listet Gitlab dort auf. Geht ein Test schief, informiert Gitlab den Admin darüber – auch per E-Mail.

Abbildung 4: Mittendrin statt nur dabei: Gitlab zeigt sowohl eine &Uuml;bersicht &uuml;ber alle abgearbeiteten Pipelines als auch &hellip;

Abbildung 4: Mittendrin statt nur dabei: Gitlab zeigt sowohl eine Übersicht über alle abgearbeiteten Pipelines als auch …


Abbildung 5: &hellip; &uuml;ber deren einzelne Jobs samt der Ausgabe, die beim Abarbeiten des Jobs entstanden ist.

Abbildung 5: … über deren einzelne Jobs samt der Ausgabe, die beim Abarbeiten des Jobs entstanden ist.

Was ist mit dem Dockerfile?

Wer schon Docker-Abbilder gebaut hat, stellt sich möglicherweise eine Frage: Was ist mit dem Dockerfile? Es enthält schließlich alle relevanten Anweisungen für »docker build«. Klar ist: Es müssen neben den diversen CI-Skripten auch alle anderen Dateien vorhanden sein, die man ohne CI bräuchte – allen voran das Dockerfile.

Fazit

Gitlab-CI und die darin enthaltene Docker-Integration erweisen sich als ein sehr praktisches Gitlab-Add-on. Wer ohnehin Gitlab nutzt und sich mit dem Bau von Docker-Containern beschäftigen möchte, sollte sich Gitlab-CI sehr genau ansehen. Auch und gerade all jene, die nicht eigene Apps mit Docker verteilen wollen, sondern auf der Suche nach einer sehr schlanken und effizienten Methode sind, um Deep-Infrastructure-Komponenten zu betreiben.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben