Schneller ist besser, sagen die Kollegen. Verbrauch mal nicht so viel Bandbreite, sagen sie. Recht haben sie. Das Zauberwort heisst Cache.
Die Älteren werden sich erinnern, dass man früher selbst den Versand von HTML-formatierter Email kritisch sah, weil diese ja viel mehr Kilobytes benötigte als ihr nur-Text-Pendant. Ist alles Schnee von gestern, seit heute selbst Updates für Spiele leicht viele Gigabyte Download beanspruchen.
Aber es macht immer noch Sinn, Downloads zu begrenzen: In der Continuous-Integration-Pipeline, kurz CI. Warum? Dort wird immer wieder Software gebaut und je nach Teamgröße kann dies durchaus hunderte Mal am Tag passieren, in nur einem Team. All die npm install
und mvn package
laden benötigte Daten jedes Mal frisch herunter, auch Docker-Images sind für ihren Datenhunger bekannt.
Warum brauchen wir so viele Daten?
Kaum eine Programmiersprache kommt heute ohne einen Paketmanager aus, durch den Abhängigkeiten für das aktuelle Projekt erfüllt werden. Unser derzeitiges Projekt verwendet neben Javascript/Typescript auch Spring Boot mit Maven. Die großen Datenmengen sehen so aus:
|
Typ |
Ort |
Größe |
Frontend |
NPM |
./node_modules |
669 MB |
Backend 1 |
Maven |
~/.m2/repository |
126 MB |
Backend 2 |
Maven |
~/.m2/repository |
93 MB |
888 MB an Abhängigkeiten, allein zum Bauen der drei Teile. Knapp 87 Gigabyte Download pro Tag, nur weil unser Projekt ständig baut. Holy bandwidth, batman!
Meet cache
Der Azure-DevOps-Task Cache@2
bietet sich an, dieses Problem zu lösen: Anstatt diese Datenmenge ständig neu herunterzuladen (und damit Infrastruktur und NPM/Maven zu belasten), können wir die heruntergeladenen Daten in der Azure-Welt zwischenspeichern. Sie müssen dann zwar immer noch auf den Agenten geladen werden, dies ist aber viel schneller als der Download aus dem weiten Internet.
Dabei ist der Cache@2
Task Werkzeug-agnostisch: Jedes Tool, das Daten im Filesystem speichert, kann prinzipiell mit dem Cache-Task verwendet werden. Neben den angesprochenen Tools NPM und Maven finden sich Hinweise zur Konfiguration mit Ruby, Ccache, Docker, Go, Gradle, .NET, Yarn, Python und PHP.
## establish NPM cache
- task: Cache@2
displayName: Cache NPM
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
path: "$(Pipeline.Workspace)/.npm"
## Install Node Modules
- task: Npm@1
displayName: Install NPM packages
inputs:
command: ci
workingDir: "$(working_directory)"
In diesem Beispiel erzeugen wir den Cache für das Frontend-Modul, welches NPM verwendet. Die genaue Konfiguration ist stets abhängig vom jeweiligen Paketmanager, die Dokumentation
hält zahlreiche Beispiele bereit. Am Rande: Wusstest du, dass die Quellen aller Azure-DevOps-Tasks
auf Github öffentlich sind?
Fallstricke
“There are only two hard things in Computer Science: cache invalidation and naming things.”
Phil Karlton
Ein Cache ist eine tolle Sache, solange bis er nicht richtig funktioniert. Der Cache@2
Task von Azure kümmert sich um das Problem der Cache-Identifikation, indem ein oder mehrere ID-Bestandteile zu einem Fingerprint zusammengeführt werden. Im Beispiel oben sind das die drei mit |
getrennten Teile des Attributes key
:
"npm"
- ein fester String
$(Agent.OS)
- eine Azure-Laufzeitvariable, bei uns zumeist "Linux"
- die Datei
package-lock.json
bzw deren MD5 Hash.
Wenn sich die Datei package-lock.json
verändert (die ihr natürlich alle brav im git versioniert, richtig?) dann ändert sich der dritte Bestandteil der Cache-Identifikation - und damit die gesamte ID. Dadurch wird ein neuer Cache erstellt, welcher beim ersten Mal befüllt wird.
Hier muss man aufpassen: Manche build-Tools verändern die als Identifikation angegebene Datei. So verändert der Maven@4
Task und seine Vorgänger die Datei pom.xml
, deren Verwendung als Identifikation eigentlich selbstverständlich ist. Lösung: Man kopiert die Datei vor der Veränderung und benutzt die Kopie als Cache-Bestandteil:
## make backup of pom.xml because Maven task will change it and break cache id
- script: cp pom.xml pom.xml.orig
## establish Maven cache
- task: Cache@2
displayName: Cache Maven local repo
inputs:
key: 'maven | "$(Agent.OS)" | pom.xml.orig'
path: $(Pipeline.Workspace)/.m2/repository
## build, this will change the original pom.xml
- task: Maven@4
displayName: Build & Test
inputs:
options: --no-transfer-progress
goals: package
mavenPOMFile: pom.xml
mavenOptions: "-Dmaven.repo.local=$(Pipeline.Workspace)/.m2/repository"
Tipp: Alte Caches müssen nicht abgeräumt werden, da sie 7 Tage nach der letzten Verwendung automatisch gelöscht werden. Auch erzeugen Caches keine Extrakosten, danke Microsoft.
Fazit
Caching verkürzt unsere Pipeline-Zeiten erheblich, ist gut für unser Projekt und alle anderen Nutzer der Infrastruktur - und sollte daher in jedem Azure-CI-Projekt Verwendung finden. Viel Spaß beim Cachen!