paint-brush
自动化应用程序架构图:我如何构建一个工具来从源代码映射代码库经过@vladimirf
29,410 讀數
29,410 讀數

自动化应用程序架构图:我如何构建一个工具来从源代码映射代码库

经过 Vladimir Filipchenko6m2024/07/30
Read on Terminal Reader

太長; 讀書

有没有想过有一种工具可以立即将您的代码转换为清晰的可视化图表?好吧,这正是 NoReDraw 的功能!该工具诞生于软件工程师的挫败感,它可以识别工件和配置等关键组件,并将它们链接在一起以创建全面的架构图。它被设计为超级可定制且易于扩展,确保您的文档保持最新,而无需每次发生更改时重新绘制图表的麻烦。
featured image - 自动化应用程序架构图:我如何构建一个工具来从源代码映射代码库
Vladimir Filipchenko HackerNoon profile picture
0-item

因为生命太短暂,无法重绘图表


我最近加入了一家新公司,担任软件工程师。和往常一样,我必须从头开始。比如:应用程序的代码在哪里?它是如何部署的?配置从哪里来的?值得庆幸的是,我的同事们出色地完成了将一切“基础设施即代码”的工作。所以我不禁想:如果一切都在代码中,为什么没有一个工具来连接所有的点?


此工具将检查代码库并构建应用程序架构图,突出显示关键方面。新工程师可以查看该图并说:“啊,好吧,这就是它的工作原理。”


首先要做的事情

无论我怎么努力搜索,我都找不到类似的东西。我找到的最接近的匹配项是绘制基础设施图的服务。我将其中一些放入此评论中,以便您可以仔细查看。最终,我放弃了谷歌搜索,决定尝试开发一些新的很酷的东西。


首先,我使用 Gradle、 Docker和 Terraform 构建了一个示例Java应用程序。GitHub 操作管道将该应用程序部署在 Amazon Elastic Container Service 上。这个 repo 将成为我将构建的工具的来源(代码在这里)。


其次,我绘制了一个非常高级的图表来说明我希望看到的结果:



我决定有两种类型的资源:

遗迹

我觉得神器这个词太过复杂,所以我选择了遗物。那么遗物是什么?它是你想看到的任何东西的 90%。包括但不限于:

  • 工件(方案中的蓝色框,即 Jar、Docker 镜像),
  • 配置 Terraform 资源(方案上的粉色框,即 EC2 实例、ECS、SQS 队列),
  • Kubernetes 资源,
  • 还有很多很多


每个 Relic 都有一个名称(例如,my-shiny-app)、可选类型(例如,Jar)和一组完整描述 Relic 的键 → 值对(例如,路径 → /build/libs/my-shiny-app.jar)。它们被称为Definitions 。Relic 的定义越多越好。

来源

第二种类型是Source 。Source 定义、构建或提供 Relic(例如上面的黄色框)。Source 描述某个地方的 Relic,并给出它来自哪里的感觉。虽然 Source 是我们获取最多信息的组件,但它们在图表上通常具有次要含义。您可能不需要很多从 Terraform 或 Gradle 到其他 Relic 的箭头。


Relic 和 Source 之间存在多对多关系。


分而治之

覆盖每一段代码是不可能的。现代应用程序可能有许多框架、工具或云组件。仅 AWS 就有大约 950 个 Terraform 资源和数据源!该工具必须易于扩展和解耦,以便其他人或公司可以做出贡献。


虽然我是 Terraform 提供商架构的忠实粉丝,但我还是决定构建相同的架构,尽管它有所简化:

提供者


Provider有一项明确的职责:根据请求的源文件构建 Relic。例如, GradleProvider读取 *.gradle 文件并返回JarWarGz Relic。每个 Provider 都会构建它们所知道的类型的 Relic。Provider 不关心 Relic 之间的交互。它们以声明方式构建 Relic,彼此完全隔离。


通过这种方法,您可以轻松深入到您想要的程度。GitHub Actions 就是一个很好的例子。典型的工作流 YAML 文件由数十个使用松散耦合的组件和服务的步骤组成。工作流可以构建 JAR,然后构建 Docker 映像,并将其部署到环境中。工作流中的每个步骤都可以由其提供程序覆盖。因此,假设Docker Actions的开发人员创建仅与他们关心的步骤相关的提供程序。


这种方法允许任意数量的人并行工作,为工具添加更多逻辑。最终用户还可以快速实现他们的提供程序(在某些专有技术的情况下)。请参阅下面的自定义部分以了解更多信息。


合并还是不合并

在进入最有趣的部分之前,让我们先看看下一个陷阱。两个 Provider,每个 Provider 创建一个 Relic。这很好。但如果其中两个 Relic 只是在两个地方定义的同一组件的表示,该怎么办?以下是一个例子。


AmazonECSProvider解析任务定义 JSON 并生成类型为AmazonECSTask 的Relic。GitHub 操作工作流也有一个与 ECS 相关的步骤,因此另一个提供程序创建了一个AmazonECSTaskDeployment Relic。现在,我们有了重复项,因为两个提供程序彼此一无所知。此外,任何一方都不应该假设另一方已经创建了 Relic。然后呢?


遗物合并


由于每个重复项都有定义(属性),因此我们无法删除任何一个重复项。唯一的方法是合并它们。默认情况下,下一个逻辑定义合并决策:


 relic1.name() == relic2.name() && relic1.source() != relic2.source()


如果两个 Relics 的名称相同,但是它们是在不同的 Sources 中定义的,我们会将它们合并(就像在我们的示例中,repo 中的 JSON 和任务定义引用在 GithHub Actions 中)。


当我们合并时,我们:

  1. 选择单个名称
  2. 合并所有定义(键 → 值对)
  3. 创建引用两个原始源的复合源


画一条线

我故意忽略了 Relic 的一个关键方面。它可能有一个Matcher — 最好有它!Matcher 是一个布尔函数,它接受一个参数并对其进行测试。Matcher 是链接过程的关键部分。如果一个 Relic 与另一个 Relic 的任何定义匹配,它们将被链接在一起。


还记得我说过 Provider 对其他 Provider 创建的 Relic 一无所知吗?这仍然是正确的。但是,Provider 为 Relic 定义了一个 Matcher。换句话说,它表示结果图上两个框之间箭头的一侧。


遗物匹配


示例。Dockerfile 有一个 ENTRYPOINT 指令。


 ENTRYPOINT java -jar /app/arch-diagram-sample.jar


我们可以肯定地说,Docker 会将ENTRYPOINT下指定的所有内容都容器化。因此, Dockerfile Relic 有一个简单的 Matcher 函数: entrypointInstruction.contains(anotherRelicsDefinition) 。最有可能的是,Definitions 中带有arch-diagram-sample.jar的一些Jar Relics 会与其匹配。如果是, DockerfileJar Relics 之间会出现一个箭头。


定义 Matcher 后,链接过程看起来非常简单。链接服务遍历所有 Relic 并调用它们的 Matcher 函数。Relic A 是否与 Relic B 的任何定义匹配?是吗?在结果图中在这些 Relic 之间添加一条边。该边也可以命名。


可视化

最后一步是可视化前一阶段的最终图表。除了明显的 PNG 之外,该工具还支持其他格式,例如MermaidPlant UMLDOT 。这些文本格式可能看起来不太吸引人,但巨大的优势是您可以将这些文本嵌入到几乎任何 wiki 页面中( GitHub 汇合还有很多)。


示例 repo 的最终图表如下:

最终图表


定制

插入自定义组件或调整现有逻辑的能力至关重要,尤其是在工具处于初始阶段时。默认情况下,Relics 和 Sources 足够灵活;您可以将任何内容放入其中。其他每个组件都是可自定义的。现有提供程序未涵盖您需要的资源?轻松实现您自己的提供程序。对上面描述的合并或链接逻辑不满意?没问题;添加您自己的LinkStrategyMergeStrategy 。将所有内容打包到 JAR 文件中并在启动时添加。在此处阅读更多信息。


结尾

基于源代码生成图表可能会受到关注。特别是NoReDraw工具(是的,这就是我所说的工具的名称)。欢迎贡献者


最显著的好处(从名字就可以看出)是组件更改时无需重新绘制图表。缺乏工程关注是文档(尤其是图表)过时的原因。使用NoReDraw之类的工具,这不再是问题,因为它可以轻松插入任何 PR/CI 管道。记住,生命太短暂,没有时间重新绘制图表😉