Alles neu macht der Bot: Bibliotheken aktualisieren mit Scala-Steward und Renovate

Oha, dieses Projekt haben wir lange nicht angefasst. Die benutzten Bibliotheken hängen voller Spinnweben, und auch die Programmiersprachenversion war schon vor einem Jahr nicht mehr aktuell. Bevor ich loslegen kann, muss hier erst einmal aufgeräumt werden… Über Sicherheitslücken in den alten Bibliotheken will ich gar nicht nachdenken.

Da kommen mir diese beiden sympathischen Helfer gelegen: Scala-Steward und Renovate treten an, alle Projekte laufend aktuell zu halten. Nie mehr Aufräumen, das Gegenstück zum Staubsaugerroboter. Aber welcher Bot ist besser, und wo sind die Fallstricke?

Fahrrad - lange nicht benutzt

Scala-Steward und Renovate

Beide Bots legen für neue Bibliotheks-Versionen Merge-Requests an. Der Scala-Steward beschränkt sich dabei auf Scala, mit Renovate lassen sich auch andere Programmiersprachen aktualisieren wie JavaScript, Python und sogar Docker-Baseimages.

Es gibt auch noch andere vergleichbare Dienste wie Githubs dependabot, dependencies.io oder snyk, wir nicht ausprobiert haben.

Viel Microservice, viel Ehr

Aktualisiert man alle Bibliotheken regelmäßig, kann das umfangreich werden: Wir benutzen die Bots in einem größeren Webshop-Projekt in Gitlab mit ca. 50 Microservices und knapp 10 internen Bibliotheken. Genügend Ecken, in denen sich Staub ansammeln kann. Jeder der Microservices hat 10-20 Abhängigkeiten, mehrmals in der Woche wird in jedem Service eine interne oder externe Bibliothek aktualisiert.

Wir aktualisieren dabei automatisch auf die neuste Version von externen Bibliothken und auch von internen, selbstgeschrieben Bibliotheken. Gerade die internen Bibliotheken multiplizieren die Updates: Wenn eine viel benutze Open-Source-Bibliothek aktualisiert wird (zum Beispiel etwas von apache-commons oder akka), werden fast alle Services und internen Bibliotheken aktualisiert. Da es damit von der internen Bibliothek auch eine neue Version gibt, werden zusätzlich noch einmal alle Services aktualisiert, die die interne Bibliothek benutzen.

Continuous deployment

Selbst ein gründlicher Mensch wird bei dieser Menge an Services und Updates nur prüfen, dass die Tests grün sind, und so haben wir nach einer kurzen Testphase alle Aktualisierungen automatisch gemerged und deployed. Die große Frage: Ging dabei etwas kaputt? Natürlich rumpelt es von Zeit zu Zeit: fast alle Fehler fielen sofort im Unit- oder Integrationstest auf. In wenigen Fällen schafften es Fehler bis zum Live-Deployment, das dann beim ersten Start abbrach - typischerweise wegen inkompatibler Bibliotheks-Versionen. Nur sehr selten wirkte sich ein Bug in der Produktion aus - so hatten wir die “Ehre”, die ersten zu sein, die ein Akka-Streams-Memory-Leak melden konnten.

Für unseren Webshop war es ein guter Kompromiss, die Bots so einzustellen, dass sie nur während unserer Arbeitszeiten deployen, damit wir schnell auf etwaige Probleme reagieren können. Und noch eine Warnung: Bevor man automatisch Updates aus verschiedenen Repositories zieht, sollte man auch über Supply-Chain-Attacken nachdenken.

So viel zum Big Picture - wie aber schlugen sich Scala-Steward und Renovate im Einzelvergleich?

Der spartanische: Scala-Steward

Der Scala-Steward findet sehr zuverlässig neue Versionen der Libraries, und legt dann für jedes Update einen einzelnen Merge-Request an. Ansonsten ist er eher spartanisch ausgestattet, und wir haben einiges an Tooling vermisst:

Auto-merge

Wie oben beschrieben, wollen wir die Aktualisierungen automatisch mergen. Das Automerge-Tooling von Scala-Steward gibt es aber leider laut FAQ nur für Github, und nicht für Gitlab. Wir halfen uns mit einem kleinen Skript, das über die Gitlab-Rest-API regelmäßig Merge-Requests mit grünen Tests mergte.

Auto-bump

Die internen Bibliotheken haben bei uns Versionsnummern. Bei jeder Aktualisierung - auch durch die Bots - muss die Versionsnummer erhöht werden, damit die Bibliothek publiziert werden kann. Das war erstaunlich kompliziert zu erreichen, da dafür ein zusätzlicher Commit notwendig ist. Uns half das Plugin sbt-release.

Auto-rebase

Wir verwenden im Projekt “Fast-forward merges”. Gibt es zwei Updates für einen Service, gibt es auch zwei parallele Merge-Requests. Für den zweiten Merge-Request müssen wir ein Rebase auf den ersten machen. So erweiterten wir unser Auto-merge-Skript um das automatische Rebasen… die Komplexität stieg.

Erschwerender “fun fact”: Wenn sich benachbarte Zeilen ändern, also zwei direkt nebeneinander stehende Bibliotheken aktualisiert werden, tut sich git schwer, ein automatisches rebase zu machen, und der Konflikt muss manuell gelöst werden.

Auto-Combine

Letzendlich stellten wir fest, dass die kleinteiligen Merge-Requests von Scala-Steward unsere Continuous Integration Pipeline durch die andauernden Rebases, Tests und Deployments ans Limit brachten: Die Entwickler:innen beschwerten sich, dass sie wegen Stau in den Pipelines nicht mehr arbeiten konnten.

Sinnvoller als viele kleinteilige Merge-Requests ist es, alle zu einem Zeitpunkt offenen Updates für einen Service zu einem großen Merge-Requests zusammenzufassen: Es gibt nur ein Deployment für alle gemeinsamen Updates, die Rebases entfallen, und in der Commit-Historie es gibt nur einen Commit. Scala Steward without the noise beschreibt eine Lösung für Github, für Gitlab hätten wir sicher auch einen Weg gefunden. Wir hatten zu diesem Zeitpunkt aber genug davon, Tooling für den Scala-Steward zu bauen, und schauten uns Renovate an.

Hausnummer - aktualisiert

Der Generalist: Renovate

Renovate bringt eine Wundertüte an Tooling mit: Automerge, Rebases, Zusammenfassen von Updates in einem Merge-Request und wohl (von uns nicht verwendet) auch ein Auto-Bump von Library-Versionsnummern. Unser gesamter Verhau an Skripten wurde hinfällig.

Alle Pipelines und Updates liefen gut, auch die CI war ruhig. Verdächtig ruhig. Zu ruhig?

Gegencheck

Wir hatten den Eindruck, dass nicht alle Bibliotheken aktualisiert wurden, für die es eigentlich Updates gab. Ein Gegencheck mit sbt-updates bestätigte den Verdacht: Einige Bibliotheken blieben tatsächlich veraltet. Aber warum?

Regex gegen Scala

Was wir lernten: Der Renovate Bot ist in JavaScript geschrieben, und versucht mit einem relativ einfachen Regex-Parser alle möglichen Programmiersprachen zu parsen. Kann das gut gehen, damit eine Scala build.sbt zu verstehen? You do the math.

Grundsätzlich wirkt Renovate schon recht clever, zum Beispiel ist es kein Problem, Versionsummmern in Variablen auszulagern. Wir stellten aber fest, dass er einige Varianten - alles gültiger Scala-Code - nicht verstand. Hier ein paar Beispiele aus unserer Praxis:

ok:
  lazy val compileDependencies = Seq(
not ok:
  lazy val compileDependencies = {
    Seq(

ok:
  lazy val compileDependencies = Seq(
not ok:
  def compileDependencies = Seq(

ok:
  lazy val compileDependencies = Seq(
not ok:
  lazy val compileDependencies =
    Seq(

Jeweils die zweite Variante wurde nicht als Dependency erkannt und damit auch nicht aktualisiert, stillschweigend. Tritt so ein Fall auf, gibt es zwei Möglichkeiten: Beim Renovate einen Bug-Report stellen, damit diese nachbessern; oder zähneknirschend die build.sbt auf eine etwas üblichere Schreibweise ändern, wenn möglich.

Spieglein, Spieglein an der Wand

Noch eine zweite Falle sei kurz erwähnt: Wir hatten in unserer Infrastruktur einen internen Mirror von Maven-Central, der teilweise nur ältere Versionen externer Bibliotheken vorhielt, und nicht die aktuelle Version. Weil dieser in der Renovate-Config bei den registryUrls vorne stand, wurde Maven-Central gar nicht mehr gefragt und damit nicht die aktuelle Version gefunden. Lösung war einfach ein Tausch der Reihenfolge, doch Obacht: Es erhöht das Risiko für Supply-Chain-Attacks, wenn auch für interne Bibliotheken zuerst in Maven-Central gesucht wird.

Mit dieser Änderung der Konfiguration und der angepassten builds.sbt aktualisierte Renovate endlich alle Bibliotheken.

Und der Gewinner ist…

Wegen des besseren Toolings haben wir uns klar für Renovate entschieden. Allerdings unter der Prämisse, dass wir von Zeit zu Zeit mit sbt-updates prüfen, ob wir wirklich alle Bibliotheken aktualisieren. Schöner Bonus: Mit Renovate können wir auch Basis-Images, JavaScript und Python aktualisieren.

Kommentare