はじめに
AWSの構成図、皆さんどうやって作っていますか。
drawioで手作業、Lucidchart、PowerPointのアイコン貼り付け…色々あると思いますが、最近はLLM(ChatGPT, Claudeなど)に「こういう構成図を作って」と頼んでSVGやdraw.io XMLを吐かせる人も増えていると思います。
ただ、これを実運用すると、設計は合っているのに作図品質が安定しないという壁にぶつかりがちです。要素が被る、線が交差する、修正のたびに別の場所が壊れる。
この記事では、その課題を「LLMに直接描かせる」のをやめて、Pythonのdiagramsライブラリ経由で機械生成することで解決した話を共有します。
結論を先に書くと、
- 設計の正確性:LLMが担保(チェックリストで強制)
- 作図品質:Graphviz(diagramsのバックエンド)が担保
- 変更追従:コード化されているのでPRレビュー可能
という分業体制に変えたら、同じ品質の構成図をコード100行で初版生成できるようになりました。
ここに至るまでの試行錯誤
最初からこの構成にたどり着いたわけではなく、4回失敗してから方針を変えた経緯があります。先にそこを共有します。
第1〜4幕:drawio手書きの改訂ループ
最初はLLMにdrawioのXMLを直接書かせていました。同じ構成図をv1からv4まで改訂しています。
| Ver | 主な指摘 |
|---|---|
| v1 | NAT GWがPrivate Subnetに配置 / ALBがVPC外 / Aurora Writer/Readerが別クラスター扱い / WAFがECRにアタッチ |
| v2 | NAT GW/ALBは修正されたが、AuroraとRedisは依然としてAZごとに別リソース表現 |
| v3 | Cluster枠とReplication Group枠を導入したが、AZ境界と論理境界が交差して破綻 |
| v4 | Cluster枠がAZ枠を貫通 / 凡例エリアと図が重なる / 長い説明ラベルが他要素を貫通 |
| v5 | DB Subnet Groupを2AZにまたがる親クラスターとして導入、論理整合が取れた / 線の重なり、ラベル被りが残存 |
| v6 | V5から大きな内容は変えず配置をすこし変えてみた |
v5の時点で「AWSサービス仕様の正確性95点 / 作図品質(視認性)70点」くらいの感触でした。設計の正確性は到達したけど、綺麗に描く部分が最後まで安定しなかった。
違和感の正体
「ここまで来たら次は完璧」と思いながら4回直したところで、これ以上の進化が見られなさそうと思いました。
状況を整理すると
LLMにSVGやXMLを手書きさせる時、内部では何が起きているか。<rect x="100" y="200" />という文字列だけ見て「他の要素と被るか」を判定するには、頭の中で全要素の座標を2D展開して衝突判定をする必要があります。要素が5個ならまだしも、20個30個と増えると指数的に難しくなり、必ず見落としが出る。
つまりLLMの能力不足ではなく、LLMにレイアウトアルゴリズムが内蔵されていないことが原因です。GraphvizやdagreやELKのような専用ライブラリは「ノード配置→エッジの最短経路計算→衝突解消」をアルゴリズム的に最適化していますが、LLMはそれを手で書いているだけなので、全体最適にならない。
苦手な問題を真正面から解かせていた、ということに気付きました。
第6幕:diagramsライブラリへの切り替え
そこでタスクを分解しました。
| 仕事 | 得意なのは | 担当 |
|---|---|---|
| 構造定義(リソース、接続、グループ) | LLM | LLM |
| AWSサービス仕様の正しさ判定 | LLM | LLM |
| 空間配置の最適化 | アルゴリズム | Graphviz |
| 線の経路計算 | アルゴリズム | Graphviz |
| 要素間距離の確保 | アルゴリズム | Graphviz |
LLMが得意なところと苦手なところを分けて、苦手なところを機械に任せるという方針です。これがこの記事で言いたいことの全てです。
ちなみに切り替え直後に、AZ順序を強制しようとしてinvisible edgeを入れて全体レイアウトを破綻させました。これは「drawio時代と同じ失敗を別の道具でやろうとしていた」だけで、道具を変えても発想が変わらないと同じドツボにハマることが往々にしてありますよね。
全体像
最終的にたどり着いたワークフローは次のようになります。
ポイントはLLMの仕事を「構造定義」までに限定し、空間配置はGraphvizに任せること。
実例:2AZ構成のAPIアーキテクチャ
ALB+ECS Fargate(2AZ)+Aurora Cluster+ElastiCache Replication Group+VPC Endpointという、よくある構成を例にします。
1. diagramsのインストール
pip install diagrams
# Graphvizは別途インストールが必要
# macOS: brew install graphviz
# Ubuntu: sudo apt install graphviz
2. コード
from diagrams import Diagram, Cluster, Edge
from diagrams.aws.network import (
ALB, NATGateway, InternetGateway, Endpoint
)
from diagrams.aws.compute import Fargate, Lambda
from diagrams.aws.database import Aurora, ElastiCache
from diagrams.aws.security import WAF, SecretsManager, IAMRole
from diagrams.aws.storage import S3
from diagrams.onprem.client import Users
graph_attr = {
"fontsize": "14",
"splines": "spline",
"nodesep": "0.7",
"ranksep": "1.4",
"compound": "true",
}
with Diagram(
"api architecture",
show=False,
direction="TB",
graph_attr=graph_attr,
filename="api-arch",
outformat="png",
):
user = Users("internet-users")
with Cluster("AWS Cloud"):
with Cluster("IAM Roles (Global)"):
role_exec = IAMRole("task-execution")
role_app = IAMRole("task-app")
with Cluster("Regional services"):
waf = WAF("waf-acl")
secrets = SecretsManager("aurora-master")
s3ep = S3("S3 GW Endpoint")
with Cluster("VPC (10.0.0.0/16)"):
igw = InternetGateway("igw")
alb = ALB("alb (2AZ)")
with Cluster("AZ: 1a"):
with Cluster("Private Subnet 1a"):
ep_a = Endpoint("VPC EP")
task_a = Fargate("task A")
with Cluster("AZ: 1c"):
with Cluster("Private Subnet 1c"):
ep_c = Endpoint("VPC EP")
task_c = Fargate("task B")
# DB Subnet Groupは2AZをまたぐ単一論理リソースとして描く
with Cluster("DB/Cache Subnet Group (2AZ)"):
with Cluster("Aurora Cluster"):
aurora_w = Aurora("Writer")
aurora_r = Aurora("Reader")
with Cluster("ElastiCache RG"):
redis_p = ElastiCache("Primary")
redis_r = ElastiCache("Replica")
# 接続関係
user >> Edge(label="HTTPS:443", penwidth="2.5") >> igw >> alb
alb >> [task_a, task_c]
task_a >> Edge(color="purple", label="PrivateLink") >> ep_a
task_c >> Edge(color="purple") >> ep_c
[task_a, task_c] >> Edge(color="magenta", label="cluster ep") >> aurora_w
[task_a, task_c] >> Edge(color="magenta", label="primary ep") >> redis_p
aurora_w >> Edge(style="dashed", color="magenta", label="replication") >> aurora_r
redis_p >> Edge(style="dashed", color="magenta", label="replication") >> redis_r
waf >> Edge(color="gray45", label="attach") >> alb
role_exec >> Edge(color="gray60", label="Exec") >> task_a
role_app >> Edge(color="gray60", label="Task") >> task_a
3. 実行
python api-arch.py
# → api-arch.pngが生成される
これでPNGが出力されます。要素の被りはゼロ、線の交差は最小化、クラスター枠は論理階層通りにネストされた図が、コード100行強で出てきます。
LLMが得意なこと、苦手なこと
LLMは次のような作業が得意です。
- リソースの名前や役割を理解する
- AWSサービスの仕様(NAT GWはPublic Subnetに置くべき、AuroraはCluster単位、など)を把握する
- 構造をPythonコードに落とす
一方、次の作業は本質的に苦手です。
- 要素を2D空間で重ならないように配置する
- 線が他の要素を貫通しないように経路を計算する
- 全体最適なレイアウトを保ちながら部分修正する
これは試行錯誤の章で書いた通り、LLMはトークン列を処理する道具で、2D空間最適化のためのアルゴリズムを内蔵していないからです。
Graphvizが得意なこと
Graphvizは逆に、空間配置の最適化のために作られたツールです。
- ノード配置(レイヤー割り当て、レイヤー内順序最適化)
- エッジ経路計算(最短経路、交差最小化)
- 要素間距離の確保(
nodesep,ranksepで制御)
つまり「LLMが苦手なことを、Graphvizが得意」という綺麗な相補関係になっています。
diagramsライブラリの位置付け
diagramsはGraphvizのPythonラッパーで、AWS/GCP/Azure/Kubernetesなどのアイコンを揃えています。
- Pythonコードで構造を記述
- 裏でGraphvizがレイアウト計算
- PNG/SVG/PDFなどで出力
このうち「構造をPythonコードで記述」をLLMに任せるのが、今回のキモです。
diagramsで書く時のテクニック
実運用してみて見つけた、効きどころと注意点を共有します。
1:Clusterの入れ子でAWSの論理階層を表現
AWSの構成図で重要なのは論理階層です。
AWS Cloud
└ Region
└ VPC
└ AZ
└ Subnet
└ Resource
これをwith Cluster(...):のネストで素直に表現できます。
with Cluster("VPC"):
with Cluster("AZ: 1a"):
with Cluster("Private Subnet 1a"):
task_a = Fargate("task A")
特にAZをまたぐ論理リソース(Aurora Cluster, ElastiCache Replication Group, DB Subnet Groupなど)は、AZ枠の外側に親クラスターを作って表現します。
# NG:AZごとにAuroraを別々のクラスターとして描く
with Cluster("AZ: 1a"):
aurora_w = Aurora("Writer")
with Cluster("AZ: 1c"):
aurora_r = Aurora("Reader") # 別物に見えてしまう
# OK:DB Subnet Groupの中にAurora Clusterを1つだけ作る
with Cluster("DB/Cache Subnet Group (2AZ)"):
with Cluster("Aurora Cluster"):
aurora_w = Aurora("Writer")
aurora_r = Aurora("Reader")
手書きだとここで「Cluster枠がAZ境界を貫通する」破綻が起きがちですが、コードで階層を表現すれば構造的に発生しません。
2:Edgeの色と線種で意味を標準化
# 主データフロー:黒太線
user >> Edge(label="HTTPS:443", color="black", penwidth="2.5") >> alb
# PrivateLink:紫実線
task >> Edge(color="purple", label="PrivateLink") >> ep
# レプリケーション:マゼンタ破線
writer >> Edge(style="dashed", color="magenta", label="replication") >> reader
# IAM Role関連付け:グレー細線
role >> Edge(color="gray60", label="Exec") >> task
「主データフローは太い黒」「PrivateLinkは紫」のようにルール化しておくと、複数の構成図を作る時に統一感が出ます。
3:graph_attrでレイアウトの方向性を制御
graph_attr = {
"direction": "TB", # Top to Bottom(LRならLeft to Right)
"splines": "spline", # 線を曲線にする(orthoなら直角)
"nodesep": "0.7", # ノード間の最小距離
"ranksep": "1.4", # 階層間の距離
"compound": "true", # Cluster間のエッジを許可
}
splinesはortho(直角配線)も使いたくなりますが、要素が多いと逆に交差が増えることがあります。splineの方が無難。
注意1:細かい位置調整は反映されない
「AZ-1aを左に、1cを右に配置したい」みたいな指定は、完全にはコントロールできません。Graphvizは内部のヒューリスティックで配置を決めるので、宣言順だけでは順序が逆になることもある。
invisible edgeで順序を強制する方法もありますが、副作用で全体レイアウトが破綻することもあるので、深追いは禁物です(冒頭で書いた通り、これは僕も一度ハマりました)。「AZの左右は本質ではない」と割り切るのが現実解。
注意2:凡例は別途用意する
diagramsには凡例機能がありません。
- Matplotlibで別PNGを作って合成
- Markdownの表で線種・色の意味を列挙
- 注記(※注1〜N)として図外に記述
のいずれかで運用することになります。
注意3:全関係を線で描くと交差地獄
KMSで複数リソースを暗号化、IAM Roleで複数TaskにPolicy付与、のような1対多の関係を全部線で描くと、線が交差して読めなくなります。
「全関係を線で描く」のを諦めて、重要な線だけ描き、その他は注記表に逃がすのがコツです。
# NG:KMSから全暗号化対象に線を引く
kms >> aurora_w
kms >> redis_p
kms >> secrets
# OK:暗号化関係は注記に逃がす(線を引かない)
# 図中のKMSラベルに「※注6: Aurora/Redis/Secretsを暗号化」と書く
drawioとdiagramsの比較
冒頭の試行錯誤で作ったv4(drawio)と、最終的なdiagrams方式を並べた結果です。
| 観点 | drawio | diagrams |
|---|---|---|
| 要素の被り | 残存 | ゼロ |
| 線の交差 | 残存 | 最小化済み |
| 主データフローの可読性 | 追いにくい | 縦方向に整列、追いやすい |
| AWS仕様の正確性 | 95点 | 95点(同等) |
| 変更追従性 | 全部手作業 | PRで対応可能 |
| アイコンサイズ統一 | ばらつきあり | 自動で統一 |
設計の正確性は両方とも到達できます。差は作図品質と保守コストに出る、というのが実感です。
おわりに
「LLMにAWS構成図を作らせる」というと、ついついSVGやdraw.io XMLを直接吐かせがちですが、それだとLLMの弱点(2D空間最適化が苦手)を真正面から突くことになり、品質が安定しません。
タスクを分解して、
- 構造定義はLLM
- 空間配置はGraphviz
- AWS仕様の正しさはチェックリスト
という分業に倒すと、それぞれの道具の得意領域で勝負させられます。
何回かの改訂を繰り返してようやくこの結論にたどり着きましたが、振り返ってみると「LLMにドライバーで釘を打たせていた」だけでした。釘打ち機(diagrams)を渡したら、同じ仕事が10分の1の工数で終わるようになっています。
同じような問題で悩んでいる方の参考になれば幸いです。
最後に筆者がdrawioで書いた場合の構成図です。どの構成図が好みかコメントください。







