xenvman: Flexible Microservice-Testumgebungen (und mehr)

Hallo allerseits!


Ich möchte ein wenig über das Projekt sprechen, an dem ich in den letzten sechs Monaten gearbeitet habe. Ich mache das Projekt in meiner Freizeit, aber die Motivation für seine Entstehung kam von den Beobachtungen, die bei der Hauptarbeit gemacht wurden.


In einem Arbeitsprojekt verwenden wir die Architektur von Microservices. Eines der Hauptprobleme, das sich im Laufe der Zeit manifestiert hat, und die zunehmende Anzahl dieser Services ist das Testen. Wenn ein bestimmter Dienst von fünf bis sieben anderen Diensten sowie einer anderen Datenbank (oder sogar mehreren) zum Booten abhängt, ist es sehr unpraktisch, ihn sozusagen in einer "Live" -Form zu testen. Sie müssen Mokas von allen Seiten so fest anziehen, dass Sie nicht einmal den Teig selbst erkennen können. Nun, oder organisieren Sie irgendwie eine Testumgebung, in der alle Abhängigkeiten wirklich gestartet werden können.


Um die zweite Option zu vereinfachen, habe ich mich einfach hingesetzt , um xenvman zu schreiben. Kurz gesagt, dies ist so etwas wie ein Docker-Compose- und Testcontainer-Hybrid, nur ohne Bindung an Java (oder eine andere Sprache) und mit der Fähigkeit, Umgebungen dynamisch über die HTTP-API zu erstellen und zu konfigurieren.


xenvman in Go geschrieben und als einfacher HTTP-Server implementiert, mit dem Sie alle verfügbaren Funktionen aus jeder Sprache nutzen können, die dieses Protokoll sprechen kann.


Die Hauptsache, die Xenvman tun kann, ist:


  • Beschreiben Sie Umgebungsinhalte flexibel mit einfachen JavaScript-Skripten
  • Erstellen Sie Bilder im laufenden Betrieb
  • Erstellen Sie die richtige Anzahl von Containern und kombinieren Sie sie zu einem einzigen isolierten Netzwerk
  • Leiten Sie interne Ports der Umgebung nach außen weiter, damit Tests die erforderlichen Dienste auch von anderen Hosts aus erreichen können
  • Ändern Sie die Zusammensetzung der Umgebung (Stoppen, Starten und Hinzufügen neuer Container) unterwegs dynamisch, ohne die Arbeitsumgebung anzuhalten.

Umwelt


Die Hauptfigur in Xenvman ist die Umgebung. Dies ist eine solche isolierte Blase, in der alle erforderlichen Abhängigkeiten (in Docker-Containern verpackt) Ihres Dienstes gestartet werden.



Die obige Abbildung zeigt den Xenvman-Server und die aktiven Umgebungen, in denen verschiedene Dienste und Datenbanken ausgeführt werden. Jede Umgebung wurde direkt aus dem Integrationstestcode erstellt und wird nach Abschluss gelöscht.


Muster


Was direkt Teil der Umgebung ist, wird durch Vorlagen bestimmt, bei denen es sich um kleine Skripte in JS handelt. xenvman verfügt über einen integrierten Interpreter dieser Sprache. Wenn eine Anforderung zum Erstellen einer neuen Umgebung eingeht, werden einfach die angegebenen Vorlagen ausgeführt, von denen jede einen oder mehrere Container zur Ausführung zur Liste hinzufügt.


JavaScript wurde ausgewählt, um das dynamische Ändern / Hinzufügen von Vorlagen zu ermöglichen, ohne dass der Server neu erstellt werden muss. Darüber hinaus verwenden die Vorlagen normalerweise nur die grundlegenden Funktionen und Datentypen der Sprache (das gute alte ES5, kein DOM, React und andere Magie), sodass die Arbeit mit Vorlagen selbst für diejenigen, die JS überhaupt kennen, keine besonderen Schwierigkeiten bereiten sollte.


Vorlagen sind parametrierbar, dh wir können die Logik der Vorlage vollständig steuern, indem wir bestimmte Parameter in unserer HTTP-Anforderung übergeben.


Erstellen Sie Bilder im laufenden Betrieb


Eine der bequemsten Funktionen von xenvman ist meiner Meinung nach die Erstellung von Docker-Images direkt während der Konfiguration der Umgebung. Warum könnte dies notwendig sein?
Um beispielsweise in unserem Projekt ein Image eines Dienstes zu erhalten, müssen Sie die Änderungen in einem separaten Zweig festschreiben, starten und warten, bis Gitlab CI das Image sammelt und ausfüllt.
Wenn sich nur ein Dienst geändert hat, kann dies 3-5 Minuten dauern.


Und wenn wir aktiv neue Funktionen in unserem Service sägen oder versuchen zu verstehen, warum dies nicht funktioniert, das gute alte fmt.Printf hin und her hinzufügen oder den Code häufig irgendwie ändern, ist selbst eine Verzögerung von 5 Minuten großartig, um die Leistung zu löschen ( unsere als Codeschreiber). Stattdessen können wir dem Code einfach alle erforderlichen Fehlerbehebungen hinzufügen, ihn lokal kompilieren und dann einfach die fertige Binärdatei an die HTTP-Anforderung anhängen.


Nach Erhalt dieser Genehmigung nimmt die Vorlage diese Binärdatei und erstellt unterwegs ein temporäres Image, von dem aus wir den Container bereits starten können, als wäre nichts passiert.


In unserem Projekt prüfen wir beispielsweise in der Hauptvorlage für Dienste, ob die Binärdatei in den Parametern vorhanden ist, und wenn ja, sammeln wir das Bild unterwegs, andernfalls laden wir einfach die latest Version des dev herunter. Der weitere Code zum Erstellen von Containern ist für beide Optionen identisch.


Ein kleines Beispiel


Schauen wir uns zur Verdeutlichung das Mikrobeispiel an.


Nehmen wir an, wir schreiben eine Art Wunderserver (nennen wir es wut ), der eine Datenbank benötigt, um alles dort zu speichern. Nun, als Basis haben wir MongoDB gewählt. Für vollständige Tests benötigen wir daher einen funktionierenden Mongo-Server. Natürlich können Sie es lokal installieren und ausführen, aber zur Vereinfachung und Sichtbarkeit des Beispiels gehen wir davon aus, dass dies aus irgendeinem Grund schwierig ist (bei anderen, komplexeren Konfigurationen in realen Systemen entspricht dies eher der Wahrheit).


Wir werden also versuchen, xenvman zu verwenden, um eine Umgebung mit Mongo und unserem wut Server zu erstellen.


Zunächst müssen wir ein Basisverzeichnis erstellen, in dem alle Vorlagen gespeichert werden:


$ mkdir xenv-templates && cd xenv-templates


Erstellen Sie als Nächstes zwei Vorlagen, eine für Mongo und eine für unseren Server:


$ touch mongo.tpl.js wut.tpl.js


mongo.tpl.js


Öffnen Sie mongo.tpl.js und schreiben Sie dort Folgendes:


 function execute(tpl, params) { var img = tpl.FetchImage(fmt("mongo:%s", params.tag)); var cont = img.NewContainer("mongo"); cont.SetLabel("mongo", "true"); cont.SetPorts(27017); cont.AddReadinessCheck("net", { "protocol": "tcp", "address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}' }); } 

Die Vorlage muss eine execute () - Funktion mit zwei Parametern haben.
Die erste ist eine Instanz des tpl-Objekts, über das die Umgebung konfiguriert wird. Das zweite Argument (params) ist nur ein JSON-Objekt, mit dem wir unsere Vorlage parametrisieren.


In der Schlange


 var img = tpl.FetchImage(fmt("mongo:%s", params.tag)); 

Wir bitten Xenvman, das Docker- mongo:<tag> Image herunterzuladen mongo:<tag> , wobei <tag> die Version des Images ist, das wir verwenden möchten. Im Prinzip entspricht diese Zeile dem docker pull mongo:<tag> tpl docker pull mongo:<tag> , mit dem einzigen Unterschied, dass alle Funktionen des tpl Objekts im Wesentlichen deklarativ sind, tpl das Image wird tatsächlich erst heruntergeladen, nachdem xenvman alle in der Umgebungskonfiguration angegebenen Vorlagen ausgeführt hat.


Nachdem wir das Bild haben, können wir einen Container erstellen:


 var cont = img.NewContainer("mongo"); 

Auch hier wird der Container an dieser Stelle nicht sofort erstellt, wir erklären einfach die Absicht, ihn sozusagen zu erstellen.


Als nächstes bringen wir ein Etikett auf unserem Behälter an:


 cont.SetLabel("mongo", "true"); 

Verknüpfungen werden verwendet, damit sich Container in einer Umgebung finden können, um beispielsweise die IP-Adresse oder den Hostnamen in die Konfigurationsdatei einzugeben.


Jetzt müssen wir den internen Mongo-Port (27017) aufhängen. Dies ist einfach so zu bewerkstelligen:


  cont.SetPorts(27017); 

Bevor xenvman über die erfolgreiche Erstellung der Umgebung berichtet, sollten Sie sicherstellen, dass alle Dienste nicht nur ausgeführt werden, sondern auch Anfragen annehmen können. Xenvman hat hierfür Bereitschaftsprüfungen .
Fügen Sie eine solche für unseren Mongo-Behälter hinzu:


  cont.AddReadinessCheck("net", { "protocol": "tcp", "address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}' }); 

Wie wir sehen können, gibt es hier in der Adressleiste Stubs, in die die erforderlichen Werte unmittelbar vor dem Start der Container dynamisch eingesetzt werden.


Anstelle von {{.ExternalAddress}} externe Adresse des Hosts, auf dem xenvman ausgeführt wird, ersetzt, und anstelle von {{.Self.ExposedPort 27017}} externe Port ersetzt, der an den internen 27017 weitergeleitet wurde.


Lesen Sie hier mehr über Interpolation.


Infolgedessen können wir eine Verbindung zum Mongo herstellen, der in der Umgebung ausgeführt wird, beispielsweise direkt außerhalb des Hosts, auf dem wir unseren Test ausführen.


wut.tpl.js


Also, c, nachdem wir uns mit der Monga befasst haben, werden wir eine weitere Vorlage für unseren wut Server schreiben.
Da wir das Bild unterwegs sammeln möchten, unterscheidet sich die Vorlage geringfügig:


 function execute(tpl, params) { var img = tpl.BuildImage("wut-image"); img.CopyDataToWorkspace("Dockerfile"); // Extract server binary var bin = type.FromBase64("binary", params.binary); img.AddFileToWorkspace("wut", bin, 0755); // Create container var cont = img.NewContainer("wut"); cont.MountData("config.toml", "/config.toml", {"interpolate": true}); cont.SetPorts(params.port); cont.AddReadinessCheck("http", { "url": fmt('http://{{.ExternalAddress}}:{{.Self.ExposedPort %v}}/', params.port), "codes": [200] }); } 

Da wir BuildImage() Image hier BuildImage() , verwenden wir BuildImage() anstelle von FetchImage() :


  var img = tpl.BuildImage("wut-image"); 

Um das Bild zusammenzusetzen, benötigen wir mehrere Dateien:
Dockerfile - Anweisungen zum Zusammenstellen eines Bildes
config.toml - Konfigurationsdatei für unseren wut Server


Verwenden der Methode img.CopyDataToWorkspace("Dockerfile"); Wir kopieren die Docker-Datei aus dem Vorlagendatenverzeichnis in ein temporäres Arbeitsverzeichnis .


Das Datenverzeichnis ist ein Verzeichnis, in dem wir alle Dateien speichern können, die für die Funktion unserer Vorlage erforderlich sind.


Im temporären Arbeitsverzeichnis kopieren wir die Dateien (mit img.CopyDataToWorkspace ()), die in das Bild gelangen.


Folgendes folgt:


  // Extract server binary var bin = type.FromBase64("binary", params.binary); img.AddFileToWorkspace("wut", bin, 0755); 

Wir übergeben die Binärdatei unseres Servers direkt in den Parametern in codierter (base64) Form. Und in der Vorlage dekodieren wir sie einfach und speichern die resultierende Zeichenfolge im Arbeitsverzeichnis als Datei unter dem Namen wut .


Erstellen Sie dann einen Container und hängen Sie die Konfigurationsdatei darin ein:


  var cont = img.NewContainer("wut"); cont.MountData("config.toml", "/config.toml", {"interpolate": true}); 

Ein Aufruf von MountData() bedeutet, dass die Datei config.toml aus dem Datenverzeichnis im Container unter dem Namen /config.toml . Das interpolate Flag teilt xenvman dem Server mit, dass alle dortigen Stubs vor dem Mounten in der Datei ersetzt werden sollen.


So könnte die Konfiguration aussehen:


 {{with .ContainerWithLabel "mongo" "" -}} mongo = "{{.Hostname}}/wut" {{- end}} 

Hier suchen wir nach dem Container mit dem mongo Label und ersetzen seinen Hostnamen, unabhängig davon, um was es sich in dieser Umgebung handelt.


Nach dem Ersetzen sieht die Datei möglicherweise folgendermaßen aus:


 mongo = “mongo.0.mongo.xenv/wut” 

Als nächstes veröffentlichen wir erneut den Port und starten eine Bereitschaftsprüfung, diesmal HTTP:


 cont.SetPorts(params.port); cont.AddReadinessCheck("http", { "url": fmt('http://{{.ExternalAddress}}:{{.Self.ExposedPort %v}}/', params.port), "codes": [200] }); 

Unsere Vorlagen sind dafür bereit und wir können sie im Integrationstestcode verwenden:


 import "github.com/syhpoon/xenvman/pkg/client" import "github.com/syhpoon/xenvman/pkg/def" //  xenvman  cl := client.New(client.Params{}) //      env := cl.MustCreateEnv(&def.InputEnv{ Name: "wut-test", Description: "Testing Wut", // ,      Templates: []*def.Tpl{ { Tpl: "wut", Parameters: def.TplParams{ "binary": client.FileToBase64("wut"), "port": 5555, }, }, { Tpl: "mongo", Parameters: def.TplParams{"tag": “latest”}, }, }, }) //      defer env.Terminate() //     wut  wutCont, err := env.GetContainer("wut", 0, "wut") require.Nil(t, err) //      mongoCont, err := env.GetContainer("mongo", 0, "mongo") require.Nil(t, err) //    wutUrl := fmt.Sprintf("http://%s:%d/v1/wut/", env.ExternalAddress, wutCont.Ports[“5555”]) mongoUrl := fmt.Sprintf("%s:%d/wut", env.ExternalAddress, mongoCont.Ports["27017"]) // !      ,            ,   

Es scheint, dass das Schreiben von Vorlagen zu lange dauert.
Mit dem richtigen Design ist dies jedoch eine einmalige Aufgabe, und dann können dieselben Vorlagen immer mehr (und sogar für verschiedene Sprachen!) Wiederverwendet werden, indem sie einfach durch Übergabe bestimmter Parameter feinabgestimmt werden. Wie Sie im obigen Beispiel sehen können, ist der Testcode selbst sehr einfach, da wir alle Hülsen beim Einrichten der Umgebung in Vorlagen einfügen.


In diesem kleinen Beispiel finden Sie hier nicht nur alle Funktionen von xenvman, sondern auch eine ausführlichere Schritt-für-Schritt-Anleitung .


Kunden


Derzeit gibt es Kunden für zwei Sprachen:


Geh
Python


Das Hinzufügen neuer ist jedoch nicht schwierig, da die bereitgestellte API sehr, sehr einfach ist.


Webschnittstelle


In Version 2.0.0 wurde eine einfache Weboberfläche hinzugefügt, mit der Sie Ihre Umgebungen verwalten und verfügbare Vorlagen anzeigen können.





Wie unterscheidet sich Xenvman von Docker-Compose?


Natürlich gibt es viele Ähnlichkeiten, aber Xenvman scheint mir ein etwas flexiblerer und dynamischerer Ansatz zu sein als die statische Konfiguration in der Datei.
Hier sind meiner Meinung nach die Hauptunterscheidungsmerkmale:


  • Die gesamte Steuerung erfolgt über die HTTP-API. Daher können wir Umgebungen aus dem Code jeder Sprache erstellen, die HTTP versteht
  • Da xenvman auf einem anderen Host ausgeführt werden kann, können wir alle Funktionen auch von einem Host aus nutzen, auf dem Docker nicht installiert ist.
  • Die Fähigkeit, Bilder im laufenden Betrieb dynamisch zu erstellen
  • Die Möglichkeit, die Zusammensetzung der Umgebung (Hinzufügen / Stoppen von Containern) während des Betriebs zu ändern
  • Reduzierter Boilerplate-Code, verbesserte Zusammensetzung und die Möglichkeit, Konfigurationscode durch die Verwendung parametrierbarer Vorlagen wiederzuverwenden

Referenzen


Github-Projektseite
Detailliertes Schritt-für-Schritt-Beispiel in Englisch.


Fazit


Das ist alles In naher Zukunft plane ich, die Gelegenheit hinzuzufügen
Rufen Sie Vorlagen aus Vorlagen auf und ermöglichen Sie ihnen, diese effizienter zu kombinieren.


Ich werde versuchen, alle Fragen zu beantworten, und ich würde mich freuen, wenn jemand anderes dieses Projekt nützlich findet.

Source: https://habr.com/ru/post/de439236/


All Articles