人生は図を描き直すには短すぎるから
私は最近、ソフトウェア エンジニアとして新しい会社に入社しました。いつものことながら、ゼロから始めなければなりませんでした。たとえば、アプリのコードはどこにあるのか、どのようにデプロイされるのか、構成はどこから来るのか、などです。ありがたいことに、同僚たちがすべてを「インフラストラクチャ アズ コード」にする素晴らしい仕事をしてくれました。そこで、私はこう考えました。 「すべてがコード内にあるのなら、すべての点を結びつけるツールがないのはなぜだろう?」
このツールはコードベースをレビューし、重要な側面を強調したアプリケーション アーキテクチャ ダイアグラムを作成します。新しいエンジニアはダイアグラムを見て、「ああ、なるほど、これが仕組みだ」と言うことができます。
どれだけ検索しても、そのようなものは見つかりませんでした。私が見つけた最も近いものは、インフラストラクチャ図を描くサービスでした。 このレビューでは、それらのいくつかを取り上げましたので、詳しく見てみてください。結局、私はグーグル検索をあきらめて、何か新しいクールなものを開発してみることにしました。
まず、Gradle、 Docker 、Terraform を使用してサンプルJavaアプリを構築しました。GitHub アクション パイプラインは、Amazon Elastic Container Service にアプリをデプロイします。このリポジトリは、これから構築するツールのソースになります (コードはこちら)。
次に、結果として何を実現したいのかを非常に高レベルで図解しました。
リソースには次の 2 種類があると判断しました。
アーティファクトという用語には意味が多すぎると思ったので、遺物 を選びました。では、遺物とは何でしょうか? 遺物とは、見たいものの 90% です。これには以下が含まれますが、これらに限定されません。
すべての Relic には、名前 (例: my-shiny-app)、オプションのタイプ (例: Jar)、および Relic を完全に説明するキー → 値のペアのセット (例: パス → /build/libs/my-shiny-app.jar) があります。これらは定義と呼ばれます。Relic の定義が多ければ多いほど、より優れたものになります。
2 番目のタイプはSourceです。Source は Relic を定義、構築、またはプロビジョニングします (上記の黄色のボックスなど)。Source は Relic をある場所で説明し、その Relic がどこから来たのかを示します。Source は最も多くの情報を取得するコンポーネントですが、通常は図では二次的な意味を持ちます。Terraform または Gradle から他のすべての Relic に向かう矢印はおそらくあまり必要ありません。
Relic と Source には多対多の関係があります。
すべてのコードをカバーするのは不可能です。最新のアプリには、多くのフレームワーク、ツール、クラウド コンポーネントが含まれている可能性があります。AWS だけでも、Terraform 用のリソースとデータ ソースが約 950 個あります。ツールは、他の人や企業が貢献できるように、簡単に拡張でき、設計上分離されている必要があります。
私は、信じられないほどプラグ可能な Terraform プロバイダーのアーキテクチャの大ファンですが、簡略化しては同じものを構築することにしました。
プロバイダーには、要求されたソース ファイルに基づいて Relic を構築するという明確な責任が 1 つあります。たとえば、 GradleProvider は*.gradle ファイルを読み取り、 Jar 、 War 、またはGz Relic を返します。各プロバイダーは、認識しているタイプの Relic を構築します。プロバイダーは Relic 間のやり取りを気にしません。プロバイダーは Relic を宣言的に構築し、互いに完全に分離します。
このアプローチでは、必要なだけ深く掘り下げることが容易です。良い例としては、GitHub Actions があります。一般的なワークフロー YAML ファイルは、疎結合されたコンポーネントとサービスを使用した数十のステップで構成されています。ワークフローでは、JAR をビルドしてから Docker イメージをビルドし、それを環境にデプロイできます。ワークフローのすべてのステップは、そのプロバイダーによってカバーできます。そのため、たとえばDocker Actionsの開発者は、関心のあるステップにのみ関連するプロバイダーを作成します。
このアプローチにより、任意の数の人が並行して作業できるようになり、ツールにロジックを追加できます。エンド ユーザーは、プロバイダーをすばやく実装することもできます (独自のテクノロジの場合)。詳細については、以下の「カスタマイズ」を参照してください。
最も興味深い部分に入る前に、次の罠を見てみましょう。2 つのプロバイダーがあり、それぞれが 1 つの Relic を作成します。これは問題ありません。しかし、これらの Relic のうち 2 つが、2 か所で定義された同じコンポーネントの単なる表現である場合はどうなるでしょうか。次に例を示します。
AmazonECSProvider はタスク定義 JSON を解析し、 AmazonECSTaskタイプの Relic を生成します。GitHub アクションワークフローにも ECS 関連のステップがあるため、別のプロバイダーがAmazonECSTaskDeployment Relic を作成します。両方のプロバイダーが互いについて何も知らないため、重複が発生します。さらに、いずれかのプロバイダーが別のプロバイダーがすでに Relic を作成していると想定するのは誤りです。では、どうすればよいのでしょうか。
それぞれの定義 (属性) があるため、重複したものを削除することはできません。唯一の方法は、それらをマージすることです。デフォルトでは、次のロジックがマージの決定を定義します。
relic1.name() == relic2.name() && relic1.source() != relic2.source()
名前が同じでも、異なるソースで定義されている 2 つの Relic をマージします (この例では、リポジトリ内の JSON とタスク定義参照が GithHub アクション内にあります)。
合併すると、次のようになります。
私は Relic の重要な側面を 1 つ意図的に省略しました。Relic にはMatcherがあるかもしれませんが、あった方がよいのです。Matcher は引数を受け取ってテストするブール関数です。Matcher はリンク処理の重要な部分です。Relic が別の Relic の定義と一致する場合、それらはリンクされます。
プロバイダーは他のプロバイダーによって作成された Relic について何も知らないと言ったことを覚えていますか? それは今でも真実です。ただし、プロバイダーは Relic の Matcher を定義します。言い換えると、結果の図の 2 つのボックス間の矢印の片側を表します。
例: Dockerfile には ENTRYPOINT 命令があります。
ENTRYPOINT java -jar /app/arch-diagram-sample.jar
ある程度の確信を持って、Docker はENTRYPOINTで指定されたものをすべてコンテナ化すると言えます。したがって、 Dockerfile Relic には、 entrypointInstruction.contains(anotherRelicsDefinition)
という単純な Matcher 関数があります。おそらく、 arch-diagram-sample.jar
が Definitions に含まれているJar Relic がこれに一致するでしょう。一致する場合は、 DockerfileとJar Relic の間に矢印が表示されます。
Matcher が定義されていると、リンク プロセスは非常に簡単に見えます。リンク サービスはすべての Relic を反復処理し、それぞれの Matcher の関数を呼び出します。Relic A は Relic B の定義のいずれかと一致しますか? はい? 結果のグラフでそれらの Relic 間にエッジを追加します。エッジに名前を付けることもできます。
最後のステップは、前の段階の最終的なグラフを視覚化することです。明らかな PNG に加えて、このツールはMermaid 、 Plant UML 、 DOTなどの追加の形式をサポートしています。これらのテキスト形式は見た目があまり魅力的ではないかもしれませんが、大きな利点は、これらのテキストをほぼすべての wiki ページ (
サンプル リポジトリの最終的な図は次のようになります。
カスタム コンポーネントをプラグインしたり、既存のロジックを微調整したりする機能は、特にツールが初期段階にある場合には不可欠です。Relics と Sources はデフォルトで十分に柔軟であるため、必要なものを何でも入れることができます。その他のすべてのコンポーネントはカスタマイズ可能です。既存のプロバイダーでは必要なリソースがカバーされませんか? 独自のプロバイダーを簡単に実装できます。上記のマージまたはリンク ロジックに満足できませんか? 問題ありません。独自のLinkStrategyまたはMergeStrategyを追加してください。すべてを JAR ファイルにパックして、起動時に追加します。詳細については、こちらを参照してください。
ソース コードに基づいてダイアグラムを生成することは、おそらく注目を集めるでしょう。特にNoReDrawツール (はい、これは私が話していたツールの名前です)。貢献者を歓迎します。
最も注目すべき利点(名前からわかる)は、コンポーネントが変更されてもダイアグラムを再描画する必要がないことです。エンジニアリングの注意が足りないため、ドキュメント全般(特にダイアグラム)が古くなります。NoReDraw のようなツールを使用すると、PR/CI パイプラインに簡単にプラグインできるため、もう問題にはなりません。ダイアグラムを再描画するには人生は短すぎることを忘れないでください 😉