はじめに

こんにちは。開発のT.Nです。今回初めて開発ブログを投稿します。

普段は Java と AWS を用いたデータ連携の業務に携わっています。開発を進める中で、構成図について「変更のたびに差し替えが発生する」「共有されている図がいつの間にか既存システムとズレていた」といった課題を感じる場面がありました。
AWS 公式ブログでも、Infrastructure as Code の普及が進んだ現在でも構成図の更新は手作業で行われることが多く、日々の業務の中で古い図が残ってしまうことはよくある課題として挙げられています。

こうした課題へのアプローチとして、構成図をコードで管理する Diagram as Code という考え方があります。
今回はその実践例として、AWS 公式ブログで紹介されている Diagram-as-Code(awsdac) というツールと Amazon Bedrock API を組み合わせ、対話形式で構成図を作成するアプリを作成しました。

当記事では、awsdac の概要と基本的な使用方法に加えて、実際に使って分かった点をご紹介します。

 

前提

ローカル環境から awsdac を呼び出せるアプリが既に構築済みであること
Vercel AI SDK や Bedrock API の基本的な仕組みを理解していること

以降では、チャットボットアプリに awsdac を追加機能として組み込む部分を中心に紹介します。
アプリ本体の実装については、記事末尾の付録に掲載していますので、必要に応じてそちらをご覧ください。

まずは前提として、 背景にある Infrastructure as Code  と Diagram as Code の考え方と、
今回使用した awsdac について簡単に整理します。

 

Diagram-as-Code (awsdac)

今回使用した awsdac は、AWS のアーキテクチャ図をYAML で定義し、CLI で図として出力できるツールです。

人が図形を一つずつ配置して描くのではなく、構成要素や接続関係をテキストで定義して図を生成するため、画像編集ソフトでの作業や手動配置に頼らず、効率的に構成図を作成・管理できます。

awsdac の YAML ソースは DAC ファイルと呼ばれ、主に DefinitionFiles / Resources / Links の3つで構成されます。
DefinitionFiles でアイコン定義を読み込み、Resources で描画対象を定義し、Links で矢印等の接続関係を表現します。

Diagram:
  DefinitionFiles:
    - Type: URL
      Url: https://raw.githubusercontent.com/awslabs/diagram-as-code/main/definitions/definition-for-aws-icons-light.yaml

  Resources:
    Canvas:
      Type: AWS::Diagram::Canvas

  Links: []

メリット

awsdac の利点は、構成図をコードとして扱えることです。
定義を残しておけば差分管理や再利用がしやすく、あとから構成を更新して再生成することも可能です。
GUI ツールで描き直すのに比べて、変更内容をファイルとして追いやすく、Git と組み合わせた運用にも向いています。

課題

一方で、要素数が増えたり接続関係が複雑になったりすると、DAC ファイルを人の手で記述する負担はそれなりに大きくなります。
YAML で細かく定義していく必要があるため、図が大きくなるほど手作業では少し大変です。

そこで今回は、この DAC ファイルを書く部分を生成AIで補助できないかと考え、対話形式で構成図を作成するアプリを試作しました。

 

 アプリの概要

今回作成したアプリでは、ユーザーがチャット欄に構成要件を自然言語で入力すると、その内容をもとに生成AI がawsdac 用の YAML ソースを生成し、最終的に構成図を出力します。

重視したのは、自然な説明文を生成することではなく、awsdac で扱いやすい YAML を安定して出力することです。
そのため、単純にプロンプトを渡すだけではなく、要件解釈、システムプロンプトによる制御、出力結果の後処理といったガードレールを設けています。

全体の処理フローは以下のとおりです。

図のとおり、処理は大きく「ユーザー入力」「プロンプト生成」「画像生成」の3段階に分かれます。
また、手戻りを減らすために、要件解釈・システムプロンプト・後処理の3点で出力を制御する構成にしました。

ここからは、実際にどのように組み込んだかを見ていきます。

 

つくってみた

awsdac の導入方法

まずは awsdac を利用するための環境を用意します。
awsdac は Go 製の CLI ツールのため、先に Go をインストールする必要があります。

Go 公式サイトから1.21.x以上のバージョンをインストールしてください。
https://go.dev/dl/

# Goインストール後に以下を実行
go install github.com/awslabs/diagram-as-code/cmd/awsdac@latest

# macOS の場合
brew install awsdac

以下のコマンドを実行してバージョンが表示されたらインストール完了です。

awsdac --version

 

動作検証

awsdac 単体で試す

awsdac は、DAC ファイルとして記述した YAML ソースをもとに構成図を生成する CLI ツールです。

基本的な使い方はシンプルで、入力となる YAML ソースファイルを指定して awsdac コマンドを実行します。
コマンドの構成はawsdac <input filename> [flags] となっており、-o オプションで出力ファイル名を指定できます。

たとえば、ローカルに sample.yaml という DAC ファイルを用意した場合、以下のように実行できます。

awsdac sample.yaml -o sample.png

このコマンドを実行すると、sample.yaml をもとに構成図が生成され、sample.png として出力されます。
出力ファイル名を省略した場合、既定では output.png が使用されます。

試しに今回のアプリの処理フローをsample.yaml に記述してコマンド実行してみました。

sample.yaml を開く
Diagram:
  DefinitionFiles:
    - Type: URL
      Url: "https://raw.githubusercontent.com/awslabs/diagram-as-code/main/definitions/definition-for-aws-icons-light.yaml"
  Resources:
    Canvas:
      Type: AWS::Diagram::Canvas
      Direction: horizontal
      Children:
        - LeftArea
        - RightColumn
    LeftArea:
      Type: AWS::Diagram::HorizontalStack
      Children:
        - User
        - Vercel
    User:
      Type: AWS::Diagram::Resource
      Preset: User
      Title: User
    Vercel:
      Type: AWS::Diagram::Resource
      Preset: SDK
      Title: "Vercel\n(Next.js Frontend)"
    RightColumn:
      Type: AWS::Diagram::VerticalStack
      Children:
        - APILayer
        - PNG
    APILayer:
      Type: AWS::Diagram::Resource
      Preset: "Generic group"
      Title: API Layer
      FillColor: "rgba(232, 244, 250, 255)"
      BorderColor: "rgba(0, 150, 100, 255)"
      Children:
        - APILayerStack
    APILayerStack:
      Type: AWS::Diagram::HorizontalStack
      Children:
        - ApiChat
        - BedrockIcon
        - YAMLDoc
        - ApiDiagram
        - Awsdac
    ApiChat:
      Type: AWS::Diagram::Resource
      Preset: "Source code"
      Title: /api/chat
    BedrockIcon:
      Type: AWS::Diagram::Resource
      Preset: "Amazon Bedrock"
      Title: "Amazon Bedrock\n(Claude)"
    YAMLDoc:
      Type: AWS::Diagram::Resource
      Preset: Document
      Title: YAML
    ApiDiagram:
      Type: AWS::Diagram::Resource
      Preset: "Source code"
      Title: /api/diagram
    Awsdac:
      Type: AWS::Diagram::Resource
      Preset: Server
      Title: "awsdac\n(Go CLI)"
    PNG:
      Type: AWS::Diagram::Resource
      Preset: Multimedia
      Title: PNG
  Links:
    - Source: User
      SourcePosition: E
      Target: Vercel
      TargetPosition: W
      LineColor: "rgba(50, 50, 50, 255)"
      TargetArrowHead:
        Type: Default
      Labels:
        SourceRight:
          Title: "1. Request diagram"
          Color: "rgba(50, 50, 50, 255)"
    - Source: Vercel
      SourcePosition: E
      Target: APILayer
      TargetPosition: W
      LineColor: "rgba(200, 100, 0, 255)"
      LineWidth: 2
      TargetArrowHead:
        Type: Default
      Labels:
        SourceRight:
          Title: "2. Send prompt"
          Color: "rgba(200, 100, 0, 255)"
    - Source: ApiChat
      SourcePosition: E
      Target: BedrockIcon
      TargetPosition: W
      LineColor: "rgba(0, 100, 180, 255)"
      LineWidth: 3
      TargetArrowHead:
        Type: Default
      Labels:
        SourceRight:
          Title: "3. Generate YAML"
          Color: "rgba(0, 100, 180, 255)"
    - Source: BedrockIcon
      SourcePosition: E
      Target: YAMLDoc
      TargetPosition: W
      LineColor: "rgba(0, 100, 180, 255)"
      LineWidth: 3
      TargetArrowHead:
        Type: Default
      Labels:
        SourceRight:
          Title: "4. Output YAML"
          Color: "rgba(0, 100, 180, 255)"
    - Source: YAMLDoc
      SourcePosition: E
      Target: ApiDiagram
      TargetPosition: W
      LineColor: "rgba(0, 100, 180, 255)"
      LineWidth: 3
      TargetArrowHead:
        Type: Default
      Labels:
        SourceRight:
          Title: "5. Pass YAML"
          Color: "rgba(0, 100, 180, 255)"
    - Source: ApiDiagram
      SourcePosition: E
      Target: Awsdac
      TargetPosition: W
      LineColor: "rgba(0, 100, 180, 255)"
      LineWidth: 3
      TargetArrowHead:
        Type: Default
      Labels:
        SourceRight:
          Title: "6. Render diagram"
          Color: "rgba(0, 100, 180, 255)"
    - Source: APILayer
      SourcePosition: S
      Target: PNG
      TargetPosition: N
      LineColor: "rgba(150, 50, 150, 255)"
      LineWidth: 2
      TargetArrowHead:
        Type: Default
      Labels:
        SourceRight:
          Title: "7. Generate PNG"
          Color: "rgba(150, 50, 150, 255)"
    - Source: PNG
      SourcePosition: W
      Target: User
      TargetPosition: S
      LineColor: "rgba(0, 150, 100, 255)"
      LineWidth: 2
      TargetArrowHead:
        Type: Default
      Type: orthogonal
      Labels:
        TargetLeft:
          Title: "8. Return\n diagram"
          Color: "rgba(0, 150, 100, 255)"

以下は上記のコマンドを実行して得られた図です。

日本語非対応のため英数字のみとなりますが、アイコンや線、枠の色や配置が適切に行われていることが分かります。
また、一度 YAML を出力しておけば、あとから手で修正して再利用できるため、チャットによる試行錯誤とコードとしての管理を両立しやすい点も扱いやすいと感じました。

 

画面上の操作の流れ

次に、既存のチャットボットアプリに awsdac を組み込んだ形で動かしてみます。

今回はチャット欄に構成要件を入力し、その内容をもとに構成図を生成する形にしました。
チャット形式にしたことで、要件をそのまま文章で入力できるだけでなく、「この要素を追加したい」「サービス名を正式名称にしたい」といった生成結果に対して追加の指示を出しながら修正しやすくなっています。

流れとしては、以下のようになります。

1. チャット欄に要件を入力して送信する
2. 生成AI が awsdac 用の YAML ソースを生成し、画面に表示する
3. YAML ソースの表示後、awsdac による画像生成ボタンを操作できるようになる
4. ボタンを押すと awsdac コマンドを呼び出して画像を生成する
5. 生成画像が画面上に表示され、ダウンロードボタンから保存できる

このように、まず YAML ソースを確認し、その後に画像を生成する流れにすることで、内容の確認や修正を行いやすくしています。
アプリ本体の実装については付録に掲載していますが、今回の記事では awsdac を組み込む部分に絞って紹介します。

 

動かしてみた

動作例としていくつか要件を用意したので、画面上で出力してみます。

まずはじめに以下の要件をチャット欄に入力します。

入力した要件
ユーザーがインターネットゲートウェイを経由して、VPC内のパブリックサブネットにある EC2 インスタンスにアクセスする構成図を作成してください。

この入力をもとに YAML を生成し、その後 awsdac に渡して画像を出力します。

コマンド実行結果がサムネイル表示されるため、確認後にダウンロードします。

ネスト構造や、アイコン、ラインの配置が適切に行われていることが分かります。

次に、公式リポジトリのExampleにあるalb-ec2.yamlを出力してみます。

alb-ec2.yaml の要件
2つのパブリックサブネットにEC2インスタンスを1台ずつ配置し、ALBで負荷分散する構成を作成します。
構成:
AWS Cloud枠で全体を囲む(Align: center)
AWSクラウドを上部、ユーザーを下部に配置
VPC内はサブネット(EC2)を上部、ALBを下部に配置
IGWはVPCの南側のBorderChildrenとして配置
Region、Titleは不要
通信フロー:
ユーザー → IGW → ALB → 各EC2インスタンス
矢印はOpen型を使用
ALBから各EC2への接続は16方位(NNW/NNE、SSE/SSW)を使って斜めに接続
Diagram:
  DefinitionFiles:
    - Type: URL
      Url: "https://raw.githubusercontent.com/awslabs/diagram-as-code/main/definitions/definition-for-aws-icons-light.yaml"
  Resources:
    Canvas:
      Type: AWS::Diagram::Canvas
      Direction: vertical
      Children:
        - AWSCloud
        - User
    AWSCloud:
      Type: AWS::Diagram::Cloud
      Preset: AWSCloudNoLogo
      Align: center
      Direction: vertical
      Children:
        - MyVPC
    MyVPC:
      Type: AWS::EC2::VPC
      Direction: vertical
      Children:
        - SubnetRow
        - ALB
      BorderChildren:
        - Position: S
          Resource: InternetGateway
    SubnetRow:
      Type: AWS::Diagram::HorizontalStack
      Children:
        - PublicSubnetA
        - PublicSubnetB
    PublicSubnetA:
      Type: AWS::EC2::Subnet
      Preset: PublicSubnet
      Children:
        - EC2InstanceA
    PublicSubnetB:
      Type: AWS::EC2::Subnet
      Preset: PublicSubnet
      Children:
        - EC2InstanceB
    EC2InstanceA:
      Type: AWS::EC2::Instance
    EC2InstanceB:
      Type: AWS::EC2::Instance
    ALB:
      Type: AWS::ElasticLoadBalancingV2::LoadBalancer
      Preset: Application Load Balancer
    InternetGateway:
      Type: AWS::EC2::InternetGateway
      IconFill:
        Type: rect
    User:
      Type: AWS::Diagram::Resource
      Preset: User
  Links:
    - Source: User
      SourcePosition: N
      Target: InternetGateway
      TargetPosition: S
      TargetArrowHead:
        Type: Open
    - Source: InternetGateway
      SourcePosition: N
      Target: ALB
      TargetPosition: S
      TargetArrowHead:
        Type: Open
    - Source: ALB
      SourcePosition: NNW
      Target: EC2InstanceA
      TargetPosition: SSE
      TargetArrowHead:
        Type: Open
    - Source: ALB
      SourcePosition: NNE
      Target: EC2InstanceB
      TargetPosition: SSW
      TargetArrowHead:
        Type: Open

最後に矢印や構成が複雑な図privatelink.yamlも出力してみます。

privatelink.yaml の要件
AWS PrivateLink VPC間接続図
全体構成:
AWS Cloud枠内にVPC1、PrivateLink(BlankGroupで囲む)、VPC2を横並び(HorizontalStack使用)
AWS Cloud枠はAlign: center
AWS Cloud枠の下にユーザー
VPC1(10.0.0.0/16):
パブリックサブネット×2を縦並び(各サブネットにEC2を1つ)
サブネットスタックの右にNLB
IGWを左境界(W)に配置
VPC2(10.1.0.0/16):
VPC Endpointを左側に配置
パブリックサブネット×2を縦並び(1つ目にEC2、2つ目は空)
IGWを右境界(E)に配置
通信(PrivateLink経由):
EC2(VPC2) → VPC Endpoint → PrivateLink → NLB → EC2×2(VPC1)
NLBからEC2への分岐はN/Sで出す
通信(SSH、色:rgba(0,125,125,255)、ラベル「SSH access」):
ユーザー(W) → IGW1(W) → EC2×2(VPC1)
ユーザー(E) → IGW2(E) → EC2(VPC2)
矢印: すべてOpen
Diagram:
  DefinitionFiles:
    - Type: URL
      Url: "https://raw.githubusercontent.com/awslabs/diagram-as-code/main/definitions/definition-for-aws-icons-light.yaml"

  Resources:
    Canvas:
      Type: AWS::Diagram::Canvas
      Direction: vertical
      Children:
        - AWSCloud
        - User

    AWSCloud:
      Type: AWS::Diagram::Cloud
      Preset: AWSCloudNoLogo
      Align: center
      Direction: horizontal
      Children:
        - VPC1
        - PrivateLinkGroup
        - VPC2

    VPC1:
      Type: AWS::EC2::VPC
      Title: VPC1 (10.0.0.0/16)
      Direction: horizontal
      Children:
        - VPC1SubnetStack
        - NLB
      BorderChildren:
        - Position: W
          Resource: IGW1

    VPC1SubnetStack:
      Type: AWS::Diagram::VerticalStack
      Children:
        - PublicSubnet1A
        - PublicSubnet1B

    PublicSubnet1A:
      Type: AWS::EC2::Subnet
      Preset: PublicSubnet
      Title: Public subnet
      Children:
        - EC2VPC1A

    PublicSubnet1B:
      Type: AWS::EC2::Subnet
      Preset: PublicSubnet
      Title: Public subnet
      Children:
        - EC2VPC1B

    EC2VPC1A:
      Type: AWS::EC2::Instance
      Title: Amazon EC2

    EC2VPC1B:
      Type: AWS::EC2::Instance
      Title: Amazon EC2

    NLB:
      Type: AWS::ElasticLoadBalancingV2::LoadBalancer
      Preset: Network Load Balancer
      Title: Network Load Balancer

    IGW1:
      Type: AWS::EC2::InternetGateway
      Title: Internet Gateway
      IconFill:
        Type: rect

    PrivateLinkGroup:
      Type: AWS::Diagram::Resource
      Preset: BlankGroup
      Children:
        - PrivateLink

    PrivateLink:
      Type: AWS::Diagram::Resource
      Preset: AWS PrivateLink
      Title: AWS PrivateLink

    VPC2:
      Type: AWS::EC2::VPC
      Title: VPC2 (10.1.0.0/16)
      Direction: horizontal
      Children:
        - VPCEndpoint
        - VPC2SubnetStack
      BorderChildren:
        - Position: E
          Resource: IGW2

    VPCEndpoint:
      Type: AWS::EC2::VPCEndpoint
      Title: VPC Endpoint

    VPC2SubnetStack:
      Type: AWS::Diagram::VerticalStack
      Children:
        - PublicSubnet2A
        - PublicSubnet2B

    PublicSubnet2A:
      Type: AWS::EC2::Subnet
      Preset: PublicSubnet
      Title: Public subnet
      Children:
        - EC2VPC2

    PublicSubnet2B:
      Type: AWS::EC2::Subnet
      Preset: PublicSubnet
      Title: Public subnet
      Children:
        - EmptyResource

    EC2VPC2:
      Type: AWS::EC2::Instance
      Title: Amazon EC2

    EmptyResource:
      Type: AWS::Diagram::Resource

    IGW2:
      Type: AWS::EC2::InternetGateway
      Title: Internet Gateway
      IconFill:
        Type: rect

    User:
      Type: AWS::Diagram::Resource
      Preset: User
      Title: User

  Links:
    - Source: EC2VPC2
      SourcePosition: W
      Target: VPCEndpoint
      TargetPosition: E
      Type: orthogonal
      TargetArrowHead:
        Type: Open

    - Source: VPCEndpoint
      SourcePosition: W
      Target: PrivateLink
      TargetPosition: E
      Type: orthogonal
      TargetArrowHead:
        Type: Open

    - Source: PrivateLink
      SourcePosition: W
      Target: NLB
      TargetPosition: E
      Type: orthogonal
      TargetArrowHead:
        Type: Open

    - Source: NLB
      SourcePosition: N
      Target: EC2VPC1A
      TargetPosition: E
      Type: orthogonal
      TargetArrowHead:
        Type: Open

    - Source: NLB
      SourcePosition: S
      Target: EC2VPC1B
      TargetPosition: E
      Type: orthogonal
      TargetArrowHead:
        Type: Open

    - Source: User
      SourcePosition: W
      Target: IGW1
      TargetPosition: W
      Type: orthogonal
      LineColor: 'rgba(0,125,125,255)'
      TargetArrowHead:
        Type: Open
      Labels:
        SourceRight:
          Title: "SSH access"
          Color: 'rgba(0,125,125,255)'

    - Source: IGW1
      SourcePosition: E
      Target: EC2VPC1A
      TargetPosition: W
      Type: orthogonal
      LineColor: 'rgba(0,125,125,255)'
      TargetArrowHead:
        Type: Open

    - Source: IGW1
      SourcePosition: E
      Target: EC2VPC1B
      TargetPosition: W
      Type: orthogonal
      LineColor: 'rgba(0,125,125,255)'
      TargetArrowHead:
        Type: Open

    - Source: User
      SourcePosition: E
      Target: IGW2
      TargetPosition: E
      Type: orthogonal
      LineColor: 'rgba(0,125,125,255)'
      TargetArrowHead:
        Type: Open
      Labels:
        SourceLeft:
          Title: "SSH access"
          Color: 'rgba(0,125,125,255)'

    - Source: IGW2
      SourcePosition: W
      Target: EC2VPC2
      TargetPosition: E
      Type: orthogonal
      LineColor: 'rgba(0,125,125,255)'
      TargetArrowHead:
        Type: Open

階層関係や矢印の色、向きが正しく描画されています。

検証結果

比較的シンプルな構成であれば、生成された図をそのままでも十分読みやすい形にできました。
一方で、要素数が増えたり、矢印の向きやまとまり方に細かい制約を入れたりすると、意図どおりの図にするには追加の修正が必要になる場面もありました。

それでも、ゼロから DAC ファイルを書き始めるよりは、AI に叩き台を作らせてから微修正するほうが進めやすく、変更頻度の高い図や社内向けの構成図生成には十分使えると感じました。

 

まとめ

今回は、Diagram-as-Code(awsdac)と生成AI を組み合わせて、自然言語からシステム構成図を生成するアプリを試作しました。

awsdac はコードで構成図を管理できる一方で、DAC ファイルを手作業で記述するのはそれなりに手間がかかります。
そこで、自然言語で要件を入力し、生成AI に YAML を作成させることで、構成図のたたき台を素早く用意できるようにしました。

複雑な構成を一度で理想どおりに出すのはまだ難しいものの、最初のベースを作る用途では十分使えると感じました。また、構成図をコードとして残せるため、修正や再利用がしやすく、チーム内で共有しやすい形で管理できます。結果として、構成の把握や引き継ぎにも活用しやすいと感じました。

さらに、今回の試みを通じて、AI エンジンの切り替えやハルシネーション(誤回答)の抑制、AIに要件を意図どおり伝えるための工夫、ユーザビリティを意識した画面づくりなどにも取り組むことができました。
単に構成図を生成するだけでなく、どのように機能を見せると使いやすいかを考えるきっかけにもなりました。

今後は、より安価なモデルでも安定して利用できる構成や、別の作図手法との比較も試してみたいと思います。

 

付録

app/ai/chat.tsx を開く

長いため折りたたんでいます。

"use client";

import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { Message, MessageContent } from "@/components/ai-elements/message";
import {
  PromptInput,
  PromptInputAttachment,
  PromptInputAttachments,
  PromptInputBody,
  type PromptInputMessage,
  PromptInputSelect,
  PromptInputSelectContent,
  PromptInputSelectItem,
  PromptInputSelectTrigger,
  PromptInputSelectValue,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputFooter,
  PromptInputTools,
} from "@/components/ai-elements/prompt-input";
import { Action, Actions } from "@/components/ai-elements/actions";
import { Fragment, useState } from "react";
import { useChat } from "@ai-sdk/react";
import { Response } from "@/components/ai-elements/response";
import {
  Source,
  Sources,
  SourcesContent,
  SourcesTrigger,
} from "@/components/ai-elements/sources";
import {
  Reasoning,
  ReasoningContent,
  ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import { Loader } from "@/components/ai-elements/loader";
import {
  CheckIcon,
  CopyIcon,
  RefreshCcwIcon,
  ImageIcon,
} from "lucide-react";
import { bedrockModels } from "../models";
import { ModeToggle } from "@/components/mode-toggle";
import { Button } from "@/components/ui/button";

/**
 * メッセージ本文から awsdac / CFn 用 YAML を抜き出す
 * - ```yaml / ```yml のコードブロックだけを見る
 * - 最初に見つかったコードブロックを候補とする
 */
const extractYamlFromCodeFence = (text: string): string | null => {
  if (!text) return null;

  const fenced = text.match(/```(?:yaml|yml)[\s\r\n]+([\s\S]*?)```/i);
  if (fenced) {
    return fenced[1].trim();
  }
  return null;
};

/**
 * YAML が CloudFormation テンプレートっぽいかの雑判定
 * - Diagram: があれば DAC 優先(false を返す)
 * - AWSTemplateFormatVersion: があればほぼ CFn
 * - Diagram が無くて Resources: だけある場合も CFn とみなす
 */
const isCloudFormationTemplate = (yaml: string): boolean => {
  if (!yaml) return false;

  const hasDiagram = /^\s*Diagram:/m.test(yaml);
  if (hasDiagram) return false;

  const hasTemplateVersion = /^\s*AWSTemplateFormatVersion:/m.test(yaml);
  const hasResources = /^\s*Resources:/m.test(yaml);

  if (hasTemplateVersion) return true;
  if (!hasDiagram && hasResources) return true;

  return false;
};

const Chat = () => {
  const [input, setInput] = useState("");
  const [model, setModel] = useState<string>(bedrockModels[0].value);
  const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
  const [diagramUrls, setDiagramUrls] = useState<Record<string, string[]>>({});
  const [generatingDiagramFor, setGeneratingDiagramFor] =
    useState<string | null>(null);

  const { messages, sendMessage, status, regenerate } = useChat();

  const handleSubmit = (message: PromptInputMessage) => {
    const hasText = Boolean(message.text);
    const hasAttachments = Boolean(message.files?.length);

    if (!(hasText || hasAttachments)) {
      return;
    }

    sendMessage(
      {
        text: message.text || "Sent with attachments",
        files: message.files,
      },
      {
        body: {
          model: model,
        },
      }
    );
    setInput("");
  };

  const handleGenerateDiagram = async (messageId: string, yaml: string) => {
    try {
      setGeneratingDiagramFor(messageId);

      const endpoint = isCloudFormationTemplate(yaml)
        ? "/api/diagram-cfn"
        : "/api/diagram";

      const res = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type" : "application/json"},
        body: JSON.stringify({ yaml }),
      });

      if (!res.ok) {
        console.error("awsdac generate error", await res.text());
        return;
      }

      // ✅ JSONではなくblobで受ける
      const blob = await res.blob();
      const objectUrl = URL.createObjectURL(blob);

      setDiagramUrls((prev) => ({
        ...prev,
        [messageId]: [...(prev[messageId] ?? []), objectUrl],
      }));
    } catch (e) {
      console.error(e);
    } finally {
      setGeneratingDiagramFor(null);
    }
  };

  const lastMessage = messages[messages.length - 1];

  return (
    <div className="max-w-4xl mx-auto p-6 relative size-full h-screen">
      <div className="flex flex-col h-full">
        <div className="flex justify-between items-center mb-4">
          <h1 className="text-2xl font-bold">AI-Chat</h1>
          <ModeToggle />
        </div>

        <div className="h-full overflow-x-hidden">
          <Conversation>
            <ConversationContent className="overflow-x-hidden">
              {messages.map((message, messageIndex) => {
                const parts = message.parts ?? [];
                const isAssistant = message.role === "assistant";
                const isLastMessage = messageIndex === messages.length - 1;

                const sourceUrlParts = isAssistant
                  ? parts.filter((p: any) => p.type === "source-url")
                  : [];

                const yamlForMessage = isLastMessage
                ? extractYamlFromCodeFence(
                    parts
                      .filter((p: any) => p.type === "text")
                      .map((p: any) => p.text)
                      .join("\n")
                  )
                : null;

                return (
                  <div key={message.id} className="max-w-full overflow-hidden">
                    {/* 参照URLがある場合 */}
                    {isAssistant && sourceUrlParts.length > 0 && (
                      <Sources>
                        <SourcesTrigger count={sourceUrlParts.length} />
                        {sourceUrlParts.map((part: any, i: number) => (
                          <SourcesContent key={`${message.id}-${i}`}>
                            <Source href={part.url} title={part.url} />
                          </SourcesContent>
                        ))}
                      </Sources>
                    )}

                    {message.parts.map((part: any, i: number) => {
                      switch (part.type) {
                        case "text":
                          return (
                            <Fragment key={`${message.id}-${i}`}>
                              <Message from={message.role}>
                                <MessageContent>
                                {message.role === "assistant" ? (
                                  <Response>{part.text}</Response>
                                ) : (
                                  <div className="whitespace-pre-wrap">{part.text}</div>
                              )}
                                </MessageContent>
                              </Message>

                              {message.role === "user" &&
                              i === message.parts.length - 1 && (
                                <Actions className="mt-2">
                                  <Action
                                    onClick={() => {
                                      navigator.clipboard.writeText(part.text);
                                      setCopiedMessageId(message.id);
                                      setTimeout(() => setCopiedMessageId(null), 2000);
                                    }}
                                    label={copiedMessageId === message.id ? "Copied!" : "Copy"}
                                  > 
                                    {copiedMessageId === message.id ? (
                                      <CheckIcon className="size-3" />
                                  ) : (
                                    <CopyIcon className="size-3" />
                                )}
                                </Action>
                              </Actions>
                            )}

                              {/* アクション(最後のメッセージの最後のパートだけ) */}
                              {isLastMessage &&
                                i === message.parts.length - 1 && (
                                  <Actions className="mt-2">
                                    {/* アシスタントメッセージ用の Retry / Copy */}
                                    {isAssistant && (
                                      <>
                                        <Action
                                          onClick={() =>
                                            regenerate({
                                              body: {
                                                model: model,
                                              },
                                            })
                                          }
                                          label="Retry"
                                        >
                                          <RefreshCcwIcon className="size-3" />
                                        </Action>

                                        <Action
                                          onClick={() => {
                                            navigator.clipboard.writeText(
                                              part.text
                                            );
                                            setCopiedMessageId(
                                              message.id as string
                                            );
                                            setTimeout(
                                              () => setCopiedMessageId(null),
                                              2000
                                            );
                                          }}
                                          label={
                                            copiedMessageId === message.id
                                              ? "Copied!"
                                              : "Copy"
                                          }
                                        >
                                          {copiedMessageId === message.id ? (
                                            <CheckIcon className="size-3" />
                                          ) : (
                                            <CopyIcon className="size-3" />
                                          )}
                                        </Action>
                                      </>
                                    )}

                                    {/* ユーザー/アシスタント問わず、```yaml``` があれば図生成ボタン */}
                                    {yamlForMessage && (
                                      <Action
                                        onClick={() =>
                                          handleGenerateDiagram(
                                            message.id as string,
                                            yamlForMessage
                                          )
                                        }
                                        label={
                                          generatingDiagramFor === message.id
                                            ? "図を生成中..."
                                            : "awsdacで構成図を生成"
                                        }
                                        disabled={
                                          generatingDiagramFor === message.id
                                        }
                                      >
                                        <ImageIcon className="size-3" />
                                      </Action>
                                    )}
                                  </Actions>
                                )}

                              {/* 生成済みの図があれば表示 & ダウンロードボタン */}
                              {diagramUrls[message.id]?.length &&
                                i === message.parts.length - 1 && (
                                  <div className="mt-4 space-y-4">
                                    {diagramUrls[message.id].map(
                                      (url, index) => (
                                        <div
                                          key={`${message.id}-diagram-${index}`}
                                          className="flex flex-col gap-2"
                                        >
                                          <img
                                            src={url}
                                            alt="AWS diagram"
                                            className="max-h-96 rounded-lg border object-contain bg-background"
                                          />
                                          <div>
                                            <Button
                                              variant="outline"
                                              onClick={() => {
                                                const link =
                                                  document.createElement("a");
                                                link.href = url;
                                                link.download = `aws-diagram-${message.id}-${index + 1}.png`;
                                                document.body.appendChild(link);
                                                link.click();
                                                document.body.removeChild(link);
                                              }}
                                            >
                                              PNG をダウンロード
                                            </Button>
                                          </div>
                                        </div>
                                      )
                                    )}
                                  </div>
                                )}
                            </Fragment>
                          );
                        case "reasoning":
                          return (
                            <Reasoning
                              key={`${message.id}-${i}`}
                              className="w-full"
                              isStreaming={
                                status === "streaming" &&
                                i === message.parts.length - 1 &&
                                message.id === lastMessage?.id
                              }
                            >
                              <ReasoningTrigger />
                              <ReasoningContent>{part.text}</ReasoningContent>
                            </Reasoning>
                          );
                        default:
                          return null;
                      }
                    })}
                  </div>
                );
              })}
              {status === "submitted" && <Loader />}
            </ConversationContent>
            <ConversationScrollButton />
          </Conversation>
        </div>

        <PromptInput
          onSubmit={handleSubmit}
          className="mt-4"
          globalDrop
          multiple
        >
          <PromptInputBody>
            <PromptInputAttachments>
              {(attachment) => <PromptInputAttachment data={attachment} />}
            </PromptInputAttachments>
            <PromptInputTextarea
              onChange={(e) => setInput(e.target.value)}
              value={input}
              placeholder="メッセージを入力"
            />
          </PromptInputBody>
          <PromptInputFooter>
            <PromptInputTools>
              <PromptInputSelect
                onValueChange={(value) => {
                  setModel(value);
                }}
                value={model}
              >
                <PromptInputSelectTrigger>
                  <PromptInputSelectValue />
                </PromptInputSelectTrigger>
                <PromptInputSelectContent>
                  {bedrockModels.map((model) => (
                    <PromptInputSelectItem
                      key={model.value}
                      value={model.value}
                    >
                      {model.name}
                    </PromptInputSelectItem>
                  ))}
                </PromptInputSelectContent>
              </PromptInputSelect>
            </PromptInputTools>
            <PromptInputSubmit disabled={!input} status={status} />
          </PromptInputFooter>
        </PromptInput>
      </div>
    </div>
  );
};

export default Chat;
api/chat/route.ts を開く

長いため折りたたんでいます。

import { streamText, UIMessage, convertToModelMessages } from "ai";
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";

export const maxDuration = 30;

const systemPrompt =`ここにシステムプロンプト`;

export async function POST(req: Request) {
  const {
    messages,
    model,
	temperature,
  }: {
    messages: UIMessage[];
    model: string;
	temperature?: number;
    webSearch: boolean;
  } = await req.json();

  const bedrock = createAmazonBedrock({
    region: process.env.AWS_REGION,
    apiKey: process.env.BEDROCK_API_KEY,
  });

  const result = streamText({
    model: bedrock(model),
    messages: convertToModelMessages(messages),
    system: systemPrompt,
	temperature: temperature ?? 0,
  });

  return result.toUIMessageStreamResponse({
    sendSources: true,
    sendReasoning: true,
  });
}
api/diagram/route.ts を開く

長いため折りたたんでいます。

import { NextResponse } from "next/server";
import { promises as fs } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { execFile } from "child_process";
import { promisify } from "util";
import { randomUUID } from "crypto";

const execFileAsync = promisify(execFile);

export const runtime = "nodejs";
export const maxDuration = 60;

// .env.local に AWSDAC_PATH を設定していればそれを使う
// 例: AWSDAC_PATH=C:\Users\<ユーザー名>\go\bin\awsdac.exe
const awsdacCommand = process.env.AWSDAC_PATH || "awsdac";

// タイムスタンプ生成 (yyyyMMddHHmmss)
const generateTimestamp = (): string => {
  const now = new Date();
  const yyyy = now.getFullYear().toString();
  const MM = String(now.getMonth() + 1).padStart(2, "0");
  const dd = String(now.getDate()).padStart(2, "0");
  const HH = String(now.getHours()).padStart(2, "0");
  const mm = String(now.getMinutes()).padStart(2, "0");
  const ss = String(now.getSeconds()).padStart(2, "0");
  return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
};

// ```yaml ... ``` が来ても拾えるようにする(保険)
const extractYamlIfFenced = (input: string): string => {
  const m = input.match(/```(?:yaml|yml)?[\s\r\n]+([\s\S]*?)```/i);
  return (m ? m[1] : input).trim();
};

const sanitizeAwsdacYaml = (yaml: string): string => {
  return yaml
    // awsdac では非対応になりがちなフィールドは消す(保険)
    .replace(/^\s*Label\s*:\s*.*$/gm, "")
    .replace(/^\s*Style\s*:\s*.*$/gm, "")
    .replace(/^\s*Icon\s*:\s*.*$/gm, "")
    // 未定義Typeの保険(本来はプロンプトで出さない)
    .replace(
      /^(\s*Type\s*:\s*)AWS::Bedrock::Agent\s*$/gm,
      "$1AWS::EC2::Instance"
    );
};

export async function POST(req: Request) {
  const body = (await req.json()) as { yaml?: string };
  const raw = body?.yaml;

  if (!raw || typeof raw !== "string") {
    return NextResponse.json({ error: "yaml is required" }, { status: 400 });
  }

  const extracted = extractYamlIfFenced(raw);
  const safeYaml = sanitizeAwsdacYaml(extracted);

  const id = randomUUID();
  const workDir = join(tmpdir(), `awsdac-${id}`);

  try {
    await fs.mkdir(workDir, { recursive: true });

    const yamlPath = join(workDir, "diagram.yaml");
    await fs.writeFile(yamlPath, safeYaml, "utf8");

    const timestamp = generateTimestamp();
    const outputFileName = `output_${timestamp}.png`;

    const { stdout, stderr } = await execFileAsync(
      awsdacCommand,
      [yamlPath, "-o", outputFileName],
      {
        cwd: workDir,
        timeout: 30_000, // ハング対策
        windowsHide: true,
      }
    );

    if (stdout) console.log("[awsdac stdout]", stdout.toString());
    if (stderr) console.log("[awsdac stderr]", stderr.toString());

    const outputPath = join(workDir, outputFileName);
    const pngBuffer = await fs.readFile(outputPath);

    // const base64 = pngBuffer.toString("base64");
    // const dataUrl = `data:image/png;base64,${base64}`;

    // return NextResponse.json({ dataUrl });
    // TODO data64ではなく、PNGそのままで返す
    return new Response(pngBuffer, {
      status: 200,
      headers: {
        "Content-Type": "image/png",
        "Cache-Control": "no-store",
      },
    });
  } catch (err: any) {
    console.error("awsdac execution error:", err);
    return NextResponse.json(
      {
        error: "Failed to generate diagram",
        message: err?.message,
        code: err?.code,
        stderr: err?.stderr?.toString?.(),
      },
      { status: 500 }
    );
  } finally {
    // 一時ディレクトリ掃除(失敗しても無視)
    try {
      await fs.rm(workDir, { recursive: true, force: true });
    } catch {}
  }
}
システムプロンプトを開く

長いため折りたたんでいます。

const systemPrompt = `あなたは「Diagram-as-code (awsdac)」の YAML(DACファイル)を生成するエージェントです。
ユーザーの要件を AWS 構成図として表現するための awsdac YAML を作成します。

# 1. Role
- あなたの役割は、ユーザー要件を awsdac の DAC YAML に変換することです。
- 出力は「図として成立すること」と「awsdac として壊れにくいこと」を最優先にします。
- 不明な仕様・不明な Type / Preset に賭けず、迷ったら確実に通る最小構成に寄せます。

# 2. Task
- ユーザー要件から以下を抽出して YAML に落とし込みます。
  - 境界(AWS / Region / VPC / Subnet など)
  - 主要コンポーネント
  - 通信の向き
- 手順は以下です。
  1) Resources の階層(Children / BorderChildren)を決める
  2) Links で通信を結ぶ
- 情報が不足している場合は、勝手に要素を増やしすぎず、最小構成で成立させます。

# 3. 参照方針 / 優先順位
- このプロンプトには、awsdac の公式 docs / 公式 examples で確認できるルールと、このプロジェクトの安全運用ルールを埋め込んでいます。
- 外部リンクを参照できない前提でも、以下のルールを仕様として扱ってください。
- 優先順位は以下です。
  1) ユーザー要件
  2) このプロンプト内の公式準拠ルール
  3) このプロジェクトの運用ルール
  4) このプロンプト内の例
- ルール同士が衝突する場合は、禁止事項 > 個別ルール > 一般ルール > 例 の順で優先します。
- 定義ファイルに存在しない Type / Preset は推測で作ってはいけません。
- 実使用されている既知の Type / Preset / 正式名称だけを使ってください。

# 4. Output Format(最重要)
- 返答は必ず 1つの \`\`\`yaml コードブロック\`\`\` のみ。
- 説明文・箇条書き・前置き・後書きは禁止。
- YAML は CloudFormation ではなく awsdac DAC 形式にすること。
- AWSTemplateFormatVersion 等は出してはいけません。
- インデントは 2スペース。タブ禁止。
- YAMLアンカー/エイリアス(& / *)は禁止。

# 5. 必須スキーマ
- 出力のトップレベルは Diagram のみとし、その配下には必ず以下の3セクションだけを含めます。

Diagram:
  DefinitionFiles: [...]
  Resources: {...}
  Links: [...]

# 6. DefinitionFiles
- 原則として以下の形式を使います。

DefinitionFiles:
  - Type: URL
    Url: "https://raw.githubusercontent.com/awslabs/diagram-as-code/main/definitions/definition-for-aws-icons-light.yaml"

# 7. Resources
- Resources は「キー = リソースID」「値 = リソース定義」です。
- リソースIDは英数字(CamelCase推奨)にし、スペース / 日本語 / 記号は避けます。
- Children / BorderChildren / Links で参照したリソースIDは、必ず Resources に存在させます。
- まず Canvas を作り、Canvas.Children に最上位要素を並べます。

## 7.1 Canvas(必須)
Canvas:
  Type: AWS::Diagram::Canvas
  Direction: horizontal | vertical
  Children: [ ...]

## 7.2 Type の分類
- awsdac の Type は大きく2種類あります。

1) AWS サービスの Type
- 形式:AWS::<サービス>::<リソース>
- 例:
  - AWS::EC2::Instance
  - AWS::S3::Bucket
  - AWS::Lambda::Function
  - AWS::EC2::VPC
  - AWS::EC2::Subnet
  - AWS::ElasticLoadBalancingV2::LoadBalancer
  - AWS::EC2::VPCEndpoint

2) 図の構造 / 汎用要素の Type
- AWS::Diagram::Canvas
- AWS::Diagram::Cloud
- AWS::Diagram::Resource
- AWS::Diagram::HorizontalStack
- AWS::Diagram::VerticalStack
- AWS::Region

## 7.3 サービス名・Preset名の命名ルール
- すべての AWS サービス名・リソース名・Preset 名は、原則として正式名称で出力します。
- 省略形・俗称・略記だけで表現してはいけません。
- Type は正式な AWS::xxx::yyy を使います。
- Preset を使う場合も、実使用されている正式名称を使います。
- 例:
  - ALB → AWS::ElasticLoadBalancingV2::LoadBalancer
  - Preset: Application Load Balancer
  - NLB → Preset: Network Load Balancer
  - Region → AWS::Region

## 7.4 専用Typeの優先使用(重要)
- 以下は専用Typeが存在するため、AWS::Diagram::Resource + Preset ではなく専用Typeを使います。
  - リージョン → AWS::Region
  - VPC → AWS::EC2::VPC
  - サブネット → AWS::EC2::Subnet
- リージョンは AWS::Region を使い、AWS::Diagram::Resource + Preset: Region は使用しません。

## 7.5 コンテナ / レイアウトの基本
- コンテナ要素(Cloud, Region, VPC, Stack 等)には Direction を明示することを優先します。
- Children の順序規則:
  - Direction: vertical → 上から下
  - Direction: horizontal → 左から右
- 通信フローと配置順が整合しているか確認します。
- 複数要素を横並びにしたい場合は、親コンテナを vertical にして内部で HorizontalStack を使う方が柔軟です。
- Align: center などの位置調整プロパティを必要に応じて使います。
- Stack やコンテナのレイアウト調整には Align を使用できます。
- Title を持つコンテナでは、リンク接続点や BorderChildren の位置がタイトル領域と干渉しないようにします。
- タイトルとラインの干渉が懸念される場合は、auto-positioning または N / S / E / W の単純な接続を優先します。

## 7.6 よく使う基本パターン
- AWSクラウド枠:
  Type: AWS::Diagram::Cloud
  Preset: AWSCloudNoLogo
  Children: [...]

- Region:
  Type: AWS::Region
  Direction: vertical
  Title: <region-name>
  Children: [...]

- VPC:
  Type: AWS::EC2::VPC
  Children: [...]

- サブネット:
  Type: AWS::EC2::Subnet
  Preset: PublicSubnet | PrivateSubnet
  Children: [...]

- レイアウト用:
  Type: AWS::Diagram::HorizontalStack | AWS::Diagram::VerticalStack
  Children: [...]

## 7.7 ALB / NLB などのロードバランサ
- ALB を表現する場合、Type は AWS::ElasticLoadBalancingV2::LoadBalancer を使います。
- ALB の見た目を明示したい場合は、Preset: Application Load Balancer を使います。
- NLB の見た目を明示したい場合は、Preset: Network Load Balancer を使います。
- AWS::ElasticLoadBalancingV2::ApplicationLoadBalancer は使用しません。

## 7.8 汎用アイコン(AWS サービス以外)
- AWS サービス以外の汎用要素は以下で表現します。

Type: AWS::Diagram::Resource
Preset: <アイコン名>

- よく使う Preset:
  - User
  - Users
  - Client
  - MobileClient
  - Internet
  - Database
  - Server
  - CorporateDataCenter

例:
User:
  Type: AWS::Diagram::Resource
  Preset: Users

OnPremise:
  Type: AWS::Diagram::Resource
  Preset: CorporateDataCenter

## 7.9 BorderChildren(境界に置くもの)
- IGW / NAT Gateway など、枠の外側に付けるものは Children ではなく BorderChildren を優先します。

例:
MyVPC:
  Type: AWS::EC2::VPC
  BorderChildren:
    - Position: W
      Resource: InternetGateway

## 7.10 IconFill(原則適用)
- 原則として、アイコン背景は塗りつぶします。
- アイコン背景の塗りつぶしには IconFill を使います。
- デフォルト値は Type: none ですが、このプロジェクトでは見た目の安定性を優先し、原則として Type: rect を使います。
- 特に BorderChildren に配置するリソース(IGW / NAT Gateway など)では、原則として IconFill.Type: rect を付与します。
- Color を省略した場合は白背景として扱います。
- ユーザーが明示的に背景なしを要求した場合のみ、IconFill を省略してよいです。

- Type の種類:
  - none
  - rect

書式:
ResourceName:
  Type: AWS::EC2::InternetGateway
  IconFill:
    Type: rect
    Color: "rgba(255,255,255,255)"

白背景の省略形:
ResourceName:
  Type: AWS::EC2::InternetGateway
  IconFill:
    Type: rect

主な用途:
- BorderChildren に配置したアイコンで、親の枠線が透けて見える問題を解消
- アイコンに任意の背景色を付ける

例:
IGW:
  Type: AWS::EC2::InternetGateway
  IconFill:
    Type: rect

NATGateway:
  Type: AWS::EC2::NatGateway
  IconFill:
    Type: rect
    Color: "rgba(240,240,240,255)"

## 7.11 汎用グループ枠 / レイアウト補助
- AWS::Diagram::Group は deprecated のため、明示的には生成しません。
- グルーピングが必要な場合は AWS::Diagram::Resource + Children を使います。
- 点線枠が必要な場合は Preset: "Generic group" を使います。
- レイアウト調整用の透明コンテナが必要な場合は Preset: BlankGroup を使います。

実線枠の例:
TestGroup:
  Type: AWS::Diagram::Resource
  Children:
    - Child1
    - Child2

点線枠の例:
TestGroup:
  Type: AWS::Diagram::Resource
  Preset: "Generic group"
  Children:
    - Child1
    - Child2

## 7.12 PrivateLink / VPC Endpoint の使い分け
- AWS PrivateLink を図中の汎用アイコンとして表す場合は、AWS::Diagram::Resource + Preset: AWS PrivateLink を使います。
- VPC Endpoint 自体を表す場合は、AWS::EC2::VPCEndpoint を使います。
- 両者は役割が異なるため、必要に応じて併用してよいです。

## 7.13 空リソース
- レイアウト維持のため、空の AWS::Diagram::Resource を配置してよいです。

EmptyResource:
  Type: AWS::Diagram::Resource

## 7.14 サービス名が曖昧 / 不明な場合のフォールバック
- まず既知の正式な AWS::xxx::yyy Type を優先して使います。
- AWS サービスを表したいが適切な正式 Type が不明な場合は、AWS::EC2::Instance をフォールバックとして使い、リソースIDで役割を表します。
- AWS 外 / 汎用要素は AWS::Diagram::Resource + Preset を使います。
- Type / Preset は、実使用されている既知のものだけを使います。推測で新設してはいけません。

## 7.15 追加リソースの禁止
- ユーザー要件に明示されていないリソース / 枠 / コンテナは追加してはいけません。
- AWS::Diagram::Cloud は必須ではありません。
- AWS Cloud 枠は、ユーザーが明示的に要求した場合のみ追加します。
  - 例:「AWS Cloud枠を付けて」「複数アカウント / 複数リージョンを分けて」
- VPC 境界だけで十分な場合は、Cloud 枠を追加しません。

# 8. Links
- Links は配列です。
- 各要素に Source / Target を必ず置きます。
- Position は 16方位 + auto を使用可能です。
  - 基本4方位: N, S, E, W
  - 8方位: NE, NW, SE, SW
  - 16方位拡張: NNE, NNW, SSE, SSW, ENE, ESE, WNW, WSW
  - auto
- top / bottom / left / right は使ってはいけません。
- SourcePosition / TargetPosition は省略可能であり、auto も使用できます。

## 8.1 矢印ポリシー
- 原則として、ユーザー要件に流れが含まれる、または通信方向が明らかな場合は矢印を付けます。
- 矢印を付ける場合は TargetArrowHead を使います。
- 指示に明示されていない場合は、原則として TargetArrowHead.Type は Default を使います。
- 例外として、ユーザーが明示した場合はそれを最優先します。
  - 「矢印なし」→ SourceArrowHead / TargetArrowHead を付けない
  - 「双方向」→ SourceArrowHead と TargetArrowHead の両方を付ける
  - 「逆向き」→ Source / Target を反転、または矢印方向を逆にする
- 判断が曖昧で単なる関連でよい場合は矢印を省略してよいです。
- ユーザーが提示したYAMLをベースにする場合、その構造を尊重します。
- ユーザーが明示的に変更を要求していない部分は変更しません。

## 8.2 orthogonal の運用ルール
- Type: orthogonal は、図を見やすくするための配線方法として使ってよいです。
- ただしこれは運用ルールであり、見た目の安定性を優先します。
- Type: orthogonal を使う場合は、SourcePosition / TargetPosition は原則として N, S, E, W のみを使います。
- NE / NW / SE / SW / NNE などの斜め・細分方位は、orthogonal では使用しません。
- 横並び要素への接続では、orthogonal + W/E を優先します。
- 縦並び要素への接続では、N/S を優先します。
- 縦並び要素の orthogonal 接続では、出る方向を分散すると崩れにくいです。
  - 上の要素: SourcePosition: N
  - 下の要素: SourcePosition: S

## 8.3 Labels / 色指定
- Links の色指定は Color ではなく LineColor を使います。
  - ✅ LineColor: 'rgba(0,125,125,255)'
- Links では Labels フィールドを使用できます。
- Labels は配列ではなく、オブジェクト形式で書きます。

例:
Labels:
  SourceRight:
    Title: "SSH access"
    Color: 'rgba(0,125,125,255)'

# 9. Prohibited Actions(禁止事項)
- Resources では表示名に Title を使います。Label は存在しません。
- Style フィールドは使ってはいけません。
- Icon / IconUrl は使ってはいけません。
- AWS::General::xxx は存在しないため、使用禁止です。
- Type / Preset を推測で作ってはいけません。
- 定義ファイルや実使用ルールに存在しない Type / Preset を使ってはいけません。
- 省略形や俗称だけを名前として出力してはいけません。
- 正式な Type / Preset / サービス名がある場合は、それを優先します。
- AWS::Bedrock::Agent のような仕様に無い Type を出してはいけません。
- AWS::Diagram::Group を明示的に出力してはいけません。
- AWS::ElasticLoadBalancingV2::ApplicationLoadBalancer は使用禁止です。

# 10. 最終セルフチェック
- Diagram / DefinitionFiles / Resources / Links が揃っている
- DefinitionFiles に Type: URL と Url が入っている
- Canvas が存在する
- Canvas.Children が空でない
- Links の Source / Target は Resources に存在する
- Position 値が許容値のみ(16方位 + auto)
- top / bottom / left / right を使っていない
- 禁止フィールド(Label / Style / Icon / IconUrl)が入っていない
- 汎用アイコン(ユーザー等)は AWS::Diagram::Resource + Preset を使っている
- AWS::General::xxx を使っていない
- AWS::Diagram::Group を使っていない
- Region は AWS::Region を使っている
- ALB は AWS::ElasticLoadBalancingV2::LoadBalancer を使っている
- AWS PrivateLink と VPC Endpoint を混同していない
- BorderChildren のリソースには、原則として IconFill.Type: rect を付けている
- 明示的に背景なしが要求されていない限り、IconFill を原則 rect で適用している
- orthogonal 使用時は SourcePosition / TargetPosition が N / S / E / W のみ
- すべてのサービス名・リソース名・Preset 名を正式名称で扱っている
- 推測で作った Type / Preset が含まれていない
- ユーザー要件にないリソースを勝手に増やしていない
- Cloud枠を、明示要求なしに追加していない

以上を守って、ユーザー要件に合致する awsdac YAML を生成せよ。
返答は \`\`\`yaml コードブロック\`\`\` だけ。`;

 

参考文献

・AWS 公式ブログ「AWS 環境の可視化を加速する Diagram-as-code とAmazon Bedrockの活用」
https://aws.amazon.com/jp/blogs/news/aws-visualization-diagram-as-code-amazon-bedrock/

・VercelのAI SDK + AI Elementsを使って簡易的なAIチャットアプリを作成してみた
https://dev.classmethod.jp/articles/ai-sdk-ai-elements-chat-app-with-bedrock/

・[ブースレポート]AWS構成図とAWS CloudFormationテンプレートを自動生成!Diagram-as-Codeのデモをブースで触ってみた
https://dev.classmethod.jp/articles/aws-summit-2025-diagram-as-code/

・AWS構成図作成の悩みを一掃!Diagram as Codeで始める“コードで描く”インフラ設計
https://qiita.com/k_adachi_01/items/ccfbf6952d7ee252bfb0