BLOG | NGINX

Best Practices für die Konfiguration von Microservices-Apps

NGINX-Teil-von-F5-horiz-schwarz-Typ-RGB
Javier Evans Miniaturbild
Javier Evans
Veröffentlicht am 02. März 2023

Der Leitfaden, bekannt als Zwölf-Faktoren-App, wurde erstmals vor mehr als zehn Jahren veröffentlicht. Seitdem sind fast alle vorgeschriebenen Vorgehensweisen zum De-facto-Standard für das Schreiben und Bereitstellen von Web-Apps geworden. Und obwohl sie trotz der Änderungen in der Art und Weise, wie Apps organisiert und bereitgestellt werden, weiterhin anwendbar sind, sind in manchen Fällen zusätzliche Details erforderlich, um zu verstehen, wie die Vorgehensweisen auf Microservices- Muster für die Entwicklung und Bereitstellung von Apps angewendet werden.

In diesem Blog geht es um Faktor 3, „Konfiguration in der Umgebung speichern“ , der besagt:

  • Unter Konfiguration versteht man alles, was zwischen Bereitstellungsumgebungen (die in der Zwölf-Faktor-App als „Bereitstellungen“ bezeichnet werden) variiert.
  • Die Konfiguration muss strikt vom Code der App getrennt sein – wie könnte sie sonst bei verschiedenen Bereitstellungen unterschiedlich sein?
  • Konfigurationsdaten werden in Umgebungsvariablen gespeichert.

Wenn Sie zu Microservices wechseln, können Sie diese Richtlinien weiterhin einhalten, aber nicht immer auf eine Weise, die genau einer wörtlichen Interpretation der Zwölf-Faktoren-App entspricht. Einige Richtlinien, wie die Bereitstellung von Konfigurationsdaten als Umgebungsvariablen, lassen sich problemlos übernehmen. Andere gängige Microservices-Praktiken respektieren zwar die Kernprinzipien der Zwölf-Faktoren-App, sind aber eher Erweiterungen davon. In diesem Beitrag betrachten wir drei Kernkonzepte des Konfigurationsmanagements für Microservices aus der Perspektive von Faktor 3:

Wichtige Microservices-Terminologie und -Konzepte

Bevor wir uns in die Diskussion zur Anpassung von Faktor 3 für Microservices stürzen, ist es hilfreich, einige wichtige Begriffe und Konzepte zu verstehen.

  • Monolithische App-Architektur – Ein traditionelles Architekturmodell, das App-Funktionen in Komponentenmodule aufteilt, aber alle Module in einer einzigen Codebasis enthält.
  • Microservices-App-Architektur – Ein Architekturmodell, das eine große, komplexe App aus mehreren kleinen Komponenten erstellt, die jeweils einen genau abgegrenzten Satz von Operationen ausführen (wie etwa Authentifizierung, Benachrichtigung oder Zahlungsabwicklung). „Microservice“ ist auch die Bezeichnung für die kleinen Komponenten selbst. In der Praxis können einige „Mikroservices“ tatsächlich ziemlich groß sein.
  • Service – Ein allgemeiner Begriff für eine einzelne Anwendung oder einen Mikroservice in einem System.
  • System – Im Kontext dieses Blogs der vollständige Satz an Mikroservices und unterstützender Infrastruktur, die zusammen die vollständige Funktionalität erstellen, die von der Organisation bereitgestellt wird.
  • Artefakt – Ein von einer Test- und Build-Pipeline erstelltes Objekt. Es kann viele Formen annehmen, beispielsweise ein Docker-Image, das den Code einer App enthält.
  • Bereitstellung – Eine laufende „Instanz“ eines Artefakts, die in einer Umgebung wie Staging, Integration oder Produktion ausgeführt wird.

Microservices versus Monolithen

Bei einer monolithischen Anwendung arbeiten alle Teams in der Organisation an derselben Anwendung und der umgebenden Infrastruktur. Obwohl monolithische Apps auf dem Papier im Allgemeinen einfacher erscheinen als Microservices, gibt es mehrere allgemeine Gründe, warum sich Unternehmen für die Umstellung auf Microservices entscheiden:

  • Teamautonomie – Es kann schwierig sein, die Eigentümerschaft an Funktionen und Subsystemen in einem Monolithen zu definieren. Wenn Organisationen wachsen und reifen, wird die Verantwortung für die App-Funktionalität häufig auf immer mehr Teams verteilt. Dadurch entstehen Abhängigkeiten zwischen den Teams, da das Team, dem ein Teil der Funktionalität gehört, nicht auch Eigentümer aller zugehörigen Subsysteme im Monolithen ist.
  • Verringerung des „Explosionsradius“ – Wenn eine große Anwendung als einzelne Einheit entwickelt und bereitgestellt wird, kann ein Fehler in einem Subsystem die Funktionalität der gesamten App beeinträchtigen.
  • Unabhängige Skalierung der Funktionalität – Selbst wenn nur ein einzelnes Modul in einer monolithischen App stark ausgelastet ist, muss das Unternehmen viele Instanzen der gesamten App bereitstellen, um Systemausfälle oder -verschlechterungen zu vermeiden.

Natürlich bringen Microservices auch ihre eigenen Herausforderungen mit sich – beispielsweise eine höhere Komplexität, geringere Beobachtbarkeit und die Notwendigkeit neuer Sicherheitsmodelle. Viele Unternehmen, insbesondere große oder schnell wachsende, entscheiden jedoch, dass sich diese Herausforderungen lohnen, um ihren Teams mehr Autonomie und Flexibilität bei der Schaffung zuverlässiger, stabiler Grundlagen für die Erfahrungen zu geben, die sie ihren Kunden bieten.

Erforderliche Änderungen für Microservices-Architekturen

Wenn Sie eine monolithische App in Microservices umgestalten, müssen Ihre Dienste:

  • Akzeptieren Sie Konfigurationsänderungen auf vorhersehbare Weise
  • Macht sich im weiteren System auf vorhersehbare Weise bekannt
  • Seien Sie gut dokumentiert

Für eine monolithische App sind kleine Inkonsistenzen in Prozessen und die Abhängigkeit von gemeinsamen Annahmen nicht kritisch. Bei vielen separaten Microservices können diese Inkonsistenzen und Annahmen jedoch viel Ärger und Chaos verursachen. Viele der Änderungen, die Sie mit Microservices vornehmen müssen, sind technischer Notwendigkeiten, aber überraschend viele betreffen die interne Arbeitsweise und die Interaktion der Teams mit anderen Teams.

Zu den wichtigen organisatorischen Änderungen bei einer Microservices-Architektur zählen:

  • Anstatt gemeinsam an derselben Codebasis zu arbeiten, werden die Teams völlig getrennt, wobei jedes Team die Gesamtverantwortung für einen oder mehrere Dienste trägt. Bei der gängigsten Implementierung von Microservices werden Teams zudem so umorganisiert, dass sie „funktionsübergreifend“ sind. Das bedeutet, dass ihre Mitglieder über alle erforderlichen Kompetenzen verfügen, um die Teamziele zu erreichen und dabei möglichst wenig von anderen Teams abhängig sind.
  • Plattformteams (verantwortlich für die allgemeine Integrität des Systems) müssen jetzt mehrere Dienste koordinieren, die verschiedenen Teams gehören, anstatt sich mit einer einzigen Anwendung zu befassen.
  • Die Tooling-Teams müssen weiterhin in der Lage sein, den verschiedenen Service-Owner-Teams Werkzeuge und Anleitungen bereitzustellen, um ihnen dabei zu helfen, ihre Ziele schnell zu erreichen und gleichzeitig die Systemstabilität aufrechtzuerhalten.

Diagramm zum Vergleich der Organisation von Entwicklerteams für monolithische und Microservices-Apps

Klare Definition Ihrer Servicekonfiguration

Ein Bereich der Microservices-Architektur, in dem wir Faktor 3 erweitern müssen, betrifft die Notwendigkeit, bestimmte wichtige Informationen zu einem Dienst, einschließlich seiner Konfiguration, klar zu definieren und ein Mindestmaß an gemeinsamem Kontext mit anderen Diensten anzunehmen. Faktor 3 geht zwar nicht direkt darauf ein, ist aber besonders wichtig, da eine große Anzahl separater Mikroservices zur Anwendungsfunktionalität beiträgt.

Als Serviceeigentümer in einer Microservices-Architektur besitzt Ihr Team Services, die im Gesamtsystem bestimmte Rollen spielen. Andere Teams, deren Dienste mit Ihren interagieren, müssen auf das Repository Ihres Dienstes zugreifen, um Code und Dokumentation zu lesen und Beiträge zu leisten.

Darüber hinaus ist es im Bereich der Softwareentwicklung eine bedauerliche Realität, dass sich die Zusammensetzung von Teams häufig ändert, und zwar nicht nur, weil Entwickler in das Unternehmen eintreten oder es verlassen, sondern auch aufgrund interner Umstrukturierungen. Außerdem wird die Verantwortung für einen bestimmten Dienst häufig zwischen Teams übertragen.

Angesichts dieser Realitäten müssen Ihre Codebasis und Dokumentation äußerst klar und konsistent sein. Dies wird durch Folgendes erreicht:

  • Den Zweck jeder Konfigurationsoption klar definieren
  • Klare Definition des erwarteten Formats des Konfigurations-Werts
  • Klare Definition, wie die Anwendung die Bereitstellung von Konfigurationswerten erwartet
  • Aufzeichnung dieser Informationen in einer begrenzten Anzahl von Dateien

Viele Anwendungsframeworks bieten eine Möglichkeit zum Definieren der erforderlichen Konfiguration. Beispielsweise verwendet das Convict- NPM-Paket für Node.js-Anwendungen ein vollständiges Konfigurationsschema, das in einer einzigen Datei gespeichert ist. Es fungiert als zuverlässige Quelle für die gesamte Konfiguration, die zum Ausführen einer Node.js-App erforderlich ist.

Ein robustes und leicht auffindbares Schema erleichtert sowohl Ihren Teammitgliedern als auch anderen die sichere Interaktion mit Ihrem Dienst.

So wird einem Dienst die Konfiguration bereitgestellt

Nachdem Sie klar definiert haben, welche Konfigurationswerte Ihre Anwendung benötigt, müssen Sie auch die wichtige Unterscheidung zwischen den beiden primären Quellen beachten, aus denen eine bereitgestellte Microservices-Anwendung ihre Konfiguration bezieht:

  • Bereitstellungsskripte, die Konfigurationseinstellungen explizit definieren und den Anwendungsquellcode begleiten
  • Externe Quellen, die zum Zeitpunkt der Bereitstellung abgefragt wurden

Bereitstellungsskripte sind ein gängiges Code-Organisationsmuster in Microservices-Architekturen. Da sie seit der ursprünglichen Veröffentlichung der Zwölf-Faktoren-App neu sind, stellen sie notwendigerweise eine Erweiterung derselben dar.

Muster: Bereitstellung und Infrastrukturkonfiguration neben der Anwendung

In den letzten Jahren ist es üblich geworden, einen Ordner mit dem Namen „Infrastruktur“ (oder einer Variante dieses Namens) im selben Repository wie Ihren Anwendungscode zu haben. Es enthält normalerweise:

  • Infrastruktur als Code ( Terraform ist ein gängiges Beispiel), der die Infrastruktur beschreibt, von der der Dienst abhängt, z. B. eine Datenbank
  • Konfiguration für Ihr Container-Orchestrierungssystem, z. B. Helm-Charts und Kubernetes-Manifeste
  • Alle anderen Dateien im Zusammenhang mit der Bereitstellung der Anwendung

Auf den ersten Blick könnte dies wie ein Verstoß gegen die Vorgabe von Faktor 3 aussehen, dass Konfiguration strikt vom Code getrennt ist.

Tatsächlich bedeutet die Platzierung neben Ihrer Anwendung, dass ein Infrastrukturordner die Regel tatsächlich einhält und gleichzeitig wertvolle Prozessverbesserungen ermöglicht, die für Teams, die in Microservices-Umgebungen arbeiten, von entscheidender Bedeutung sind.

Zu den Vorteilen dieses Musters gehören:

  • Das Team, dem der Dienst gehört, ist auch für die Bereitstellung des Dienstes und der dienstspezifischen Infrastruktur (z. B. Datenbanken) verantwortlich.
  • Das zuständige Team kann sicherstellen, dass Änderungen an diesen Elementen den Entwicklungsprozess (Codeüberprüfung, CI) durchlaufen.
  • Das Team kann die Bereitstellung seiner Dienste und der unterstützenden Infrastruktur problemlos ändern, ohne von der Arbeit externer Teams abhängig zu sein.

Beachten Sie, dass die Vorteile dieses Musters die Autonomie einzelner Teams stärken und gleichzeitig sicherstellen, dass der Bereitstellungs- und Konfigurationsprozess mit größerer Genauigkeit durchgeführt wird.

Welcher Konfigurationstyp gehört wohin?

In der Praxis verwenden Sie die in Ihrem Infrastrukturordner gespeicherten Bereitstellungsskripts, um sowohl die in den Skripts selbst explizit definierte Konfiguration als auch den Abruf der Konfiguration aus externen Quellen zum Bereitstellungszeitpunkt zu verwalten, indem Sie das Bereitstellungsskript für einen Dienst haben:

  1. Bestimmte Konfigurationswerte direkt definieren
  2. Definieren Sie, wo der Prozess, der das Bereitstellungsskript ausführt, in externen Quellen nach den gewünschten Konfigurationswerten suchen kann

Konfigurationseinstellungen, die für eine bestimmte Bereitstellung Ihres Dienstes spezifisch sind und vollständig unter der Kontrolle Ihres Teams stehen, können direkt in den Dateien im Infrastrukturordner angegeben werden. Ein Beispiel hierfür wäre etwa eine Begrenzung der Dauer, die eine von der App initiierte Datenbankabfrage ausgeführt werden darf. Dieser Wert kann geändert werden, indem die Bereitstellungsdatei geändert und die Anwendung erneut bereitgestellt wird.

Ein Vorteil dieses Schemas besteht darin, dass Änderungen an einer solchen Konfiguration zwangsläufig einer Codeüberprüfung und automatisierten Tests unterzogen werden, wodurch die Wahrscheinlichkeit verringert wird, dass ein falsch konfigurierter Wert einen Ausfall verursacht. Änderungen an Werten, die einer Codeüberprüfung unterzogen werden, sowie an den Werten von Konfigurationsschlüsseln sind jederzeit im Verlauf Ihrer Quellcodeverwaltungstools erkennbar.

Werte, die für die Ausführung der Anwendung erforderlich sind, auf die Ihr Team jedoch keinen Einfluss hat, müssen von der Umgebung bereitgestellt werden, in der die Anwendung bereitgestellt wird. Ein Beispiel ist der Hostname und der Port, über den der Dienst eine Verbindung zu einem anderen Mikrodienst herstellt, von dem er abhängig ist.

Da dieser Dienst nicht Eigentum Ihres Teams ist, können Sie keine Annahmen über Werte wie die Portnummer treffen. Solche Werte können sich jederzeit ändern und müssen bei einer Änderung in einem zentralen Konfigurationsspeicher registriert werden – unabhängig davon, ob die Änderung manuell oder durch einen automatischen Prozess erfolgt. Sie können dann von Anwendungen abgefragt werden, die von ihnen abhängig sind.

Wir können diese Richtlinien in zwei Best Practices für die Konfiguration von Microservices zusammenfassen.

Eine Microservices-Konfiguration sollte Folgendes nicht tun: Verlassen Sie sich auf fest kodierte oder gemeinsam vereinbarte Werte

Am einfachsten erscheint es möglicherweise, bestimmte Werte in Ihren Bereitstellungsskripten fest zu codieren, beispielsweise den Standort eines Dienstes, mit dem Ihr Dienst interagiert. In der Praxis ist die Festcodierung dieser Art von Konfiguration gefährlich, insbesondere in modernen Umgebungen, in denen sich die Service-Standorte häufig ändern. Und es ist besonders gefährlich, wenn Sie nicht Eigentümer des zweiten Dienstes sind.

Sie gehen vielleicht davon aus, dass Sie sich bei der Aktualisierung eines Service-Standorts in Ihren Skripten auf Ihre eigene Sorgfalt verlassen können, oder, schlimmer noch, dass Sie sich darauf verlassen können, dass das Eigentümerteam Sie bei Standortänderungen informiert. In Stresssituationen lässt die Sorgfalt oft nach, und wenn Sie sich auf die Genauigkeit menschlicher Methoden verlassen, besteht die Gefahr, dass Ihr System ohne Vorwarnung ausfällt.

Eine Microservices-Konfiguration erfordert: Lassen Sie den Dienst fragen: „Wo ist meine Datenbank?“

Unabhängig davon, ob Standortinformationen fest codiert sind oder nicht, darf Ihre Anwendung nicht davon abhängen, dass sich kritische Infrastruktur an einem bestimmten Standort befindet. Stattdessen muss ein neu bereitgestellter Dienst einer allgemeinen Quelle im System Fragen stellen, beispielsweise „Wo ist meine Datenbank?“, und eine genaue Antwort zum aktuellen Standort dieser externen Ressource erhalten. Die Dinge sind wesentlich einfacher, wenn sich jeder Dienst bei der Bereitstellung selbst beim System registriert.

Einen Dienst als Konfiguration verfügbar machen

Genauso wie das System Antworten auf die Fragen „Wo ist meine Datenbank?“ und „Wo ist ‚Dienst X‘, von dem ich abhängig bin?“ bereitstellen muss, muss ein Dienst dem System so zugänglich gemacht werden, dass andere Dienste ihn leicht finden und mit ihm kommunizieren können, ohne etwas über seine Bereitstellung zu wissen.

Eine wichtige Konfigurationspraxis in Microservices-Architekturen ist die Diensterkennung: die Registrierung neuer Dienstinformationen und die dynamische Aktualisierung dieser Informationen beim Zugriff anderer Dienste. Nachdem wir erklärt haben, warum Service Discovery für Microservices notwendig ist, sehen wir uns nun ein Beispiel an, wie dies mit NGINX Open Source und Consul erreicht werden kann.

Es ist üblich, mehrere Instanzen (Bereitstellungen) eines Dienstes gleichzeitig auszuführen. Dies ermöglicht nicht nur die Bewältigung zusätzlichen Datenverkehrs, sondern auch die Aktualisierung eines Dienstes ohne Ausfallzeiten durch den Start einer neuen Bereitstellung. Tools wie NGINX fungieren als Reverse-Proxy und Load Balancer, verarbeiten eingehenden Datenverkehr und leiten ihn an die am besten geeignete Instanz weiter. Dies ist ein schönes Muster, da Dienste, die von Ihrem Dienst abhängen, Anfragen nur an NGINX senden und nichts über Ihre Bereitstellungen wissen müssen.

Angenommen, Sie haben eine einzelne Instanz eines Dienstes namens Messenger , der hinter NGINX ausgeführt wird und als Reverse-Proxy fungiert.

Diagramm einer einzelnen Instanz des Mikrodienstes „Messenger“, der von NGINX per Reverse-Proxy weitergeleitet wird

Was nun, wenn Ihre App populär wird? Das ist eine gute Nachricht, doch dann fällt auf, dass die Messenger- Instanz aufgrund des erhöhten Datenverkehrs viel CPU-Leistung verbraucht und länger braucht, um Anfragen zu verarbeiten, während die Datenbank scheinbar einwandfrei funktioniert. Dies deutet darauf hin, dass Sie das Problem möglicherweise lösen können, indem Sie eine weitere Instanz des Nachrichtendienstes bereitstellen.

Wenn Sie die zweite Instanz des Messenger- Dienstes bereitstellen, woher weiß NGINX dann, dass diese live ist und kann beginnen, Datenverkehr dorthin zu senden? Das manuelle Hinzufügen neuer Instanzen zu Ihrer NGINX-Konfiguration ist eine Möglichkeit, wird jedoch schnell unhandlich, wenn mehr Dienste hoch- oder herunterskaliert werden.

Eine gängige Lösung besteht darin, die Dienste in einem System mit einem hochverfügbaren Dienstregister wie Consul zu verfolgen. Neue Serviceinstanzen werden bei ihrer Bereitstellung bei Consul registriert. Consul überwacht den Status der Instanzen, indem es ihnen regelmäßig Integritätschecks sendet. Wenn eine Instanz die Integritätsprüfung nicht besteht, wird sie aus der Liste der verfügbaren Dienste entfernt.

Diagramm von zwei Instanzen des Mikrodienstes „Messenger“, die von NGINX per Reverse-Proxy weitergeleitet werden, mit Consul zur Diensterkennung

NGINX kann ein Register wie Consul mithilfe verschiedener Methoden abfragen und sein Routing entsprechend anpassen. Denken Sie daran, dass NGINX beim Einsatz als Reverse-Proxy oder Load Balancer den Datenverkehr an „Upstream“-Server weiterleitet. Betrachten Sie diese einfache Konfiguration:


# Definieren Sie eine Upstream-Gruppe namens „messenger_service“
upstream messenger_service {
server 172.18.0.7:4000;
server 172.18.0.8:4000;
}

server {
listen 80;

location /api {
# Leiten Sie HTTP-Verkehr mit Pfaden, die mit „/api“ beginnen, zum
# „Upstream“-Block oben weiter. Der standardmäßige Lastausgleichsalgorithmus, 
# Round-Robin, wechselt Anfragen zwischen den beiden Servern 
# im Block.
proxy_pass http://messenger_service;
proxy_set_header X-Forwarded-For $remote_addr;
}
}


Standardmäßig muss NGINX die genaue IP-Adresse und den Port jeder Messenger -Instanz kennen, um den Datenverkehr dorthin weiterzuleiten. In diesem Fall ist das Port 4000 sowohl auf 172.18.0.7 als auch auf 172.18.0.8.

Hier kommen Consul und die Consul-Vorlage ins Spiel. Die Consul-Vorlage wird im selben Container wie NGINX ausgeführt und kommuniziert mit dem Consul-Client, der das Dienstregister verwaltet.

Wenn sich Registrierungsinformationen ändern, generiert die Consul-Vorlage eine neue Version der NGINX-Konfigurationsdatei mit den richtigen IP-Adressen und Ports, schreibt sie in das NGINX-Konfigurationsverzeichnis und weist NGINX an, seine Konfiguration neu zu laden. Es gibt keine Ausfallzeit, wenn NGINX seine Konfiguration neu lädt , und die neue Instanz beginnt mit dem Empfang von Datenverkehr, sobald der Neuladevorgang abgeschlossen ist.

Mit einem Reverse-Proxy wie NGINX gibt es in einer solchen Situation einen einzigen Kontaktpunkt, um sich beim System als Zugriffspunkt für andere Dienste zu registrieren. Ihr Team hat die Flexibilität, einzelne Serviceinstanzen zu verwalten, ohne befürchten zu müssen, dass andere Services den Zugriff auf den Service als Ganzes verlieren.

Machen Sie sich im März mit NGINX und Microservices vertraut

Microservices erhöhen zugegebenermaßen die Komplexität, sowohl in technischer Hinsicht für Ihre Dienste als auch in organisatorischer Hinsicht für Ihre Beziehungen zu anderen Teams. Um die Vorteile einer Microservices-Architektur nutzen zu können, ist es wichtig, die für Monolithen entwickelten Praktiken kritisch zu prüfen, um sicherzustellen, dass sie auch in einer völlig anderen Umgebung noch immer dieselben Vorteile bieten. In diesem Blog haben wir untersucht, wie Faktor 3 der Zwölf-Faktoren-App im Kontext von Mikroservices weiterhin einen Mehrwert bietet, aber von kleinen Änderungen in seiner konkreten Anwendung profitieren kann.

Weitere Informationen zur Anwendung der Zwölf-Faktoren-App auf Microservices-Architekturen finden Sie in Einheit 1 von Microservices vom März 2023 ( demnächst im Blog ). Registrieren Sie sich kostenlos, um Zugang zu einem Webinar zu diesem Thema und einem praktischen Labor zu erhalten.


„Dieser Blogbeitrag kann auf Produkte verweisen, die nicht mehr verfügbar und/oder nicht mehr unterstützt werden. Die aktuellsten Informationen zu verfügbaren F5 NGINX-Produkten und -Lösungen finden Sie in unserer NGINX-Produktfamilie . NGINX ist jetzt Teil von F5. Alle vorherigen NGINX.com-Links werden auf ähnliche NGINX-Inhalte auf F5.com umgeleitet."