Azure DevOps Cache for Speed

post-thumb

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:

  1. "npm" - ein fester String
  2. $(Agent.OS) - eine Azure-Laufzeitvariable, bei uns zumeist "Linux"
  3. 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!

Lernen Sie uns kennen

Das sind wir

Wir sind ein Software-Unternehmen mit Hauptsitz in Karlsruhe und auf die Umsetzung von Digitalstrategien durch vernetzte Cloud-Anwendungen spezialisiert. Wir sind Microsoft-Partner und erweitern Standard-Anwendungen bei Bedarf – egal ob Modernisierung, Integration, Implementierung von CRM- oder ERP-Systemen, Cloud Security oder Identity- und Access-Management: Wir unterstützen Sie!

Mehr über uns

Der Objektkultur-Newsletter

Mit unserem Newsletter informieren wir Sie stets über die neuesten Blogbeiträge,
Webcasts und weiteren spannenden Themen rund um die Digitalisierung.

Newsletter abonnieren