因为生命太短暂,无法重绘图表
我最近加入了一家新公司,担任软件工程师。和往常一样,我必须从头开始。比如:应用程序的代码在哪里?它是如何部署的?配置从哪里来的?值得庆幸的是,我的同事们出色地完成了将一切“基础设施即代码”的工作。所以我不禁想:如果一切都在代码中,为什么没有一个工具来连接所有的点?
此工具将检查代码库并构建应用程序架构图,突出显示关键方面。新工程师可以查看该图并说:“啊,好吧,这就是它的工作原理。”
无论我怎么努力搜索,我都找不到类似的东西。我找到的最接近的匹配项是绘制基础设施图的服务。我将其中一些放入此评论中,以便您可以仔细查看。最终,我放弃了谷歌搜索,决定尝试开发一些新的很酷的东西。
首先,我使用 Gradle、 Docker和 Terraform 构建了一个示例Java应用程序。GitHub 操作管道将该应用程序部署在 Amazon Elastic Container Service 上。这个 repo 将成为我将构建的工具的来源(代码在这里)。
其次,我绘制了一个非常高级的图表来说明我希望看到的结果:
我决定有两种类型的资源:
我觉得神器这个词太过复杂,所以我选择了遗物。那么遗物是什么?它是你想看到的任何东西的 90%。包括但不限于:
每个 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 文件并返回Jar 、 War或Gz 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 中)。
当我们合并时,我们:
我故意忽略了 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 会与其匹配。如果是, Dockerfile和Jar Relics 之间会出现一个箭头。
定义 Matcher 后,链接过程看起来非常简单。链接服务遍历所有 Relic 并调用它们的 Matcher 函数。Relic A 是否与 Relic B 的任何定义匹配?是吗?在结果图中在这些 Relic 之间添加一条边。该边也可以命名。
最后一步是可视化前一阶段的最终图表。除了明显的 PNG 之外,该工具还支持其他格式,例如Mermaid 、 Plant UML和DOT 。这些文本格式可能看起来不太吸引人,但巨大的优势是您可以将这些文本嵌入到几乎任何 wiki 页面中(
示例 repo 的最终图表如下:
插入自定义组件或调整现有逻辑的能力至关重要,尤其是在工具处于初始阶段时。默认情况下,Relics 和 Sources 足够灵活;您可以将任何内容放入其中。其他每个组件都是可自定义的。现有提供程序未涵盖您需要的资源?轻松实现您自己的提供程序。对上面描述的合并或链接逻辑不满意?没问题;添加您自己的LinkStrategy或MergeStrategy 。将所有内容打包到 JAR 文件中并在启动时添加。在此处阅读更多信息。
基于源代码生成图表可能会受到关注。特别是NoReDraw工具(是的,这就是我所说的工具的名称)。欢迎贡献者!
最显著的好处(从名字就可以看出)是组件更改时无需重新绘制图表。缺乏工程关注是文档(尤其是图表)过时的原因。使用NoReDraw之类的工具,这不再是问题,因为它可以轻松插入任何 PR/CI 管道。记住,生命太短暂,没有时间重新绘制图表😉