This article is also available in English
Es gibt einige Gründe, wieso wir unsere Abhängigkeiten auf dem aktuellen Stand halten wollen und sollten. Neben neuen Features, die neue Versionen oft mitbringen, und möglichen Verbesserungen der Performanz spielt hier vor allem Sicherheit eine große Rolle. Neue Versionen beheben gefundene, bekannte oder noch unbekannte Sicherheitslücken und machen somit unsere Anwendung robuster. Und auch langfristig hilft es uns, neue Versionen zeitnah zu integrieren, auch ohne dass diese akut Sicherheitsprobleme beheben. Nämlich in Vorbereitung für das nächste Sicherheitsupdate.
Gut zu sehen war dieses Problem bei Log4Shell. Für diese Lücke gab es zwar zeitnah Sicherheitsupdates, allerdings haben es manche Anwendungen bis heute nicht geschafft, auf diese zu aktualisieren. Vielfach liegt dies daran, dass diese Anwendungen einen sehr alten Stand einsetzen und das Update dementsprechend kompliziert ist und dadurch ein hohes Risiko für Fehler aufweist.
Damit wir selbst nicht in dieser Falle landen, sollten wir Updates unserer Abhängigkeiten zeitnah erledigen. Dabei gibt es vor allem drei Herausforderungen. Zuerst die hohe Anzahl von Abhängigkeiten. In der Regel haben wir, selbst in kleineren Anwendungen, schnell eine mittlere zweistellige Anzahl von Abhängigkeiten. Die Programmiersprache selbst, Frameworks und Bibliotheken, die wir einsetzen und Werkzeuge, die wir zum Bauen und Paketieren benutzen. Anschließend ist auch potenziell jedes Update selbst eine Herausforderung. Je nach Versionssprung und den enthaltenen Änderungen reicht dieses dabei von der Erhöhung einer Versionsnummer bis zu größeren Umbauten an unserem Code. Und zuletzt müssen wir überhaupt erst einmal mitbekommen, dass es ein Update gibt.
Um neue Updates mitzubekommen, habe ich mich bisher zumeist auf RSS-Feeds oder Mailinglisten verlassen. Zumindest für große Projekte funktioniert dies zuverlässig. Allerdings muss ich dann immer noch aktiv daran denken, das Update auch durchzuführen. Und bei einer hohen Anzahl von Abhängigkeiten muss ich auch eine große Menge von Quellen verfolgen.
Genau an dieser Stelle gibt es mit Renovate eine Botbasierte Lösung für dieses Problem. Der Bot analysiert dafür unser Source-Code-Repository und extrahiert unsere Abhängigkeiten. Für diese wird anschließend geprüft, ob es neuere Versionen gibt. Sollte es neuere geben, erstellt der Bot einen neuen Branch, erhöht dort die Versionsnummer in einem Commit und erstellt einen Merge Request (MR), bei GitHub auch Pull Request genannt.
Um in diesem Artikel nicht, mehr oder weniger, die Dokumentation zu wiederholen, wollen wir uns im Folgenden die Verwendung, erweiterte Konfiguration und den Betrieb von Renovate an einem Beispiel anschauen.
Beispielprojekt
Da uns in diesem Artikel nur die Abhängigkeiten unseres Projekts interessieren, spielt die Fachlichkeit, ausnahmsweise, keine Rolle. Deswegen wird hier auch der eigentliche Code der Anwendung nicht zu sehen sein.
Es handelt sich bei unserem Projekt um eine Java-Anwendung, die mit Maven gebaut wird und als Framework auf Spring Boot setzt. Zur Verwaltung der Java- und Maven-Version wird SDKMAN! verwendet. Für Maven wird zusätzlich der Maven-Wrapper verwendet.
Zusätzlich benötigen wir Node.js, um CSS und JavaScript zu bundlen. Hierzu setzen wir zum einen den Node Version Manager (nvm) ein und zum anderen, innerhalb von Maven, das frontend-maven-plugin. Da die Anwendung als Container betrieben wird, gibt es zudem ein Dockerfile.
Als Source-Code-Repository wird ein selbst gehostetes GitLab eingesetzt. In diesem läuft auch eine Pipeline mit GitLab CI zum Bauen und Paketieren der Anwendung. Diese benötigt ein eigenes Container-Image, das über die Datei Dockerfile.release gebaut wird. Zu guter Letzt wird innerhalb der Pipeline auch noch jbang genutzt, um in Java geschriebene Skripte auszuführen.
Dieser Stack ist dabei nicht aus der Luft gegriffen, sondern entspricht dem wirklichen Stack einer von mir entwickelten und intern eingesetzten Anwendung. Es gibt mit Sicherheit einfachere, aber auch komplizierte Stacks.
Um eine Menge an Listings zu vermeiden, verzichte ich hier darauf, alle Konfigurationsdateien im Detail zu zeigen. Wenn der konkrete Inhalt jedoch relevant ist, oder für Renovate angepasst werden muss, werden diese natürlich gezeigt.
Onboarding und erste Konfiguration
Sobald der Bot unser Repository das erste Mal sieht, wird er einen MR, siehe Abbildung 1, für das Onboarding unseres Projekts erstellen. Die eigentliche Codeänderung besteht dabei aus dem Hinzufügen der Datei renovate.json mit dem Inhalt aus Listing 1. Die Beschreibung des MR gibt uns allerdings schon einen guten Überblick darüber, was Renovate erkannt hat und welche Updates sich daraus direkt ergeben werden.
Nachdem dieser MR gemergt wurde, wollen wir den Bot weiter konfigurieren. Zum einen möchten wir, dass sämtliche neuen MRs für Updates mir als Bearbeitendem zugewiesen werden. Zum anderen hat uns der Onboarding MR erklärt, dass nur zwei MRs pro Stunde geöffnet werden. Dieses Limit möchten wir aufheben, um alle MRs so schnell wie möglich zu erhalten. Hierzu fügen wir drei Konfigurationswerte zur Datei renovate.json hinzu, die nun wie in Listing 2 aussieht.
Nachdem der Bot nun ohne Limits für die Anzahl von MRs und mit der Anweisung, mir diese zuzuweisen, das nächste Mal läuft, ergeben sich eine Menge an neuen MRs, siehe Abbildung 2.
Um komplexere Konfigurationen wiederverwenden zu können, bietet uns Renovate,
neben Konfigurationswerten, auch sogenannte Presets zur Verwendung an. Diese
werden über den Konfigurationswert extends eingebunden. Renovate bringt hier
von Haus aus schon eine große Anzahl mit. Darunter befindet sich auch das Preset
config:recommended
. Da dieses, wie der Name sagt, empfohlen wird, wollen wir
es auch nutzen. Die Dokumentation dieses Presets zeigt uns,
dass dieses wiederum aus einer Menge an anderen Presets besteht.
Jetzt, wo wir Presets kennen, stellen wir auch unsere vorherigen
Konfigurationswerte auf diese um. Für alle drei gibt es jeweils ein
spezialisiertes, welches etwas lesbarer ist und die Konfiguration verkürzt.
Außerdem wollen wir noch, dass Renovate alle offenen MRs jeweils rebased, sobald
es neue Commits auf dem Hauptbranch gibt. Hierzu fügen wir noch das Preset
:rebaseStalePrs
hinzu. Unsere RenovateKonfiguration sieht nun wie in Listing
3 aus.
Durch diese neue Konfiguration ergeben sich beim nächsten Lauf von Renovate drei Änderungen. Zuerst werden alle schon offenen MRs gerebased, weil wir durch die Konfigurationsänderungen neue Commits auf unserem Hauptbranch durchgeführt haben. Zweitens ergibt sich ein neuer MR für unsere Abhängigkeiten zum JDK, da Renovate dieses durch die empfohlene Konfiguration nun kennt.
Zuletzt hat Renovate auch noch ein Dependency Dashboard (s. Abb. 3) in Form eines Issues angelegt. Dieses listet dabei primär alle aktuell offenen MRs auf. Es enthält jedoch auch eine Liste aller erkannten Abhängigkeiten und wird auch genutzt, um gefundene Probleme zu kommunizieren. Kann Renovate beispielsweise gewisse Abhängigkeiten nicht auflösen oder wird eine Fehlkonfiguration erkannt, werden diese hier gemeldet. Außerdem kann das Dashboard auch für die Interaktion mit dem Bot genutzt werden, wie wir im nächsten Abschnitt sehen werden.
Updates konfigurieren
Wie wir sehen können, gibt es schnell, vor allem in Bestandsprojekten, eine
große Anzahl von Updates, die durchgeführt werden sollten. So schlägt uns der
Bot beispielsweise ein Update von Spring Boot 3.0.10 auf 3.1.4 vor. Es gibt
jedoch, mit 3.0.11, eine weitere Version von 3.0. Möchten wir auch für dieses
Update einen MR erhalten, sollten wir den Konfigurationswert
separateMinorPatch
auf true
setzen oder das Preset :separatePatchReleases
hinzufügen. Anschließend werden wir einen zusätzlichen MR für das Update auf
3.0.11 erhalten. Möchten wir zusätzlich für die Major und Minor Updates von
Spring Boot, da wir diese auf andere Wege mitbekommen, gar keine MRs mehr
erhalten, können wir dies über eine spezifische Konfiguration für einzelne Abhängigkeiten, siehe Listing 4, erledigen.
Der empfohlene Mittelweg für diesen Fall wäre es jedoch, wie oben erwähnt, das
Dependency Dashboard für diese Updates zu verwenden. Hierzu ersetzen wir die
Konfiguration von "enabled": false
aus Listing 4 durch
"dependencyDashboardApproval": true
. Anschließend enthält das Dependency
Dashboard Issue den in Abbildung 4 zu sehenden Teil und der MR für das Update
auf Spring Boot 3.1.4 würde erst nach Aktivierung der Checkbox erstellt.
Unterstützung für jbang, SDKMAN! und das frontend-maven-plugin
Nicht immer werden alle Anwendungsfälle direkt von Renovate unterstützt. Wenn es jedoch darum geht, Abhängigkeiten in bisher nicht unterstützten Dateien zu erkennen, können wir selbst für eine Lösung sorgen.
Die Komponente, um Abhängigkeiten in Dateien zu erkennen, wird innerhalb von Renovate als Manager bezeichnet. Neben vielen spezialisierten gibt es auch einen konfigurierbaren, welcher die Extraktion über Reguläre Ausdrücke vornimmt. So ermöglicht es uns jbang über Kommentare am Anfang der Datei, siehe Listing 5, Abhängigkeiten zu spezifizieren, welche dann zur Ausführung des Skripts heruntergeladen werden. Diese werden leider standardmäßig nicht von Renovate erkannt. Allerdings lassen sie sich gut über einen regulären Ausdruck erkennen, sodass wir einen eigenen Manager, siehe Listing 6, konfigurieren können.
Dieser Manager guckt in allen Dateien, die auf .java enden und im Ordner bin liegen, ob die definierte Zeichenkette gefunden wird. Wird sie gefunden, werden über benannte Gruppen Informationen aus dieser extrahiert. Die Gruppe currentValue ist dabei Pflicht und muss die aktuell genutzte Version enthalten. Weiterhin müssen wir noch den Namen der Abhängigkeit und die Quelle, bei Renovate datasource genannt, erkennen. Dies können wir entweder über den regulären Ausdruck oder fixe Templates der Konfiguration erledigen. In diesem Fall extrahieren wir den Namen über die Gruppe depName und setzen die Quelle über das Template datasourceTemplate fix auf maven.
Beim nächsten Lauf wird Renovate nun zwei weitere Abhängigkeiten, nämlich org.springframework.boot:spring-boot-dependencies und org.json:json, erkennen. Da über unsere config:recommended auch das Preset group:recommended angezogen wird und dieses wiederum auf group:springBoot verweist, wird für neue Spring Boot-Versionen nur ein MR eröffnet, welcher gleichzeitig die Version in unserem jbang-Skript und in der POM aktualisiert.
Ein ähnliches Problem existiert für unsere Nutzung von SDKMAN!. Diese wird nicht automatisch unterstützt, lässt sich aber über weitere Regex-Manager, siehe Listing 7, lösen.
In diesem Projekt gibt es noch eine weitere Stelle, die wir mit einem Regex-Manager lösen. So führt die Nutzung des frontend-maven-plugin dazu, dass wir dort die zu verwendende Version von Node.js angeben müssen. Auch diese Stelle wird nicht automatisch von Renovate erkannt. Listing 8 löst jedoch auch dieses Problem.
Wie wir in Abbildung 5 sehen können, werden nun über diese Zusatzkonfiguration weitere fünf Abhängigkeiten erkannt und wir haben weniger manuelle Arbeit zu erledigen.
Container und Digest Pinning/Updates
Standardmäßig bringt Renovate bereits eine breite Unterstützung für Container mit. So wurden sowohl die Basis-Images in unseren beiden Dockerfiles als auch innerhalb der GitLab CI-Konfiguration erkannt (s. Abb. 6).
Allerdings werden in Dockerfile.release, siehe Listing 9, zusätzliche Pakete über den Paketmanager von Alpine Linux installiert. Hierbei wird empfohlen, auch für diese Pakete eine fixe Version anzugeben.
Natürlich möchten wir auch für diese von Renovate über Updates benachrichtigt
werden. Hierzu existiert zum Glück bereits mit
regexManagers:dockerfileVersions
ein Preset, das wir nutzen können. Allerdings
müssen wir hierzu auch unser Dockerfile.release, wie in Listing 10 zu sehen,
anpassen. Hier nutzen wir eine Kombination aus Kommentaren und Variablen, um die
Versionen zu spezifizieren, welche unter der Haube erneut durch einen
Regex-Manager erkannt werden können.
Container haben jedoch noch eine zweite Besonderheit, nämlich Floating Tags.
Kurz gesagt bedeutet dies, dass auch wenn ich einen spezifischen Tag von einem
Container-Image wie eclipse-temurin:17.0.7_7-jdk-alpine
angebe, ich nicht
zwangsweise immer exakt dasselbe Image erhalte. Container Tags sind nämlich
nicht unveränderlich und dementsprechend kann sich das Ziel, auf das diese
zeigen, über die Zeit ändern. Deswegen ist es eine gute Idee, das exakt genutzte
Image anzugeben. Hierzu ist die zusätzliche Angabe eines Digests erforderlich.
Hierbei können wir uns wieder von Renovate unterstützen lassen. Hierzu können
wir zuerst das Preset docker:pinDigests nutzen. Beim nächsten Lauf wird uns
Renovate einen MR erstellen, siehe Abbildung 7, in dem alle erkannten Images mit
einem Digest versehen werden.
Zwar können sich Tags aus beliebigen Gründen ändern, in der Praxis liegt dies jedoch meistens daran, dass innerhalb unseres Tags die Version einer Abhängigkeit, zum Beispiel des JDKs, fixiert wird, sich aber das zugrunde liegende Basis-Image aufgrund von Paketupdates verändert hat. Da wir diese Updates auch durchführen sollten, wird es nach dem Digest Pinning vermehrt MRs von Renvoate geben, bei denen nur das Digest aktualisiert wird. Um von diesen nicht überrannt zu werden, kann es, bei passender Testabdeckung und Sicherheitsempfinden, sinnvoll sein, diese von Renovate selbst automatisch mergen zu lassen, ohne selbst etwas tun zu müssen. Hierzu können wir das Preset :automergeDigest nutzen. Renovate wird nun, sobald die Pipeline für einen Digest Update MR erfolgreich gelaufen ist, diesen selbst mergen.
Eigene Presets
Durch all diese Konfigurationen ist unsere renovate.json nun bereits ein ganzes Stück gewachsen und hier und da auch unübersichtlich geworden. Möchten wir nun auch noch Teile dieser Konfiguration in anderen Projekten wiederverwenden, ist spätestens der Zeitpunkt gekommen, diese in eigene Presets auszulagern. Das Ergebnis für dieses Projekt ist in Listing 11 zu sehen.
Wir haben sowohl einige der vorher verwendeten Presets als auch die Regex-Manager für die SDKMAN!-Konfiguration entfernt und dafür das Preset local>example/renovate//presets/java hinzugefügt. Dieses Preset sagt, dass Renovate im gleichen GitLab (local) im Projekt example/renovate die Datei presets/java.json, siehe Listing 12, einbinden soll. In diesem Preset hat nun die SDKMAN!-Konfiguration einen globalen Platz gefunden. Außerdem wird ein Preset für die Unterstützung von Maven-Properties zur Konfiguration von Abhängigkeitsversionen, regexManagers:mavenPropertyVersions, aktiviert und ein weiteres eigenes Preset, siehe Listing 13, verwendet.
Dieses Preset wiederum stellt die Basis auch für nicht Java-Projekte dar. Hierbei verlassen wir uns auf unzählige, sinnvolle Einstellungen, die über das eingebaute Preset config:best-practices eingestellt werden. Zusätzlich heben wir die Limits auf, aktivieren das automatische Rebase und die Unterstützung von Paketen in Dockerfiles.
Konfiguration und Setup des Bots
Jetzt haben wir viel über die Nutzung und Konfiguration von Renovate aus Sicht der Projekte gesehen. Jedoch stellt sich noch die Frage, wie wir diesen Bot nun betreiben können. In unserem aktuellen Setup nutzen wir die in GitLab selbst vorhandene Funktion von Scheduled Pipelines. Dabei wird periodisch die in Listing 14 zu sehende Pipeline durchlaufen.
Diese basiert auf dem aktuellen Renovate-Container-Image und startet den Bot. Dieser wird über die Datei config.js, siehe Listing 15, konfiguriert. Durch die Verwendung von autodiscover wird der Bot automatisch alle Projekte verwalten, die er finden kann. Dies ermöglicht es uns, dass Projekte, die Renovate benutzen möchten, nur den technischen GitLab-Nutzer des Bots als Mitglied ins Projekt einladen müssen. Durch unsere eigene onboardingConfig sorgen wir dafür, dass bereits beim Onboarding standardmäßig unser eigenes Default Preset konfiguriert wird.
Der Eintrag unter registryAliases ermöglicht es, in den projektspezifischen GitLab CI-Konfigurationen die Umgebungsvariable CI_REGISTRY zu verwenden. Ohne diesen Eintrag würde Renovate nicht erkennen, dass dieser Teil des Images ein Platzhalter ist.
Die hostRules dienen der Autorisierung bei diversen Abhängigkeitsquellen. So müssen wir uns bei der öffentlichen Docker Registry, dem Docker Hub, einloggen, um ein höheres Limit an API Requests zu haben und nicht blockiert zu werden. Die beiden anderen Einträge erlauben es, die GitLab eigene Container und Package Registry abzufragen. Hierbei ist jedoch zusätzlich zu beachten, dass der technische Nutzer des Bots Zugriff auf die passenden Projekte benötigt, da er diese ansonsten nicht sehen darf.
Da wir innerhalb desselben GitLab laufen, in dem wir auch die Projekte verwalten, können wir an vielen Stellen auf vorhandene Umgebungsvariablen zurückgreifen. Trotzdem müssen wir zusätzliche, siehe Abbildung 8, definieren. Neben dem Token für den Docker Hub, DOCKERHUB_TOKEN, benötigen wir einen Token für GitHub: GITHUB_COM_TOKEN. Von dort werden Release-Notes per API abgefragt und in die Beschreibung des MR eingefügt. Der RENOVATE_GIT_PRIVATE_KEY wird von Renovate genutzt, um die erstellten Commits in Git zu signieren. Der RENOVATE_TOKEN letztlich dient dem Zugriff auf GitLab selbst und wird deswegen auch zwingend benötigt.