Weil das Leben zu kurz ist, um Diagramme neu zu zeichnen
Ich habe vor Kurzem als Softwareentwickler in ein neues Unternehmen eingestiegen. Wie immer musste ich bei Null anfangen. Dinge wie: Wo ist der Code für eine Live-App? Wie wird sie bereitgestellt? Woher kommen die Konfigurationen? Zum Glück haben meine Kollegen fantastische Arbeit geleistet und alles zu „Infrastruktur als Code“ gemacht. Also ertappte ich mich bei dem Gedanken: Wenn alles im Code steckt, warum gibt es dann kein Tool, um alle Punkte zu verbinden?
Dieses Tool würde die Codebasis überprüfen und ein Anwendungsarchitekturdiagramm erstellen, in dem die wichtigsten Aspekte hervorgehoben werden. Ein neuer Ingenieur könnte sich das Diagramm ansehen und sagen: „Ah, okay, so funktioniert es.“
So sehr ich auch suchte, ich konnte nichts dergleichen finden. Die besten Übereinstimmungen, die ich fand, waren Dienste, die ein Infrastrukturdiagramm zeichnen. Ich habe einige davon in diese Rezension aufgenommen, damit Sie sie sich genauer ansehen können. Irgendwann gab ich das Googeln auf und beschloss, mich an der Entwicklung neuer cooler Sachen zu versuchen.
Zuerst habe ich eine Beispiel -Java- App mit Gradle, Docker und Terraform erstellt. Die GitHub-Actions-Pipeline stellt die App auf Amazon Elastic Container Service bereit. Dieses Repository wird eine Quelle für das Tool sein, das ich erstellen werde (der Code ist hier ).
Zweitens habe ich ein sehr detailliertes Diagramm dessen gezeichnet, was ich als Ergebnis sehen wollte:
Ich habe entschieden, dass es zwei Arten von Ressourcen geben soll:
Ich fand den Begriff Artefakt zu überladen, also habe ich Relikt gewählt. Was ist also ein Relikt? Es sind 90 % von allem, was Sie sehen möchten. Einschließlich, aber nicht beschränkt auf:
Jedes Relic hat einen Namen (z. B. my-shiny-app), einen optionalen Typ (z. B. Jar) und eine Reihe von Schlüssel-Wert-Paaren (z. B. Pfad → /build/libs/my-shiny-app.jar), die Relic vollständig beschreiben. Sie werden Definitionen genannt. Je mehr Definitionen Relic hat, desto besser.
Der zweite Typ ist eine Quelle . Quellen definieren, erstellen oder stellen Relikte bereit (z. B. die gelben Kästchen oben). Eine Quelle beschreibt ein Relikt an einer bestimmten Stelle und gibt einen Eindruck davon, woher es kommt. Obwohl Quellen die Komponenten sind, aus denen wir die meisten Informationen erhalten, haben sie im Diagramm normalerweise eine sekundäre Bedeutung. Sie benötigen wahrscheinlich nicht viele Pfeile, die von Terraform oder Gradle zu jedem anderen Relikt führen.
Relikt und Quelle haben eine Viele-zu-viele-Beziehung.
Es ist unmöglich, jeden Codeabschnitt abzudecken. Moderne Apps können viele Frameworks, Tools oder Cloud-Komponenten haben. Allein AWS verfügt über rund 950 Ressourcen und Datenquellen für Terraform! Das Tool muss leicht erweiterbar und von Natur aus entkoppelt sein, damit andere Personen oder Unternehmen dazu beitragen können.
Obwohl ich ein großer Fan der unglaublich steckbaren Architektur von Terraform-Anbietern bin, habe ich mich entschieden, dasselbe zu bauen, wenn auch vereinfacht:
Der Provider hat eine klare Verantwortung: das Erstellen von Relics auf der Grundlage der angeforderten Quelldateien. Beispielsweise liest GradleProvider *.gradle-Dateien und gibt Jar- , War- oder Gz- Relics zurück. Jeder Provider erstellt Relics der Typen, die ihm bekannt sind. Provider kümmern sich nicht um Interaktionen zwischen Relics. Sie erstellen Relics deklarativ und völlig isoliert voneinander.
Mit diesem Ansatz können Sie ganz einfach so tief gehen, wie Sie möchten. Ein gutes Beispiel sind GitHub Actions. Eine typische Workflow-YAML-Datei besteht aus Dutzenden von Schritten unter Verwendung lose gekoppelter Komponenten und Dienste. Ein Workflow könnte eine JAR-Datei erstellen, dann ein Docker-Image und es in der Umgebung bereitstellen. Jeder einzelne Schritt im Workflow könnte von seinem Provider abgedeckt werden. Entwickler von beispielsweise Docker Actions erstellen also einen Provider, der sich nur auf die Schritte bezieht, die für sie von Bedeutung sind.
Dieser Ansatz ermöglicht es einer beliebigen Anzahl von Personen, parallel zu arbeiten, wodurch dem Tool mehr Logik hinzugefügt wird. Endbenutzer können ihre Anbieter auch schnell implementieren (bei proprietärer Technologie). Weitere Informationen finden Sie weiter unten unter „Anpassung“.
Schauen wir uns die nächste Falle an, bevor wir uns dem spannendsten Teil widmen. Zwei Provider, von denen jeder ein Relic erstellt. Das ist in Ordnung. Aber was, wenn zwei dieser Relics nur Darstellungen derselben Komponente sind, die an zwei Stellen definiert ist? Hier ist ein Beispiel.
AmazonECSProvider analysiert die JSON-Aufgabendefinition und erstellt ein Relic vom Typ AmazonECSTask . Der GitHub-Aktionsworkflow hat auch einen ECS-bezogenen Schritt, sodass ein anderer Anbieter ein AmazonECSTaskDeployment- Relic erstellt. Jetzt haben wir Duplikate, weil beide Anbieter nichts voneinander wissen. Darüber hinaus ist es falsch, wenn einer von ihnen annimmt, dass ein anderer bereits ein Relic erstellt hat. Was dann?
Wir können keines der Duplikate löschen, da jedes von ihnen Definitionen (Attribute) hat. Die einzige Möglichkeit besteht darin, sie zusammenzuführen. Standardmäßig definiert die folgende Logik die Zusammenführungsentscheidung:
relic1.name() == relic2.name() && relic1.source() != relic2.source()
Wir führen zwei Relics zusammen, wenn ihre Namen gleich sind, sie aber in unterschiedlichen Quellen definiert sind (wie in unserem Beispiel befindet sich JSON im Repo und die Referenz zur Aufgabendefinition in GithHub Actions).
Wenn wir fusionieren:
Ich habe absichtlich einen entscheidenden Aspekt eines Relikts weggelassen. Es kann einen Matcher haben – und es ist besser, einen zu haben! Der Matcher ist eine boolesche Funktion, die ein Argument entgegennimmt und testet. Matcher sind entscheidende Teile eines Verknüpfungsprozesses. Wenn ein Relikt einer Definition eines anderen Relikts entspricht, werden sie miteinander verknüpft.
Erinnern Sie sich, als ich sagte, dass Anbieter keine Ahnung von Relikten haben, die von anderen Anbietern erstellt wurden? Das stimmt immer noch. Ein Anbieter definiert jedoch einen Matcher für ein Relikt. Mit anderen Worten, es stellt eine Seite eines Pfeils zwischen zwei Kästchen im resultierenden Diagramm dar.
Beispiel. Dockerfile hat eine ENTRYPOINT-Anweisung.
ENTRYPOINT java -jar /app/arch-diagram-sample.jar
Mit einiger Sicherheit können wir sagen, dass Docker alles containerisiert , was unter ENTRYPOINT angegeben ist. Das Dockerfile- Relic hat also eine einfache Matcher-Funktion: entrypointInstruction.contains(anotherRelicsDefinition)
. Höchstwahrscheinlich werden einige Jar- Relics mit arch-diagram-sample.jar
in den Definitionen damit übereinstimmen. Wenn ja, erscheint ein Pfeil zwischen Dockerfile und Jar- Relics.
Wenn Matcher definiert ist, sieht der Verknüpfungsprozess ziemlich unkompliziert aus. Der Verknüpfungsdienst durchläuft alle Relikte und ruft die Funktionen ihrer Matcher auf. Entspricht Relikt A einer der B-Definitionen von Relikt? Ja? Fügen Sie im resultierenden Diagramm eine Kante zwischen diesen Relikten hinzu. Die Kante könnte auch benannt werden.
Der letzte Schritt besteht darin, unseren endgültigen Graphen der vorherigen Phase zu visualisieren. Neben dem offensichtlichen PNG unterstützt das Tool weitere Formate wie Mermaid , Plant UML und DOT . Diese Textformate sehen vielleicht weniger attraktiv aus, aber der große Vorteil besteht darin, dass Sie diese Texte in fast jede Wiki-Seite einbetten können (
So sieht das endgültige Diagramm des Beispiel-Repos aus:
Die Möglichkeit, benutzerdefinierte Komponenten einzubinden oder vorhandene Logik zu optimieren, ist unerlässlich, insbesondere wenn sich ein Tool in der Anfangsphase befindet. Relics und Sources sind standardmäßig flexibel genug; Sie können alles hineinpacken, was Sie möchten. Alle anderen Komponenten sind anpassbar. Vorhandene Provider decken die von Ihnen benötigten Ressourcen nicht ab? Implementieren Sie ganz einfach Ihre eigenen. Sie sind mit der oben beschriebenen Zusammenführungs- oder Verknüpfungslogik nicht zufrieden? Kein Problem; fügen Sie Ihre eigene LinkStrategy oder MergeStrategy hinzu. Packen Sie alles in eine JAR-Datei und fügen Sie sie beim Start hinzu. Lesen Sie hier mehr.
Das Generieren eines Diagramms auf der Grundlage von Quellcode wird wahrscheinlich an Bedeutung gewinnen. Und insbesondere das NoReDraw- Tool (ja, das ist der Name des Tools, von dem ich gesprochen habe). Mitwirkende sind willkommen !
Der bemerkenswerteste Vorteil (der sich aus dem Namen ergibt) besteht darin, dass ein Diagramm nicht neu gezeichnet werden muss, wenn sich Komponenten ändern. Der Mangel an technischer Aufmerksamkeit ist der Grund, warum Dokumentation im Allgemeinen (und Diagramme im Besonderen) veraltet. Mit Tools wie NoReDraw sollte dies kein Problem mehr sein, da es sich problemlos in jede PR/CI-Pipeline integrieren lässt. Denken Sie daran, das Leben ist zu kurz, um Diagramme neu zu zeichnen 😉