Verteilte Deployment Pipelines mit Github Actions
veröffentlicht am 14.04.2022
geschrieben von Tim Lohse
- Github Actions
- , Kubernetes
- , Nx
- , Docker
Wie wir bei stadt.werk eine verteilte Build und Deployment Pipeline mit Github Actions umgesetzt haben.
Einführung
Es kann viele Gründe dafür geben, dass Build und Deployment einer neuen Software nicht im selben Repository mit Github Actions durchgeführt werden sollen oder können. Man hält vielleicht seinen Quellcode bei Github unter Versionskontrolle und schätzt die einfache Integration von Github Actions Workflows, ist aber vom Funktionsumfang anderer CI/CD Lösungen (Continuous Integration / Continuous Delivery) wie CirclCI oder Jenkins begeistert und möchte eine kombinierte / verteilte Pipeline erstellen. Außerdem können die Anforderungen der eigenen Infrastruktur auch eine treibende Kraft für das Bedürfnis nach einer verteilten Build und Deployment Pipeline sein. So verhält es sich auch im Fall unseres Softwareprojekts mit dem internen Kürzel “C4”. Dieser Blog-Eintrag soll eine Beschreibung unseres Lösungsansatzes sein und einen Einblick in die Herausforderungen während der praktischen Umsetzung gewähren.
Infrastrukturumgebung
Eine kurze Übersicht über die vorhandenen Strukturen:
Im Backend setzen wir auf eine klassische REST API, welche das Web-Frontend mit Informationen versorgen soll und mit NestJs entwickelt wird. Der Quellcode für die verschiedenen Microservices wird in einem Monorepository c4_app_backend
versioniert und weiterentwickelt. Da ein Monorepository eine Vielzahl von Projekten beinhalten soll und dies ohne klare Struktur schnell zu einem komplexen Wirrwarr an Abhängigkeiten führen kann, setzen wir auf Nx als Verwaltungswerkzeug. Nx bringt neben einem Command Line Interface (CLI) zum Generieren und Verwalten von Nx Entitäten, eine eigene Ordnerstruktur mit sich. (siehe Abb. 1)
Abb. 1: Nx Ordnerstruktur
In einem Standard Nx Workspace gibt es einen apps
- und einen libs
-Ordner für Nx Projekte und Bibliotheken. Zum Stand dieses Blogeintrags sind dort sechs Nx Projekte mit diversen privaten und geteilten Bibliotheken abgelegt, welche durch eine Build Pipeline in einem Github Actions Workflow pro Umgebung (Staging und Produktion) zu Docker Images gepackt und in Github Packages abgelegt werden.
Die gebauten Docker Images der Microservices werden als Pods in einem Kubernetes (K8s) Cluster pro Umgebung, in AWS EKS, gehostet und sind anschließend durch AWS Load Balancer und K8s Ingresses aus dem Internet erreichbar. Die Cluster enthalten zudem Monitoring und Logging Funktionalitäten (Grafana, Prometheus und Loki), sowie ein Alfresco Content Management System (CMS). Zur Authorisierung der Microservices setzen wir auf den oAuth2.0 Standard durch Keycloak, einer Open Source Identity und Access Management Lösung von RedHat. Das Deployment der Docker Images in das K8s Cluster einer jeden Umgebung geschieht automatisiert durch einen Flux Controller, welcher per Infrastructure-as-Code K8s Konfigurationsdateien aus unserem Repository c4_app_backend_deployment
ausliest und Änderungen übernimmt.
Einen Gesamtüberblick dieser Build und Deployment Kette findet sich im nachfolgenden Schaubild.
Abb. 2: Build und Deployment Überblick
Die offene Frage zum Zeitpunkt der Konzeptionsphase war, wie wir die neu gebauten Versionen der Docker Images automatisiert in den jeweiligen K8s Konfigurationsdateien aus c4_app_backend_deployment
aktualisieren können, sodass Flux nach dem GitOps-Prinzip diese erkennt und in die Umgebungen übernimmt. Für unsere Lösung dieser Herausforderung ist besonders die Ordnerstruktur in c4_app_backend_deployment
eine wichtige Grundlage (siehe Abb. 3), welche die Umgebungen der Anwendung widerspiegelt und eine Konfigurationsdatei für jeden Backend Service enthält. Diese Dateien enthalten neben K8s-spezifischen Informationen auch die Docker Image Version, sodass Flux anschließend einen Pod im Cluster aufbauen kann.
Abb. 3: c4_app_backend_deployment Ordnerstruktur
Der Build Prozess
Sehen wir uns zunächst den Quellcode der Build Pipeline an, um mit den Möglichkeiten der CLI Tools bei jedem Push auf den develop
oder main
Branch herauszufinden, welche Nx Projekte verändert wurden, um diese neu bauen zu können. Am Beispiel der Staging Build Pipeline (Code 1) lässt sich zunächst in den ersten Schritten des Workflows keine große Besonderheit erkennen.
|
|
Jedoch ist bereits das Auschecken des Repository Codes mit der Option fetch-depth: 0
überaus wichtig, sodass mit git fetch origin main
die gesamte Versionshistorie für die nachfolgenden Nx CLI Befehle vorhanden ist. Da es sich bei den Microservices um NodeJs Quellcode handelt, welcher mit externen Npm Modulen angereichert wird, darf Caching, Auditing und die Installation dieser nicht fehlen. Der Befehl nx affexted:apps
, welcher mit den Umgebungsvariablen NX_BASE
und NX_HEAD
aus der externen Action nrwl/nx-set-shas befüllt wird, erkennt Änderungen an Nx Projekten, welche zwischen dem Base-Commit und dem Head-Commit aufgetreten sind und gibt eine Liste der Projektnamen aus. Diese Liste benutzen wir, um damit eine Github Actions Matrix zu befüllen und als Ergebnis des Jobs auszugeben.
Anhand der veränderten Nx Projekte in dieser Matrix starten wir den Bauprozess der Docker Images parallel in einzelnen Jobs. (siehe Code 2)
|
|
Sollten keine Projekte verändert worden sein, bricht der Workflow durch die IF-Bedingungen ab. Zunächst werden einige Umgebungsvariablen erzeugt, welche für die Benennung des Images nötig sind. Anschließend loggen wir uns mit dem GITHUB_TOKEN
in Github Packages ein, erzeugen anhand eines Dockerfile
aus dem Root Ordner des Repository ein Docker Image und veröffentlichen es.
Im Repository c4_app_backend_deployment
befindet sich ein Workflow (siehe Code 3), in dem zunächst die Umgebung überprüft und anschließend die alte Version des jeweiligen Docker Images in den zuvor angesprochenen K8s Konfigurationsdateien mit dem sed
Befehl gefunden und durch die neue Version ersetzt wird.
|
|
Anschließend werden die geänderten Dateien zur Git Stage hinzugefügt, zu einem Commit mit dem Prefix “[K8s-Update]” hinzugefügt und an das Github Repository gesendet, sodass Flux im Anschluss die Änderungen erkennen kann.
Soweit fehlt nur noch eine Lösung zum Start des Workflows in c4_app_backend_deployment
, mit den dazugehörigen Informationen zur Anpassung der K8s Konfigurationsdateien. Diese Informationen sind der Name des neu gebauten Docker Images, die Umgebung, für die das Image gebaut wurde, und der Name des veränderten Nx Projekts, sodass die richtige Konfigurationsdatei angepasst werden kann.
Ein Blick in die Dokumentation von Github Actions liefert mehrere mögliche Events, welche als Auslöser für einen Workflow fungieren können. Da wir einen Trigger benötigten, welcher in c4_app_backend
nach dem erfolgreichen Durchlauf des Build Prozesses manuell ausgeführbar ist und einen Payload enthalten kann, kommen zwei mögliche Events in Frage: repository_dispatch
und workflow_dispatch
.
Lösungsansatz repository_dispatch
:
Auf den ersten Blick scheint repository_dispatch
die perfekte Lösung für unser Problem zu sein. Durch einen HTTP POST Request gegen den Endpunkt /repos/{owner}/{repo}/dispatches
lässt sich manuell ein Event erzeugen, welches als Auslöser für den Workflow in c4_app_backend_deployment
dient. Zusätzlich können die benötigten Informationen dem Request als Payload übergeben werden.
Jedoch ist bei der praktischen Umsetzung mit repository_dispatch
schnell ein starke Limitation aufgefallen. Das repository_dispatch
Event bezieht sich ausschließlich auf den Standard Branch eines Ziel Repository (siehe Code 3), was es für uns in der Entwicklungsphase unmöglich gemacht hat das Event zu testen, da wir bei stadt.werk auf eine Branching Strategie mit Feature Branches setzen. Das bedeutet, dass wir pro Umgebung einen langlebigen Branch (main / develop) verwalten und neue Features auf temporären Branches entwickelt und anschließend nach erfolgreichem Pull Request in die langlebigen Branches eingefügt werden. (siehe Abb. 4)
Abb. 7: Limitation repository_dispatch Event
Lösungsansatz workflow_dispatch
:
Glücklicherweise gibt es außerdem das workflow_dispatch
Event, welches sich kaum vom repository_dispatch
Event unterscheidet. Es lässt sich ebenfalls durch einen HTTP POST Request manuell ausführen und kann die benötigten Informationen als Payload übergeben. Der Unterschied liegt darin, dass eine größere Flexibilität bei der Ansprache spezifischer Workflows in unterschiedlichen Branches gegeben ist, was es für die perfekte Lösung unseres Problem macht.
Somit ergibt sich folgende Lösungsstruktur. Der Build Prozess in c4_app_backend
aus Code 1 wird um einen POST Request mit cURL
ergänzt und übergibt die notwendigen Informationen. (siehe Code 4)
|
|
Dieser dient als Auslöser für den Workflow in c4_app_backend_deployment
aus Code 3. (siehe Code 5)
|
|