left-pad: Wie abhängig sind Builds von fremdem Code?

Keine Zeit? Hier die Kurzversion in 30 Sekunden.
- Am 22. März 2016 zog ein Entwickler nach einem Namensstreit seine Pakete aus npm zurück, darunter das 11 Zeilen kleine left-pad, und legte damit viele Builds lahm.
- npm hat das Paket wiederhergestellt und angekündigt, das Zurückziehen künftig einzuschränken.
- Maven Central löscht keine veröffentlichten Artefakte, aber transitive Abhängigkeiten, Snapshots und Ausfälle sind dieselbe Klasse von Risiko.
- Hilfreich sind ein eigener Repository-Proxy, fest gepinnte Versionen und ein Blick in den Abhängigkeitsbaum.
- Ob man eine Kleinstbibliothek einbindet, ist eine Abwägung, kein Dogma.
Vor ein paar Tagen ging eine Nachricht durch die Entwicklerwelt, die man als Randnotiz aus dem JavaScript-Lager abtun könnte. Ich glaube, das wäre ein Fehler. Es geht nicht um npm, es geht um eine Frage, die sich jedes Projekt stellen sollte: Was passiert mit meinem Build, wenn etwas verschwindet, das ich nicht geschrieben habe?
Was passiert ist
Am 22. März hat ein Entwickler nach einem Streit um einen Paketnamen (es ging um „kik“) all seine Pakete aus npm entfernt. Darunter war auch left-pad, eine Funktion, die einen String links mit Zeichen auffüllt. Das Paket besteht aus ungefähr elf Zeilen. Trotzdem hing es als Abhängigkeit von Abhängigkeiten in einer ganzen Reihe bekannter Projekte, unter anderem im Umfeld von Babel und React. Wer an dem Tag einen frischen Build startete, bekam einen Fehler, weil das Paket nicht mehr aufzulösen war.
npm hat das Paket nach kurzer Zeit wiederhergestellt, und es wurde angekündigt, die Regeln fürs Zurückziehen von Paketen zu ändern. Aus Sicht der Betroffenen war das Problem also innerhalb von Stunden erledigt. Interessant finde ich trotzdem, wie schnell es so weit kam, und wie wenig die Betroffenen selbst dazu beitragen konnten.
Spott über JavaScript halte ich für unangebracht. Das Ökosystem hat sich dafür entschieden, sehr viele sehr kleine Pakete zu verwenden, und das hat echte Vorteile. Die Struktur, die dahinter steckt, kennen wir aus der Java- und .NET-Welt genauso, nur mit anderen Details.
Und bei Java und .NET?
Bei Maven Central ist die Lage in einem Punkt komfortabler: Veröffentlichte Artefakte lassen sich nicht einfach löschen. Wer eine Version einmal hochgeladen hat, kann sie nicht zurückziehen, auch nicht im Streit. Diese Unveränderlichkeit ist eine bewusste Entscheidung, und gerade nach dem Vorfall sieht man, wie sinnvoll sie ist.
Damit ist das Thema allerdings nicht erledigt, denn das Risiko hat mehrere Gesichter:
- Transitive Abhängigkeiten. Man bindet eine Bibliothek ein und bekommt zwei Dutzend weitere dazu. Manche davon hat man nie bewusst ausgewählt.
- Snapshot-Versionen. Ein
-SNAPSHOTist per Definition veränderlich. Wer sich darauf verlässt, bekommt morgen vielleicht anderen Code als heute, ohne dass irgendjemand etwas „kaputt“ gemacht hätte. - Versionsbereiche. Ein Eintrag wie
[1.0,2.0)im POM sagt: Nimm, was gerade passt. Das ist bequem und macht Builds nicht reproduzierbar. - Erreichbarkeit. Ein Repository kann schlicht nicht erreichbar sein, zum Beispiel durch Wartung, Netzprobleme oder ein Firmenfirewall, der gerade neu konfiguriert wurde.
- Fremde Repositories. Kleinere Projekte liegen in eigenen Repositories irgendwo im Netz. Ob die in drei Jahren noch existieren, weiß niemand.
Bei NuGet in der .NET-Welt sieht es ähnlich aus. Pakete bekommen Abhängigkeiten, die wieder Abhängigkeiten haben, und mit den Versionsangaben geht man so großzügig oder so streng um, wie man es sich angewöhnt hat. Und auch im C++-Umfeld, wo ich mit Bibliotheken oft von Hand umgehe, gibt es das Problem, nur sieht man es dort deutlicher: Wer eine Bibliothek ins Projektverzeichnis kopiert, hat sie wenigstens sicher.
Was ich dagegen tun würde
Das Wichtigste ist ein eigener Repository-Proxy, etwa mit Nexus oder Artifactory. Alle Builds laufen dann nicht direkt gegen Maven Central, sondern gegen eine Instanz im eigenen Netz, die abgerufene Artefakte zwischenspeichert. Fällt das öffentliche Repository aus oder verschwindet dort etwas, hat man die benötigten Dateien trotzdem noch. Nebenbei werden Builds schneller und lassen sich auch ohne Internetzugang wiederholen. Der Aufwand für Betrieb und Pflege ist real, aber überschaubar.
An zweiter Stelle kommen gepinnte Versionen. Konkrete Versionsnummern statt Bereichen, und für Releases keine Snapshots in den Abhängigkeiten. Das klingt nach Selbstverständlichkeit, wird aber in der Praxis erstaunlich oft aufgeweicht, weil es kurzfristig bequem ist.
Drittens: den Abhängigkeitsbaum kennen. Ein
mvn dependency:treeist in zwei Minuten gelaufen und liefert oft Überraschungen. Bei mir war es mehr als einmal eine Bibliothek, von der ich nicht wusste, dass sie überhaupt im Projekt steckt, in zwei verschiedenen Versionen noch dazu. Mit dem Enforcer-Plugin lässt sich außerdem prüfen, dass sich die Versionen nicht widersprechen. Es lohnt sich, das nicht nur im Notfall anzusehen, sondern ab und zu aus Neugier.
Kleinstbibliotheken abwägen
Die eigentliche Debatte nach dem Vorfall dreht sich um die Frage, ob eine Bibliothek mit elf Zeilen überhaupt existieren sollte. Ich sehe beide Seiten. Wiederverwendung ist ein altes Ideal, und eine kleine, gut getestete Funktion hat ihren Wert. Gleichzeitig kostet jede Abhängigkeit etwas: Sie muss aktuell gehalten werden, sie kann Sicherheitslücken haben, sie kann verschwinden oder sich ändern, und sie macht den Baum unübersichtlicher.
Meine Faustregel ist unspektakulär. Wenn der Code kürzer ist als die Zeit, die ich für das Prüfen einer Bibliothek brauche, schreibe ich ihn selbst oder übernehme ihn bewusst ins Projekt. Bei allem, was sicherheitsrelevant oder fachlich knifflig ist, nehme ich dagegen lieber etwas Bewährtes, auch wenn es mehr mitbringt. Das ist keine Wissenschaft, nur eine Frage, die ich mir stelle, bevor ich eine Zeile im POM ergänze: Was kostet mich diese Abhängigkeit in zwei Jahren?
Auch das Gegenargument gilt. Wer alles selbst schreibt, schreibt auch alle Fehler selbst. Eine saubere Lösung gibt es nicht, nur eine bewusste Entscheidung.
Was ich daraus mitnehme
Der Vorfall hat nichts Neues erfunden. Er hat gezeigt, dass ein Build kein Zustand ist, den man einmal herstellt, sondern ein Prozess, der jedes Mal wieder auf fremde Systeme zugreift. Solange das gut geht, denkt man nicht darüber nach. Mir hat er die Gelegenheit gegeben, bei mehreren Projekten nachzusehen, wie viel Vertrauen sie eigentlich stillschweigend voraussetzen. Das Ergebnis war nicht schlimm, aber auch nicht so ruhig, wie ich es erwartet hatte.
Wer heute eine Stunde übrig hat, kann den Abhängigkeitsbaum seines wichtigsten Projekts ausgeben, nach unbekannten Namen suchen und prüfen, woher die Artefakte tatsächlich kommen. Danach weiß man mehr als vorher, und das ist meistens schon der halbe Gewinn.
Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (npm-Logo: Boboss74, gemeinfrei; Apache-Maven-Logo: Apache Software Foundation, gemeinfrei); die Marken gehören ihren Inhabern.
Christian Ackermann
Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.