Parce que la vie est trop courte pour redessiner des schémas
J'ai récemment rejoint une nouvelle entreprise en tant qu'ingénieur logiciel . Comme cela arrive toujours, j’ai dû repartir de zéro. Des choses comme : où se trouve le code d’une application ? Comment se déploie-t-il ? D'où viennent les configurations ? Heureusement, mes collègues ont fait un travail fantastique en créant une « infrastructure en tant que code ». Alors je me suis surpris à penser : si tout est dans le code, pourquoi n’y a-t-il pas un outil pour relier tous les points ?
Cet outil examinerait la base de code et créerait un diagramme d'architecture d'application, mettant en évidence les aspects clés. Un nouvel ingénieur pourrait regarder le schéma et dire : « Ah, d'accord, c'est comme ça que ça marche. »
J'ai beau chercher, je n'ai rien trouvé de pareil. Les correspondances les plus proches que j'ai trouvées étaient les services qui dessinent un diagramme d'infrastructure. J'en ai mis quelques-uns dans cette revue afin que vous puissiez y regarder de plus près. Finalement, j’ai abandonné la recherche sur Google et j’ai décidé de m’essayer au développement de nouveaux trucs sympas.
Tout d'abord, j'ai créé un exemple d'application Java avec Gradle, Docker et Terraform. Le pipeline d'actions GitHub déploie l'application sur Amazon Elastic Container Service. Ce dépôt sera une source pour l'outil que je vais construire (le code est ici ).
Deuxièmement, j'ai dessiné un diagramme de très haut niveau de ce que je voulais voir comme résultat :
J'ai décidé qu'il y aurait deux types de ressources :
J'ai trouvé le terme artefact trop surchargé, j'ai donc choisi Relic . Alors, qu’est-ce qu’une relique ? C'est 90 % de tout ce que vous voulez voir. Y compris, mais sans s'y limiter:
Chaque relique a un nom (par exemple, my-shiny-app), un type facultatif (par exemple, Jar) et un ensemble de paires clé → valeur (par exemple, chemin → /build/libs/my-shiny-app.jar) qui décrit entièrement Relic. Elles sont appelées définitions . Plus Relic a de définitions, mieux c'est.
Le deuxième type est un Source . Les sources définissent, construisent ou fournissent des reliques (par exemple, les cases jaunes ci-dessus). Une source décrit une relique à un endroit donné et donne une idée de son origine. Bien que les sources soient les composants à partir desquels nous obtenons le plus d’informations, elles ont généralement des significations secondaires sur le diagramme. Vous n'avez probablement pas besoin de beaucoup de flèches pour passer de Terraform ou Gradle à toutes les autres reliques.
Relic et Source ont une relation plusieurs-à-plusieurs.
Couvrir chaque morceau de code est impossible. Les applications modernes peuvent comporter de nombreux frameworks, outils ou composants cloud. AWS dispose à lui seul d'environ 950 ressources et sources de données pour Terraform ! L’outil doit être facilement extensible et découplé dès sa conception afin que d’autres personnes ou entreprises puissent y contribuer.
Bien que je sois un grand fan de l'architecture incroyablement connectable des fournisseurs Terraform, j'ai décidé de construire la même chose, bien que simplifiée :
Le fournisseur a une responsabilité claire : créer des reliques basées sur les fichiers sources demandés. Par exemple, GradleProvider lit les fichiers *.gradle et renvoie Jar , War ou Gz Relics. Chaque fournisseur crée des reliques des types dont il a connaissance. Les fournisseurs ne se soucient pas des interactions entre les reliques. Ils construisent des reliques de manière déclarative, totalement isolées les unes des autres.
Avec cette approche, il est facile d’aller aussi loin que vous le souhaitez. Un bon exemple est GitHub Actions. Un fichier YAML de flux de travail typique se compose de dizaines d’étapes utilisant des composants et des services faiblement couplés. Un workflow peut créer un JAR, puis une image Docker, et le déployer dans l'environnement. Chaque étape du flux de travail pourrait être couverte par son fournisseur. Ainsi, les développeurs, disons, de Docker Actions créent un fournisseur lié uniquement aux étapes qui les intéressent.
Cette approche permet à un nombre illimité de personnes de travailler en parallèle, ajoutant ainsi plus de logique à l'outil. Les utilisateurs finaux peuvent également mettre en œuvre rapidement leurs fournisseurs (dans le cas de certaines technologies propriétaires). Voir plus sous Personnalisation ci-dessous.
Examinons le prochain piège avant d'entrer dans la partie la plus juteuse. Deux fournisseurs, chacun créant une relique. C'est très bien. Mais et si deux de ces Reliques n’étaient que des représentations du même composant défini à deux endroits ? Voici un exemple.
AmazonECSProvider analyse la définition de tâche JSON et produit une relique de type AmazonECSTask . Le workflow d'action GitHub comporte également une étape liée à ECS, donc un autre fournisseur crée une relique AmazonECSTaskDeployment . Maintenant, nous avons des doublons car les deux fournisseurs ne se connaissent pas. De plus, il est faux pour l’un d’entre eux de supposer qu’un autre a déjà créé une relique. Et alors ?
Nous ne pouvons supprimer aucun des doublons en raison des définitions (attributs) de chacun d'eux. Le seul moyen est de les fusionner. Par défaut, la logique suivante définit la décision de fusion :
relic1.name() == relic2.name() && relic1.source() != relic2.source()
Nous fusionnons deux reliques si leurs noms sont égaux, mais qu'elles sont définies dans des sources différentes (comme dans notre exemple, JSON dans le référentiel et la référence de définition de tâche se trouve dans les actions GithHub).
Lorsque nous fusionnons, nous :
J'ai intentionnellement omis un aspect crucial d'une relique. Il y a peut-être un Matcher — et c'est mieux de l'avoir ! Le Matcher est une fonction booléenne qui prend un argument et le teste. Les matchers sont des éléments cruciaux d’un processus de liaison. Si une relique correspond à une définition de la relique d'une autre, elles seront liées entre elles.
Vous vous souvenez quand j'ai dit que les fournisseurs n'avaient aucune idée des reliques créées par d'autres fournisseurs ? C'est toujours vrai. Cependant, un fournisseur définit un Matcher pour une relique. En d’autres termes, il représente un côté d’une flèche entre deux cases sur le diagramme résultant.
Exemple. Dockerfile a une instruction ENTRYPOINT.
ENTRYPOINT java -jar /app/arch-diagram-sample.jar
Avec une certaine certitude, nous pouvons dire que Docker conteneurise tout ce qui est spécifié sous ENTRYPOINT . Ainsi, Dockerfile Relic a une fonction Matcher simple : entrypointInstruction.contains(anotherRelicsDefinition)
. Très probablement, certaines reliques Jar avec arch-diagram-sample.jar
dans les définitions y correspondront. Si oui, une flèche entre Dockerfile et Jar Relics apparaît.
Avec Matcher défini, le processus de liaison semble assez simple. Le service de liaison parcourt toutes les reliques et appelle les fonctions de leur Matcher. La relique A correspond-elle à l'une des définitions de la relique B ? Oui? Ajoutez un bord entre ces reliques dans le graphique résultant. Le bord pourrait également être nommé.
La dernière étape consiste à visualiser notre graphique final de l'étape précédente. En plus du PNG évident, l'outil prend en charge des formats supplémentaires, tels que Mermaid , Plant UML et DOT . Ces formats de texte peuvent sembler moins attrayants, mais l'énorme avantage est que vous pouvez intégrer ces textes dans presque n'importe quelle page wiki (
Voici à quoi ressemble le diagramme final de l’exemple de dépôt :
La possibilité de brancher des composants personnalisés ou de modifier la logique existante est essentielle, en particulier lorsqu'un outil en est à sa phase initiale. Les reliques et les sources sont suffisamment flexibles par défaut ; vous pouvez y mettre ce que vous voulez. Tous les autres composants sont personnalisables. Les fournisseurs existants ne couvrent pas les ressources dont vous avez besoin ? Implémentez le vôtre en toute simplicité. Vous n'êtes pas satisfait de la logique de fusion ou de liaison décrite ci-dessus ? Aucun problème; ajoutez votre propre LinkStrategy ou MergeStrategy . Emballez le tout dans un fichier JAR et ajoutez-le au démarrage. En savoir plus ici .
La génération d'un diagramme basé sur le code source gagnera probablement du terrain. Et l’outil NoReDraw en particulier (oui, c’est le nom de l’outil dont je parlais). Les contributeurs sont les bienvenus !
L'avantage le plus remarquable (qui vient du nom) est qu'il n'est pas nécessaire de redessiner un diagramme lorsque les composants changent. Le manque d’attention des ingénieurs est la raison pour laquelle la documentation en général (et les diagrammes en particulier) devient obsolète. Avec des outils comme NoReDraw , cela ne devrait plus poser de problème car il est facilement connectable à n'importe quel pipeline PR/CI. N'oubliez pas que la vie est trop courte pour redessiner des schémas 😉