Потому что жизнь слишком коротка, чтобы перерисовывать диаграммы
Недавно я присоединился к новой компании в качестве инженера-программиста . Как это всегда бывает, мне пришлось начинать с нуля. Например: где находится код живого приложения? Как он развертывается? Откуда конфиги? К счастью, мои коллеги проделали фантастическую работу по созданию «инфраструктуры как кода». И я поймал себя на мысли: если все есть в коде, то почему нет инструмента, который мог бы соединить все точки?
Этот инструмент проверит кодовую базу и построит диаграмму архитектуры приложения, выделив ключевые аспекты. Новый инженер может посмотреть на диаграмму и сказать: «Ну ладно, вот как это работает».
Сколько бы я ни искал, ничего подобного я не нашел. Наиболее близкими совпадениями, которые я нашел, были сервисы, рисующие диаграмму инфраструктуры. Некоторые из них я включил в этот обзор , чтобы вы могли рассмотреть их поближе. В конце концов я бросил гуглить и решил попробовать свои силы в разработке чего-нибудь нового.
Сначала я создал пример Java- приложения с помощью Gradle, Docker и Terraform. Конвейер действий GitHub развертывает приложение в Amazon Elastic Container Service. Этот репозиторий будет исходным кодом для инструмента, который я создам (код здесь ).
Во-вторых, я нарисовал очень общую диаграмму того, что я хотел увидеть в результате:
Я решил, что будет два типа ресурсов:
Термин «артефакт» показался мне слишком перегруженным, поэтому я выбрал Relic . Так что же такое Реликвия? Это 90% всего, что вы хотите увидеть. В том числе, но не ограничивается:
Каждая реликвия имеет имя (например, my-shiny-app), необязательный тип (например, Jar) и набор пар ключ → значение (например, путь → /build/libs/my-shiny-app.jar), которые полностью описывает Relic. Они называются определениями . Чем больше определений будет у Relic – тем лучше.
Второй тип — это Source . Источники определяют, создают или предоставляют Реликвии (например, желтые прямоугольники выше). Источник описывает Реликвию в каком-то месте и дает представление о том, откуда она взялась. Хотя источники — это компоненты, из которых мы получаем больше всего информации, на диаграмме они обычно имеют второстепенное значение. Вероятно, вам не понадобится много стрел, идущих от Terraform или Gradle к любой другой реликвии.
Relic и Source имеют отношения многие-ко-многим.
Охватить каждый фрагмент кода невозможно. Современные приложения могут иметь множество фреймворков, инструментов или облачных компонентов. Только в AWS имеется около 950 ресурсов и источников данных для Terraform! Инструмент должен быть легко расширяемым и не связанным по дизайну, чтобы другие люди или компании могли внести свой вклад.
Хотя я большой поклонник невероятно подключаемой архитектуры поставщиков Terraform, я решил построить то же самое, хотя и упрощенное:
У Поставщика есть одна четкая обязанность: создание Реликвий на основе запрошенных исходных файлов. Например, GradleProvider читает файлы *.gradle и возвращает Jar , War или Gz Relics. Каждый поставщик создает реликвии тех типов, о которых он знает. Провайдеры не заботятся о взаимодействии между Реликвиями. Они строят Реликвии декларативно, полностью изолированно друг от друга.
При таком подходе можно легко проникнуть настолько глубоко, насколько захотите. Хорошим примером являются действия GitHub. Типичный YAML-файл рабочего процесса состоит из десятков шагов с использованием слабосвязанных компонентов и сервисов. Рабочий процесс может создать JAR-файл, затем образ Docker и развернуть его в среде. Каждый шаг рабочего процесса может быть охвачен его поставщиком. Итак, разработчики, скажем, Docker Actions создают Provider, связанный только с теми шагами, которые им интересны.
Такой подход позволяет любому количеству людей работать параллельно, добавляя в инструмент больше логики. Конечные пользователи также могут быстро внедрить своих поставщиков (в случае использования какой-либо запатентованной технологии). Дополнительную информацию см. в разделе «Настройка» ниже.
Прежде чем перейти к самой пикантной части, давайте разберемся в следующей ловушке. Два Провайдера, каждый из которых создаёт по одному Реликту. Это нормально. Но что, если две из этих реликвий являются просто представлениями одного и того же компонента, определенного в двух местах? Вот пример.
AmazonECSProvider анализирует JSON определения задачи и создает Relic с типом AmazonECSTAsk . В рабочем процессе действий GitHub также есть шаг, связанный с ECS, поэтому другой поставщик создает реликвию AmazonECSTaskDeployment . Теперь у нас есть дубликаты, потому что оба провайдера ничего друг о друге не знают. Более того, неверно считать, что кто-то из них уже создал Реликвию. И что?
Мы не можем удалить ни один из дубликатов из-за определений (атрибутов), которые есть у каждого из них. Единственный способ — объединить их. По умолчанию следующая логика определяет решение о слиянии:
relic1.name() == relic2.name() && relic1.source() != relic2.source()
Мы объединяем две реликвии, если их имена одинаковы, но они определены в разных источниках (как в нашем примере, JSON в репозитории, а ссылка на определение задачи находится в действиях GithHub).
При слиянии мы:
Я намеренно опустил один важный аспект Реликвии. У него может быть Matcher — и лучше, чтобы он был! Matcher — это логическая функция, которая принимает аргумент и проверяет его. Сопоставители являются важными частями процесса связывания. Если реликвия соответствует какому-либо определению чужой реликвии, они будут связаны друг с другом.
Помните, я говорил, что Провайдеры понятия не имеют о Реликвиях, созданных другими Провайдерами? Это все еще правда. Однако поставщик определяет сопоставитель для реликвии. Другими словами, он представляет собой одну сторону стрелки между двумя прямоугольниками на результирующей диаграмме.
Пример. В Dockerfile есть инструкция ENTRYPOINT.
ENTRYPOINT java -jar /app/arch-diagram-sample.jar
С некоторой уверенностью мы можем сказать, что Docker контейнеризирует все, что указано в ENTRYPOINT . Итак, Dockerfile Relic имеет простую функцию Matcher: entrypointInstruction.contains(anotherRelicsDefinition)
. Скорее всего, некоторые Jar Relics с arch-diagram-sample.jar
в Определениях будут соответствовать ему. Если да, появится стрелка между Dockerfile и Jar Relics.
После определения Matcher процесс связывания выглядит довольно простым. Служба связывания перебирает все реликвии и вызывает их функции Matcher. Соответствует ли Реликвия А какому-либо из определений Реликвии Б? Да? Добавьте ребро между этими реликвиями в полученный график. Край также может быть назван.
Последний шаг — визуализировать наш окончательный график предыдущего этапа. Помимо очевидного PNG, инструмент поддерживает дополнительные форматы, такие как Mermaid , Plant UML и DOT . Эти текстовые форматы могут показаться менее привлекательными, но их огромное преимущество состоит в том, что вы можете вставлять эти тексты практически в любую вики-страницу (
Вот как выглядит окончательная диаграмма примера репозитория:
Возможность подключать собственные компоненты или настраивать существующую логику очень важна, особенно когда инструмент находится на начальной стадии. Реликвии и источники по умолчанию достаточно гибки; вы можете положить в них все, что захотите. Любой другой компонент настраивается. Существующие поставщики не покрывают необходимые вам ресурсы? Легко реализуйте свои собственные. Не устраивает описанная выше логика слияния или связывания? Без проблем; добавьте свою собственную LinkStrategy или MergeStrategy . Упакуйте все в JAR-файл и добавьте при запуске. Подробнее читайте здесь .
Создание диаграммы на основе исходного кода, вероятно, получит распространение. И в частности инструмент NoReDraw (да, это название инструмента, о котором я говорил). Соавторы приветствуются !
Самым замечательным преимуществом (вытекающим из названия) является отсутствие необходимости перерисовывать диаграмму при изменении компонентов. Из-за отсутствия инженерного внимания документация в целом (и диаграммы в частности) устаревает. С такими инструментами, как NoReDraw , это больше не должно быть проблемой, поскольку их легко подключить к любому конвейеру PR/CI. Помните, жизнь слишком коротка, чтобы перерисовывать схемы 😉