Aus Linux-Magazin 08/2022

Wie sich Dependency-Management in Webanwendungen verlagert

© Iulian Dragomir / 123RF.com

Häufig pflegen Administratoren mit viel Aufwand ihre Webserver, Datenbanken und gewachsenen PHP-Installationen. Programmiersprachen wie Go wälzen die Verantwortung fast gänzlich auf die Entwickler einer Webanwendung ab – was sich unterm Strich sogar rentiert.

WordPress, Owncloud und zahlreiche andere Webanwendungen setzen einen Webserver, eine Datenbank sowie PHP voraus und benötigen ihre ganz eigene Umgebung. So verlangt beispielsweise Owncloud eine MySQL-kompatible Datenbank und PHP 7.4 [1]. Hinzu kommen verschiedene Erweiterungen oder Module wie die PHP-Module json und libxml, die je nach Applikation in bestimmten Versionen vorliegen müssen. Exotische oder selbst geschriebene Erweiterungen muss man vorab manuell kompilieren.

Darüber hinaus nutzen viele Webanwendungen in PHP geschriebene Bibliotheken. Owncloud [2] verwendet Archive_Tar [3] zum Jonglieren von TAR-Archiven sowie Teile des Symfony-Frameworks [4]. Beim Verwalten unterstützen Sie immerhin Tools wie Composer [5] (Abbildung 1). Als wäre das immer noch nicht genug, schreiben die Webanwendungen zusätzlich verschiedene Konfigurationseinstellungen wie den von ihnen benötigten Hauptspeicher vor.

Abbildung 1: Composer verwaltet in PHP geschriebene Bibliotheken, nicht aber PHP-Module.

Abbildung 1: Composer verwaltet in PHP geschriebene Bibliotheken, nicht aber PHP-Module.

Versionschaos entwirren

Admins müssen die verlangten Komponenten in der passenden Konfiguration aufsetzen und kontinuierlich warten, was alles andere als trivial ist, da jede neue Version einer Webanwendung andere Systemvoraussetzungen mitbringt oder das Ändern der Konfiguration erfordert. Erfahrungen des Owncloud-Teams zufolge taucht dabei ein weiteres großes Problem auf: Beim Einsatz von Linux vertrauen viele auf den Lieferumfang der Distributionen. Das klingt zunächst sogar löblich, denn einerseits testen deren Hersteller die von ihnen bereitgestellten Softwarepakete vor dem Ausliefern, und andererseits kümmert sich die Softwareverwaltung automatisch um regelmäßige Updates.

Das Angebot der Distributionen unterscheidet sich jedoch stark. Debian 11 “Bullseye” etwa offeriert in seinen Repositories nur PHP 7.4, und die mitgelieferten PHP-Module unterstützen nur dieses Release. Insbesondere in heterogenen Umgebungen müssen Sie deswegen häufig fehlende PHP-Module in passenden Versionen manuell an der Softwareverwaltung vorbei schmuggeln. Gleichzeitig müssen jedoch alle Komponenten kompatibel zueinander sein, was sich recht aufwendig gestaltet, da viele Distributionen auf gut abgehangene, stabile Programmversionen setzen. Zu solchen Linux-Systemen zählen unter anderem Debian und Red Hat Enterprise Linux sowie dessen Klone. Die dortigen, älteren PHP-Module sind unter Umständen mit anderen benötigten Kollegen oder der Webanwendung inkompatibel.

Test, Test, 1, 2, 3

Sobald die Applikation läuft, dürfen Sie nicht einfach gedankenlos alle Komponenten automatisch aktualisieren. Beispielsweise unterstützte das bei Redaktionsschluss aktuelle Owncloud 10.9 noch nicht offiziell PHP 8. Umgekehrt finden sich immer wieder gute Gründe, eine brandneue Version zu ignorieren: Nach dem Erscheinen einer neuen Major-Version etwa ist es sinnvoll, erste Fehlerkorrekturen abzuwarten. Unterm Strich müssen Administratoren den Umstieg auf neue PHP-Versionen also erst einmal blockieren.

Änderungen in Erweiterungen oder Workflows verursachen zusätzliche Schwierigkeiten. WordPress trieb mit dem Umbau seines Texteditors vielen Anwendern Schweißperlen auf die Stirn, weil zunächst einige WordPress-Erweiterungen ihn nicht unterstützten (Abbildung 2). Entsprechend muss jeder Aktualisierung ein umfassender Test vorausgehen, mitunter stehen sogar Migrationen an. Das ist beispielsweise der Fall, sobald lieb gewonnene WordPress-Erweiterungen eingestellt werden und Nutzer auf andere ausweichen müssen.

Abbildung 2: In WordPress besteht eine Seite aus Blöcken. Da dieses Vorgehen den Workflow in Redaktionen auf den Kopf stellte, tauschten einige Administratoren den Editor kurzerhand aus.

Abbildung 2: In WordPress besteht eine Seite aus Blöcken. Da dieses Vorgehen den Workflow in Redaktionen auf den Kopf stellte, tauschten einige Administratoren den Editor kurzerhand aus.

Komplexe Abhängigkeiten

Die komplexen Abhängigkeiten der Komponenten untereinander treiben nicht nur Admins in den Wahnsinn. Programmierer entwickeln und testen ihre Webanwendung meist in einer definierten Umgebung, im schlimmsten Fall mit dem alles erlaubenden Komplettpaket XAMPP auf dem eigenen Rechner [6]. In der Praxis stößt der Code zwangsweise auf abweichende Bedingungen wie restriktivere Konfigurationen. Selbst wenn Sie die Applikation nur in einer eigenen, definierten Umgebung hosten, sehen Sie sich irgendwann mit PHP-Updates oder anderen Änderungen konfrontiert.

Entsprechend müssen Entwickler ihre Webanwendung vorab in verschiedenen Konfigurationen und Umgebungen testen und kontinuierlich an unterschiedliche Bedingungen anpassen. Das gilt besonders nach dem Erscheinen neuer PHP-Versionen, die eine neue Syntax und andere Konzepte verwenden. Beispielsweise wertete PHP 8.0 erstmals den Vergleich »0 == false« als falsch aus. Bereits solche kleinen Änderungen führen dazu, dass sich komplexer Code in aktuellen PHP-Installationen plötzlich komplett anders verhält und fehlerhafte Ergebnisse erzeugt.

Umgekehrt laufen nach dem Erscheinen einer neuen Version in der Praxis noch viele Server mit älteren Fassungen, die es zu unterstützen gilt. Da besonders PHP häufig alte Zöpfe abschneidet, fällt es schwer, mehrere PHP-Versionen mit nur einer Codebasis abzudecken. Die Owncloud-Entwickler überprüfen ihr System auf einigen ausgewählten Linux-Distributionen mit den dort jeweils verfügbaren PHP- und Datenbankinstallationen. Auf jedem dieser Systeme probieren sie zudem verschiedene Konfigurationen und Kombinationen durch.

Docker lichtet das Chaos

Entlastung für Administratoren und Entwickler brachte Docker mit seinem Container-Prinzip: Jede benötigte Komponente läuft in einem eigenen Container mit einem vorgegebenen Aufbau. Sofern die Webanwendung exakt PHP 8.1.4 verlangt, bekommt sie es in einem passenden Docker-Container vorgesetzt. Nach einem PHP-Update werfen Sie den Container einfach weg und fahren einen anderen mit der neuen Version hoch. Sollte die Applikation mit dem neuen PHP-Release nicht zurechtkommen, ersetzen Sie den Container mit zwei Kommandozeilenbefehlen wieder durch einen mit der bislang genutzten PHP-Version. Dank separater Konfigurationsdateien und passender Tools – im einfachsten Fall Docker-Compose – lassen sich die Container sogar automatisiert aufsetzen und starten.

Viele Entwickler greifen inzwischen zu Node.js [7] oder dessen Konkurrenten Deno [8]. Diese Frameworks bestehen im Kern aus einer kompakten Javascript-Laufzeitumgebung. Beide bieten zusätzlich Schnittstellen, über die Javascript-Programme auf das Netzwerk oder das Dateisystem zugreifen. So lassen sich in Javascript schnell komplette Server-Anwendungen entwickeln, die vollständig ohne Webserver auskommen. Sie müssen lediglich die von der Applikation geforderte Node.js-Version auf dem Server installieren und aktuell halten. Node.js böte somit auf den ersten Blick einen eleganten Ausweg aus der Abhängigkeitshölle.

Der Node.js Package Manager NPM sorgt dafür, dass Sie eine Aufgabe nicht immer wieder neu lösen müssen. Über ihn beziehen Sie Bibliotheken für unterschiedliche Zwecke von Drittanbietern. Komplexere Node.js-Programme nutzen gleich eine ganze Batterie solcher Bibliotheken, die es auf dem Server einzurichten gilt. Einige Pakete müssen Sie zudem vor ihrem Einsatz kompilieren. Immerhin lassen sich über eine Konfigurationsdatei gezielt die benötigten Versionen und das Update-Verhalten steuern. So können Sie NPM beispielsweise anweisen, eine Bibliothek nur in dessen Versionsreihe 1.x einzusetzen und nicht eigenmächtig auf die Version 2.0 zu wechseln. Das funktioniert freilich nicht, wenn Sie Ihrer Applikation eine Bibliothek direkt beilegen und dadurch NPM umgehen. Die Pakete holt NPM aus einem eigenen Repository, dem Sie vertrauen müssen (Abbildung 3).

Abbildung 3: In der Vergangenheit kam es manchmal vor, dass Pakete aus dem NPM-Repository plötzlich verschwanden oder nicht mehr funktionierten und so komplette Webanwendungen lahmlegten.

Abbildung 3: In der Vergangenheit kam es manchmal vor, dass Pakete aus dem NPM-Repository plötzlich verschwanden oder nicht mehr funktionierten und so komplette Webanwendungen lahmlegten.

Nicht nur Node.js bringt einen Paketmanager mit: Für viele andere Sprachen existieren analoge Werkzeuge. In Python beispielsweise nutzen Sie den Paketmanager Pip. Dabei begegnen Sie jedoch ähnlichen Herausforderungen wie mit NPM: Sie müssen dem angezapften Repository vertrauen und können außerdem nur eingeschränkt kontrollieren, welche Prozesse und Updates wie auf dem eigenen System ablaufen. Schließlich müssen Sie die Abhängigkeiten doch selbst auflösen, was in der Praxis mitunter schwerfällt. Diese Erfahrungen machen Sie spätestens, sobald Sie von einer Linux-Distribution bereitgestellte Python-Pakete mit weiteren via Pip integrierten mischen müssen.

Beispiel für eine package.json

In Node.js steuert die Datei »package.json« unter anderem die benötigten Bibliotheken. Im Beispiel aus Listing 1 verlangt die Anwendung (genauer gesagt das Package) »mycalculator« eine Bibliothek namens »computer-gcd«, die mindestens in Version 1.2.1 vorliegen muss.

Listing 1

package.json

{
  "name": "mycalculator",
  "version": "1.0.0",
  "dependencies": {
    "compute-gcd": ">=1.2.1"
  }
}

Aufwendige Gaspedale

PHP, Javascript und Python sind interpretierte Sprachen, die den Vorteil haben, dass der Programmcode auf sämtlichen Plattformen läuft, für die ein entsprechender Interpreter existiert. Er wertet die Anweisungen jedoch erst zur Laufzeit aus. Das bremst die Anwendungen im Vergleich zu kompiliertem Code deutlich aus. Die Geschwindigkeitseinbußen fallen vor allem bei hohem Datenaufkommen und komplexen Webanwendungen auf.

Ein Lied davon können beispielsweise Nutzer des Content-Management-Systems Joomla [9] singen. Es lockt zwar mit flexibler Zugriffssteuerung, Workflows und interessanten Funktionen, der dafür unter der Haube notwendige komplexe PHP-Code jedoch verzögert massiv das Ausliefern einer Seite. Beschleunigung versprechen optimierte Interpreter oder auch JIT-Compiler. Einen solchen bietet beispielsweise PHP 8 von Haus aus an, das Pendant in Python heißt PyPy. Allerdings haben die alternativen Interpreter einen Haken: Die Webanwendung muss mit ihnen zurechtkommen, was den Test- und Wartungsaufwand erneut erhöht.

Als weitere Gegenmaßnahme hilft ein vorgeschalteter Cache, den Joomla von Haus anbietet (Abbildung 4). Die Owncloud-Entwickler empfehlen unter anderem das verteilt arbeitende Puffersystem Memcached [10]. Bei hoch frequentierten Seiten verteilen zudem Load Balancer die Anfragen auf mehrere Instanzen, die Sie allerdings synchronisieren müssen. Wie Owncloud unterstützen immerhin einige wenige Webanwendungen das Zusammenarbeiten verschiedener Instanzen (Abbildung 5). Die auf diese Weise jedoch noch verschärfte Abhängigkeitshölle sichert Administratoren zumindest zuverlässig ihren Arbeitsplatz.

Abbildung 4: Durch Codeoptimierungen reagiert Joomla in der aktuellen Version 4.x deutlich flotter. Dennoch ist weiterhin ein Cache an Bord, den Sie über die Einstiegsseite leeren.

Abbildung 4: Durch Codeoptimierungen reagiert Joomla in der aktuellen Version 4.x deutlich flotter. Dennoch ist weiterhin ein Cache an Bord, den Sie über die Einstiegsseite leeren.

Abbildung 5: Über die Federation-Funktion arbeiten mehrere Owncloud-Instanzen zusammen, was jedoch erhöhten Administrationsaufwand bedeutet.

Abbildung 5: Über die Federation-Funktion arbeiten mehrere Owncloud-Instanzen zusammen, was jedoch erhöhten Administrationsaufwand bedeutet.

Beschleunigen mit Go

Noch einen Schritt weiter als Node.js geht die Programmiersprache Go. Sie verlagert die Verantwortlichkeit für alle Abhängigkeiten auf die Entwickler. Dafür sorgt maßgeblich die Arbeitsweise des Go-Compilers: Er erzeugt ein statisch gelinktes, kompaktes Programm, das Sie nur noch auf Ihrem Server starten müssen. Weil ein Go-Programm keine weiteren Abhängigkeiten besitzt, müssen Sie lediglich die Applikation selbst und das Betriebssystem aktuell halten.

Da sich Go-Programme vor allem in Docker-Containern wohlfühlen, unterstützt die Programmiersprache direkt das DevOps-Prinzip. CI/CD-Systeme übersetzen das Programm, schieben es durch eine Test-Pipeline und veröffentlichen es schließlich automatisch als Docker-Container. Owncloud nutzt dazu beispielsweise DRONE [12]. Darüber hinaus unterstützt der Go-Compiler seit Version 1.5 Cross-Compiling und erzeugt passende native Programme für zahlreiche Betriebssysteme und Plattformen, die deutlich schneller laufen als interpretierte PHP- oder Python-Pendants. Diese und weitere interessante Eigenschaften steigerten Gos Beliebtheit bei Entwicklern von Server-Diensten äußerst schnell. Auch die Owncloud-Entwickler implementieren deshalb unter dem Namen Owncloud Infinite Scale (OCIS) gerade ihre Webanwendung in Go neu [13].

Jedes Go-Programm setzt sich aus Modulen zusammen. Während PHP keinen eigenen Paketmanager besitzt und bei Node.js der Administrator mit NPM alle notwendigen Pakete hinzuholen muss, verwaltet der Go-Compiler direkt die Module. Während des Übersetzens lädt er alle notwendigen Komponenten aus dem Internet und linkt sie statisch in das fertige Programm. Dank dieser Automatik benötigen Sie fürs eigentliche Übersetzen erstaunlich wenige Zutaten. Zum Kompilieren von OCIS genügt bereits Go ab Version 1.17. Den Prozess steuert dort allerdings zusätzlich Make, und das eigentliche Übersetzen findet in einem Docker-Container statt.

Abhängigkeitsautomatik

Wie Node.js sammelt Go alle benötigten Module in einer Konfigurationsdatei, die hier den Namen »go.mod« trägt. Ein Beispiel zeigt Listing 2. Sie müssen die Konfigurationsdatei nur in wenigen Fällen per Hand editieren, die meiste Arbeit übernimmt der Go-Compiler. Wollen Sie beispielsweise das Auswerten von Kommandozeilenparametern dem Cobra-Modul überlassen, genügt es, es mit der Anweisung »import “github.com/spf13/cobra”« im Quellcode zu importieren. Anschließend können Sie direkt seine Funktionen nutzen.

Listing 2

Einfache go.mod

module mycalculator
go 1.17
require github.com/spf13/cobra v1.4.0
require (
  github.com/inconshreveable/mousetrap v1.0.0 // indirect
  github.com/spf13/pflag v1.0.5 // indirect
)

Vor dem Kompilieren geht der Befehl »go mod tidy« den Quellcode durch, trägt alle gefundenen externen Module in die Konfigurationsdatei »go.mod« ein und lädt sie gleichzeitig herunter. Im Fall des Cobra-Moduls holt Go den zugehörigen Quellcode aus dem Github-Repository unter »github.com/spf13/cobra/«. Hängt ein Modul von weiteren Kollegen ab, löst Go diese Abhängigkeiten automatisch auf. Zudem stellt »go mod tidy« sicher, dass ungenutzte Module aus dem Projekt verschwinden.

Alternativ zum Aufruf »go mod tidy« können Sie explizit ein Modul herunterladen. So bindet »go get -u github.com/spf13/cobra@v1.2.1« Cobra in der Version 1.2.1 ein. Der Parameter »-u« sorgt dafür, dass Go den auf der Festplatte liegenden Cobra-Code entsprechend aktualisiert. Auf diesem Weg klappt zudem jederzeit ein Downgrade, und bei Bedarf lassen sich die Module auf einem speziellen Proxy-Server zwischenlagern.

In »go.mod« geben Sie unter anderem vor, in welchen Versionen die Module jeweils mindestens vorliegen müssen. Zudem lassen sich bestimmte Versionen ausschließen und Module durch andere ersetzen. Den allgemeinen Go-Regeln folgend nutzen die Module das Semantic Versioning Model [14]. Damit müssen Sie sich nicht mit kryptischen Versionsnummernangaben herumschlagen und können den Entwicklungsstand leicht ablesen: Module mit der Versionsnummer 0.x eignen sich noch nicht für den produktiven Einsatz.

Da Module selbst von weiteren Modulen abhängen, entsteht schnell eine Kaskade aus zahlreichen Modulen, die Sie sich wiederum zwangsweise in das eigene Programm linken. Die Owncloud-Entwickler raten daher dazu, sich regelmäßig sämtliche Abhängigkeiten anzeigen zu lassen und gegebenenfalls aufzuräumen. Hierbei helfen die Befehle »go list« und »go mod graph«. Des Weiteren lässt sich in Go-Programmen C-Code nutzen, was das Verwalten der Abhängigkeiten verkompliziert. Das gilt insbesondere, wenn Sie Ihre Applikation für mehrere Plattformen bereitstellen möchten.

Stopft ein aktualisiertes Modul eine Sicherheitslücke, müssen Go-Entwickler ein neues Binary der Anwendung erzeugen und es so schnell wie möglich ausliefern. Sie sind somit indirekt für Fehler und Sicherheitslücken in den externen Modulen mitverantwortlich, was aufwendigere Wartung und Regressionstests nach sich zieht.

Darüber hinaus benötigen einige Go-Programme die Hilfe einer Datenbank und anderer Anwendungen. OCIS erlaubt beispielsweise, sich via LDAP anzumelden, und unterstützt verschiedene Speichersysteme und Reverse Proxies. Später im Betrieb müssen Sie alle diese Komponenten wie gehabt aufsetzen und warten. Umgekehrt müssen auch die Entwickler ihre Software gegen diese Komponenten testen [15].

Mitunter greifen Go-Programme außerdem auf die Dienste externer Tools wie ImageMagick zurück. Hinzu kommen bei vielen Webanwendungen Grafiken, HTML-Seiten, CSS-Vorlagen und weitere Assets. Komplexe Server-Dienste bestehen aus Microservices, die es ebenfalls intelligent zu verwalten gilt. Vollständig arbeitslos werden Administratoren also mit Go-Programmen nicht.

Fazit: Etwas für jeden

Das gute alte Gespann aus Webserver, Datenbank und PHP gehört noch nicht zum alten Eisen. Es bildet nach wie vor das Rückgrat vieler Webseiten, zudem setzen zahlreiche Webanwendungen nach wie vor PHP voraus. Durch die weite Verbreitung existieren eine große Wissensbasis, eine engagierte Community und unzählige hilfreiche Bibliotheken. Entsprechende Webanwendungen lassen sich zudem meist schnell in Betrieb nehmen. Gerade bei komplexeren Server-Diensten stößt eine PHP-Applikation jedoch an ihre Grenzen. Administratoren müssen dann viele Abhängigkeiten entwirren und vor Updates jeweils ausführlich testen.

Spätestens an diesem Punkt bietet es sich an, auf moderne Konzepte und Sprachen zu setzen. Go erzwingt hier einen radikalen Paradigmenwechsel, indem es die Entwickler sämtliche Abhängigkeiten verwalten lässt. Damit fördert es gleichzeitig das DevOps-Prinzip, und die kompilierten Anwendungen laufen deutlich flotter als ihre interpretierten Pendants. Vor allem wer Microservices und komplexere Anwendungen umsetzen möchte, dürfte daher Go zu schätzen wissen. (csi)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben