netcup News

vspace.one e.V. – Referenzstory

03.11.2022, Kategorie: Kooperation

Grafik und Logo vspace.one e.V.Dieser Beitrag zeigt, wie mithilfe von Jenkins eine React-basierte Website von GitHub als Container auf einem virtuellen Server automatisiert bereitgestellt werden kann. Ziel ist es, allein durch eine Versionierungsoperation einer Änderung („Release“) diese neue Version innerhalb von kurzer Zeit automatisiert zu veröffentlichen.

Autoren: Damian Jesionek, Moritz Klaiber

Anlass des Artikels ist eine Case Study, die darstellt auf welche Weise wir als vspace.one e. V. den von netcup bereitgestellten Root-Server einsetzen, und um unseren Ansatz als Beispiel für ähnliche Anwendungszwecke zu teilen.

 

Grundlagen

Ein kurzer Überblick über die eingesetzten Technologien.

Jenkins

Jenkins ist ein von Kohsuke Kawaguchi entwickeltes, webbasiertes Tool zur Umsetzung von Buildautomatisierung. Das Open-Source Tool wird von der Community mit einer Vielzahl an Plugins unterstützt und kann von jedem selbst Betrieben werden, sodass es aufgrund seiner vielseitigen Einsetzbarkeit das meistgenutzte Tool für CI/CD ist. Durch viele, größtenteils kostenlose, Plugins kann Jenkins für jede aktuell verfügbare Programmiersprache verwendet werden. /

Mehr zu Jenkins.

git

git ist ein freies Versionsverwaltungssystem für Quellcode und wurde ursprünglich von Linus Torvalds für die Entwicklung des Linux Kernels entwickelt. Github dagegen ist eine Webplattform, um mit git versionierte Repositories zu verwalten und auch öffentlich tu teilen.

Viele weitere Projekte des vspace.one sind hier auf Github einzusehen. Alternativ zu Github gibt es auch Gitlab oder Gitea, welche sogar selbst gehosted werden können.

Container

VMs und Container sind beides Konzepte der Virtualisierung. Beide sind jedoch Lösungen für unterschiedliche Probleme. Während bei VMs tatsächliche Hardwareressourcen virtualisiert werden, arbeiten Container eine Abstraktionsebene höher und sind isolierte Prozesse innerhalb vom Host-Betriebssystem. Docker ist eine Open-Source Implementierung, welche der Bereitstellung von Anwendungen innerhalb portabler Container dient.

Docker Images sind üblicherweise auf hub.docker.com zu finden. Images von vspace.one.

React

React ist ein NodeJS Framework zur Entwicklung von One-Page-Web-Apps und wird im vspace.one zur Entwicklung der Website verwendet. Prinzipiell ist vieles in diesem Artikel auch auf Webanwendungen anwendbar, welche in anderen Sprachen und Frameworks entwickelt worden sind. Daher wird nicht genauer auf dieses Framework eingegangen.

Umsetzung

Zunächst sollen alle Abschnitte der Umsetzung einzeln beschrieben und zum Schluss zusammengesetzt und ganzheitlich betrachtet werden. Der Ansatz für jede Automatisierung läuft idR genauso ab: alle einzelnen Schritte sollten zuvor einmal manuell durchgeführt werden.

Versionierung während der Entwicklung

Die Versionierung findet über git statt, damit der Quelltext auch auf der Plattform github.com veröffentlicht werden kann. Verwendet wird hier ein einfacher Development-Branch-Workflow, der durch das nachfolgende Schaubild verdeutlicht wird:

Development-Branch-Workflow

Damit der Veröffentlichungsworkflow korrekt funktioniert, muss die Pipeline in Jenkins zwischen einem „fertigen“ und einem „unfertigen“ Zustand unterscheiden können. Das wird durch die meisten üblichen Git-Flows ermöglicht, da in der Regel ein funktionsfähiger und zur Veröffentlichung geeigneter Stand in einem master-Branch erwartet wird. Dev-Branches wie der bei uns als „beta“ bezeichnete Branch sind eine Erweiterung vom dauerhaften master-Branch, die einen Zwischenstand von neuen Änderungen vor einer Veröffentlichung sammeln. Auf diesen Branches ist es dann möglich mehrere Änderungen vor einer Veröffentlichung sowohl zu sammeln als auch die Kombination dieser zu testen.

Da es immer wieder vorkommt, dass manche Änderungen etwas mehr Arbeit und somit auch Zeit benötigen, werden oft weitere Branches aus dem beta-Branch abgezweigt. Diese konzentrieren sich meist nur auf eine bestimmte größere Änderung und vermeiden somit Konflikte und Komplexität, die bei gleichzeitiger Arbeit (vielleicht sogar mehrerer Personen) auftreten könnten.

Diese ganzen Änderungen müssen anschließend auch an Jenkins oder andere Automatisierungstools mitgeteilt werden. Dies ist auf Github möglich, indem für ein Repository Webhooks eingestellt werden. Webhooks sind simple HTTPS-POST-Aufrufe auf einen Webserver, welcher beispielsweise Jenkins sein könnte. Mit diesen Aufrufen werden diverse Informationen von Github mitgeliefert, wie beispielsweise der Grund für den Webhook, geänderte Branches usw. Auf Basis dieser Informationen kann dann über den weiteren Ablauf entschieden werden, zum Beispiel verschiedene Pipelines für den master oder den beta Branch auszuführen.

Webhooks

Erstellen/Bauen/Veröffentlichen des Containers

Da wir die Website als Gesamtpaket in einem Container-Image verpacken muss hierfür das nötige Tooling entsprechend konfiguriert werden. Grundsätzlich funktioniert das für alle Programmiersprachen. Es gibt natürlich von Sprache zu Sprache unterschiede, denn am Ende muss im Image das fertig kompilierte Programm oder in unserem Fall mit NodeJS inklusive dem vollständigen Quelltext und allen benötigten NPM-Paketen zur Verfügung stehen.

Am Anfang sollte immer der übliche Entwicklungsprozess betrachtet und festgehalten werden. Dieser ist für unsere Webseite recht einfach gehalten. Vorbedingung ist es Version 11 von NodeJS sowie NPM zur Verfügung zu haben. Den Check-out vom git-Repository können wir an dieser Stelle weglassen, da sich Jenkins später selbst darum kümmert. An der Stelle gehen wir einfach davon aus einen beliebigen funktionstüchtigen Branch vorliegen zu haben.

Um anschließend die benötigten NPM-Pakete zu laden und die Website zu starten sind nur diese vier Befehle nötig:

Diese Schritte müssen dann genauso oder zumindest ähnlich in der Dockerfile auftauchen.

In der Dockerfile gibt es noch die Besonderheit, dass die Umgebungen für install/build und für die spätere Ausführung getrennt werden. Hintergrund ist, dass wir im Produktionsbetrieb nur mit den Daten aus dem mit „npm build“ generierten build-Ordner arbeiten wollen. Mehr zum Dockerfile Format.

Damit lässt sich nun mit einem Befehl ein Container-Image erstellen:

Die Ausgaben zeigen dann den Buildprozess von NPM. Das resultierende Image kann nun auf eine beliebige Image-Registry hochgeladen werden. In unserem Fall ist das hub.docker.com und das image heißt vspaceone/web-react.

Update der Website auf dem Server

Im letzten Abschnitt wurde das Container-Image gebaut. Dieses sollte anschließend ausgeführt werden. Am einfachsten lässt sich das mit docker-compose erreichen. Dazu muss man eine Datei namens docker-compose.yml mit folgendem Inhalt anlegen:

version: “3”

Darin werden das auszuführende Container-Image selbst und der Port angegeben, unter welchem die Anwendung im Container erreichbar sein soll. Mit „docker-compose up -d“ kann der Container im Hintergrund gestartet werden.

Im docker-compose.yml ist zu sehen, dass die Anwendung damit auf Port 80 erreichbar sein wird. Das bedeutet auch, dass noch keine TLS-Verschlüsselung im Container stattfindet. In unserem Fall ist das in Ordnung, da auf dem Webserver aus Sicherheitsgründen sowieso kein Container ein direktes Portmapping bekommt. Stattdessen wird ein Reverseproxy davorgeschaltet, welcher in unserem Fall Traefik2 ist. Alternativ können natürlich auch andere Reverse Proxies verwendet werden, wie z. B. Apache oder NGINX. Soll eine Anwendung allerdings trotzdem direkt freigegeben werden, muss statt dem „expose“ Attribut „ports“ angegeben werden.

Bei dem Update handelt es sich lediglich um einen Neustart vom Container. Mit „docker-compose pull“ wird dabei die neue Image-Version heruntergeladen und mit „docker-compose up -d“ der Container gestoppt und mit dem neuen Image neugestartet.

Dieser Prozess muss ebenso automatisch ausführbar sein. In unserem Fall setzen wir wieder auf Webhooks, diesmal mit dem simplen Tool. Dieses empfängt Webhooks von Jenkins mit unterschiedlichen Daten, anhand derer WebhookD festgelegte Scripts wie in unserem Fall dieses ausführt:

Um alles sicher und minimalistisch zu gestalten, verwenden wir keinerlei Variablen die WebhookD aus den Webhooks bei der Ausführung von Skripten zur Verfügung stellt. Dadurch vermeidet man am besten Code-Injection. Zusätzlich ist die URL der Webhooks geheim und beinhaltet ein Secret damit ein Update nicht versehentlich oder böswillig ausgeführt werden kann.

Verbinden der einzelnen Phasen mit Jenkins

Alle bisherigen Phasen können nun in Jenkins mit Hilfe einer sogenannten Pipeline verbunden werden. Was dann an allen genannten Stellen passiert soll das folgende Diagramm noch zusammenfassen:

Verbindung Phasen mit Jenkins

  1. Aus dem Abschnitt „Versionierung“: Jenkins muss das git-Repository herunterladen. Das ist nicht nur wichtig, damit der Quellcode des Projekts gebaut werden kann, sondern weil sich im gleichen Repository auch das „Jenkinsfile“ befindet. Dieses beinhaltet wiederum Code, welcher die Jenkins Pipeline definiert, die dann durch Jenkins ausgeführt werden kann.
  2. Aus dem Abschnitt „Erstellen/Bauen/Veröffentlichen des Containers“: Jenkins führt hier den beschriebenen Bauvorgang anhand der im Jenkinsfile festgelegten Befehle aus.
  3. Veröffentlichung des erstellten Container-Image auf dem Docker-Hub durch die Jenkins-Pipeline.
  4. Aus dem Abschnitt „Update der Website auf dem Server“: Jenkins benachrichtigt den Server über eine Webhook, dass eine neue Version des Images auf dem Docker-Hub bereitsteht.
  5. Daraufhin lädt der Server das neue Image und tauscht den laufenden Container durch einen aktualisieren aus.

Damit Jenkins diesen Prozess kennt wird er in der Jenkinsfile definiert. Eine vereinfachte Version wird hier zur Veranschaulichung dargestellt.

Die Jenkinsfile wird in einer Groovy-basierten Domain Specific Language geschrieben, daher sieht die Syntax zunächst ungewöhnlich aus. Verwendet wird für dieses Beispiel die deklarative Syntax. Die geskriptete Syntax ist dagegen viel näher an regulärem Groovy-Code.

Jede deklarative Pipeline beginnt zuerst mit dem pipeline-Block:

In einer Pipeline soll die erste Deklaration stets festlegen auf welchem „Agent“ eine Pipeline ausgeführt werden soll. Für dieses Beispiel gehen wir nicht weiter drauf ein und geben an, einen beliebigen verfügbaren zu verwenden. Es ist natürlich in unserem Fall einer vorhanden, auf dem alle Tools wie git, docker und co. vorinstalliert sind.

Anschließend können Umgebungsvariablen festgelegt werden. In Jenkins können geheime Zugangsdaten im Credential-Storage gespeichert und auf Pipelines über ihre ID abgerufen werden. Damit werden diese nie im Klartext sichtbar, weder in der Pipeline noch in den Logs der Ausführung. Zudem legen wir gleich fest, wie das gebaute Image genannt werden soll.

Zuletzt soll nun der zuvor bereits ausführlich betrachtete Build-Ablauf definiert werden. Dieser wird in Stages unterteilt. Da wir sichergestellt haben, dass die Anwendung beim Build-Prozess des Image auch mitgebaut und „eingebacken“ wird, muss lediglich das Image gebaut und veröffentlicht werden. Dadurch ließe sich diese Pipeline grundsätzlich für alles Image-Builds mit Docker wiederverwenden.

In Jenkins ausgeführt sieht das folgendermaßen aus:

Jenkins

Die vollständige Pipeline:

Fazit

Diese Art von Ende zu Ende Automatisierung des Lebenszyklus hat eine breite Verwendung in der sogenannten DevOps Arbeitsweise. Ziel ist es die Entwickler (Developers) möglichst eng in den Administrationsprozess und die Systemadministratoren (IT oder auch Operations) der Anwendung möglichst eng in die Entwicklung einzubinden. Abgesehen von dem im Firmenumfeld zu erwartetem Mehrwert durch die Ermöglichung einer enormen Beschleunigung der Durchlaufzeiten von Änderungen und Features hat das besonders im Open-Source-Bereich und eben auch in Vereinen wie dem vspace.one positive Effekte.

Dadurch, dass der Lebenszyklus einer Anwendung durchgehend automatisiert wird, entsteht zusätzliches Feedback für die Maintainer eines Projekts bezüglich Lauffähigkeit und Qualität. Das und auch eine automatische Bereitstellung eines Testsystems für manuelle Tests oder gar die Ausführung automatischer Tests senkt die Hürde besonders für neue Beiträge von Einsteigern. Auch durch berufliche Erfahrungen einiger Mitglieder konnte dieser Prozess immer weiter optimiert werden.

Dieses ganze Setup betreiben wir im vspace.one e. V. auf einem von netcup bereitgestellten RS 2000 G9 Root Server bereits seit mehreren Jahren erfolgreich. Mittlerweile sind auch viele weitere Anwendungen zu Jenkins und der Webseite dazugekommen wie beispielsweise das Wikisystem (DokuWiki), ein Monitoringsystem (Prometheus, Grafana, Alertmanager), Gitea für kritischere oder interne Infrastrukturprojekte, Nextcloud, Mitgliederverwaltung, Mailing und noch einige mehr. Die 4 AMD-Epyc-Kerne, 16GB RAM sowie der 320GB große SSD-Speicher bieten für alle diese Anwendungen ausreichend Leistung.

Unsererseits ist es eine klare Empfehlung, sich in Containerisierung (z.B. mit Docker) einzulesen und diese in solchen Setups (einzelner Server, viele Anwendungen; oft auch bei privaten Homeservern anzutreffen) extensiv zu nutzen. Damit lässt sich eine klare Trennung zwischen Daten und der Anwendung selbst herstellen. So werden Updates, Backups und (z.B. mit Docker Compose) das Klonen und zu migrieren einzelner Anwendungen oder des ganzen Server super einfach!