Clean-Up Strategien bei CI/CD

In unserem letzten Blogbeitrag («Binary Reproducible Builds im Java‑Umfeld«) ging es darum, dass ein Build-System (in unserem Falle GitLab) reproduzierbare Artefakte produziert.
Bei der Generierung von Artefakten wird oftmals ein Punkt erst zu spät angegangen: Das Aufräumen.
Derzeit haben wir auf GitLab etwa 2’700 Pipelines mit 5’300 CI/CD-Jobs pro Woche, WARs mit bis zu 300 MiB, container images mit 200-400 MiB, sowie etliche Bibliotheken.
GitLab bietet einige integrierte Mechanismen, mit der der Speicherbedarf reduziert werden kann:
- Automatisches Löschen von Pipelines nach x Tagen
- Lebensdauer von artifacts der Pipeline
- Löschen von container images nach x Tagen und Beibehaltung der letzten n Tags
Die ersten beiden Punkte sind sehr einfach umzusetzen:
Innerhalb der Projekteinstellungen, CI/CD-Settings automatic pipeline cleanup auf einen Wert stellen, der es noch ermöglicht über Fehler bei Merge-Requests oder Probleme mit einzelnen Pipelineläufen zu reden, z.B. 7 Tage oder 1 Monat.
Dies hat allerdings zur Folge, dass nach dieser Zeit alle Informationen über vergangene Builds oder deren Testergebnisse gelöscht sind.
Mit der Integration von reproducible builds ist dies aber keine Einschränkung, da die Pipelines zu jedem commit nachträglich wiederholt werden können.
Artefakte
Artefakte, gemäss der Definition von GitLab, sind Dateien, die von einem Job der Pipeline an einen weiteren Job weitergereicht werden. Dies können z.B. Ordner wie target oder dist sein, die die Klassen aus dem compile-Job an einen test-Job weiterreichen, damit der test-Job nicht erneut die Kompilation durchführen muss.
Im angular-/react Umfeld wird hier in Beispielen häufig der komplette node_modules-Ordner weitergereicht. Dies führt dann dazu, dass schnell 500 MiB an Daten am Ende des einen Jobs gepackt werden müssen, zum GitLab-Server übertragen, bei Beginn des nächsten Jobs vom GitLab-Server an den runner kopiert um dort entpackt zu werden. Neben dem Netzwerk-Traffic zwischen GitLab-Server und dem runner, was zu Performance Problemen führen kann, werden die Artefakte in der Default-Einstellung der Pipeline ewig aufbewahrt. Daher sind für die Artefakte zwei Dinge zu betrachten: Sie sollten möglichst klein sein und/oder eine kurze Lebenszeit haben.
Je nach Projekt kann es sinnvoller (was die Zeit für den Durchlauf einer Pipeline betrifft) sein, mehrere Jobs zu einem zusammenzufassen:
Statt getrennter compile, test, check_dependencies, integration_test Jobs, die jeweils nur ein anderes maven Kommando aufrufen, ist ein Job, der im Rahmen von mvn deploy alle diese Schritte ausführt, deutlich schneller fertig. Als Artefakt fallen dann nur noch die Testergebnisse an, da sonst keine Daten mehr in den nachfolgenden Jobs benötigt werden (JARs bzw. container images werden in der GitLab Registry abgelegt).
Die Lebenszeit der Artefakte sollte auf ein Minimum beschränkt sein und orientiert sich an der Frage: Wie viel später wird ein nachfolgender Job einer bestehenden Pipeline nochmals ausgeführt? Zum Beispiel kann ein Artefakt eines Jobs Daten für das Deployment bereitstellen, dass erst in einem deploy-k8s Job durchgeführt wird.
Falls also ein fehlgeschlagener deploy-k8s-Job, z.B. wegen einer nächtlichen renovate Aktualisierung, am nächsten Morgen wiederholt werden soll, müssen diese Artefakte noch zur Verfügung stehen, sofern man nicht die gesamte Pipeline neu starten will.
JARs, npm-Pakete und container images
Wir verwenden bei uns die in GitLab eingebaute Registry für die Pakete, die in der Pipeline gebaut werden. Eine externe Registry oder externes Repository bringt die administrative Aufgabe mit sich, dass die Berechtigungen der Projekte (und die daran beteiligten Mitarbeitenden) zwischen GitLab und z.B. nexus abgeglichen werden müssten. Durch die Nutzung der GitLab-Variante entfällt dieser Aufwand.
Unsere Pipelines produzieren entweder Bibliotheken (JARs, WARs bzw. npm-Pakete) oder fertige container images, die dann im Kubernetes-Cluster für Integrations- und E2E-Tests gestartet werden. Die Versionsnummer der CI/CD Builds enthält dabei sowohl den Branch als auch die commit-id um diese bei gleichzeitig laufenden Pipelines eindeutig referenzieren zu können.
Grenzen von GitLab
GitLab stellt bereits einige automatisierte Cleanup Regeln bereit (im Rahmen der Container‑Image‑Retention). Über diese kann festgelegt werden, dass Images, die älter als x Tage sind, gelöscht werden, oder dass nur die letzten n Tags pro Projekt erhalten bleiben sollen.
All diese Mechanismen funktionieren zuverlässig, solange die zu löschenden Objekte keine weitere Referenz im System mehr besitzen. Sobald jedoch externe Abhängigkeiten (z. B. laufende Deployments, manuelle Pull‑Vorgänge oder historische Release‑Tags) im Spiel sind, stösst die einfache „Zeit‑basiert‑löschen“-Logik von GitLab an ihre Grenzen.
Der Grund liegt darin, dass GitLab nicht wissen kann, ob ein Image oder ein Artefakt noch von einem anderen System, z.B. im Kubernetes‑Cluster, einer Test‑Umgebung, benötigt wird. Wird ein noch genutztes Image gelöscht, kann dies evtl. nicht mehr vom Cluster genutzt werden, es sei denn, exakt diese Version befindet sich noch auf dem entsprechenden Kubernetes Knoten.
Daher verzichten wir auf die reine zeitbasiert Logik und setzen statt dessen eine kontextabhängige Aufräumstrategie ein.
- Alle Images der letzten drei Tage werden beibehalten, der Rest gelöscht.
- Alle Images, die sich an die semver Notation halten oder eine fachlicher Versionsummer haben, sind als veröffentlichte Versionen von dem Cleanup ausgenommen.
- Alle Images, die von irgendeiner Workload (Pod, Deployment, CronJob, StatefulSet, … ) referenziert werden, werden nicht gelöscht
- Alle Images, die im Rahmen einer durch renovate angestossenen Aktualisierung entstanden sind, werden nach einem Tag gelöscht

JAR und WAR Pakete
GitLab bietet hier keinen Automatismus für die Löschung, daher werden bei uns alte Pakete von maven- und npm-builds nach einem ähnlichen Prinzip aufgeräumt:
Alle anderen Pakete werden nach 30 Tagen gelöscht
- Veröffentlichte Release‑Versionen: niemals löschen.
- Renovate‑Builds: maximal einen Tag behalten
- Branch‑/Snapshot‑Builds: diese werden drei Tage vorgehalten

Fazit
Der Einsatz von GitLab als Package-Registry bietet viele Vorteile gegenüber einer eigenständigen, externen Registry Lösung (artifactory, nexus, …):
- keine Admin-Aufwände für deren Betrieb
- automatische Berechtigung gemäss der Gruppen/-Projektzuordnung in GitLab
- einfache Löschregeln.
Für den praktischen Einsatz mit vielen gleichzeitig aktiven Versionen, die zum Teil mehrere Wochen auf Test-Instanzen für Abnahmen/Migrationen bereitstehen müssen, reichen diese Mechanismen leider nicht aus, aber durch die gut dokumentierte REST-API von GitLab ist eine einfache Erweiterung/Ersatz dieser Löschregeln schnell implementiert. Mit zwei einfachen Python-Skripten wird unsere GitLab-Registry täglich bereinigt und wir haben keine Probleme mit wachsendem Speicherbedarf.
Im nächsten Teil unserer Serie wird es darum gehen, wie die Ausführungszeiten der Pipelines optimiert werden können, wenn auf die GitLab-eigenen Mechanismen wie cache und artifacts verzichtet wird.


