Diagram-as-codeをMCP経由で使ってみた

記事タイトルとURLをコピーする

ディベロップメントサービス1課の上田勝久と申します。昨年12月に中途入社しまして、初の投稿になります。よろしくお願い申し上げます。

研修中にAWS CDKで構築する機会がありまして、CDKのコードを書いているときに「今どんな構成になっているか」をサクッと図解できるといいなと思っていたのですが、やはり先駆者がいらっしゃいました。

blog.serverworks.co.jp

そこで紹介されているDiagram-as-codeというツール(以下awsdacと書きます)に注目したところ、GitHubのREADMEでMCP Server Integrationなるものを見つけました。その時点ではMCPとは何かもよくわかっていなかったので、今回勉強がてら試してみます。

MCPとは?

MCP(Model Context Protocol)とは、AIアプリケーションを外部システムに接続するためのオープンソース標準規格です。(Model Context Protocolより一部を和訳)

今までのAIは「絵を描く手順」は知っていても「筆」を持っていませんでした。MCPは、AIに特定のツールの「握り方」を教えるための共通規格です。つまり、awsdacのMCP Server Integrationがあるということは、MCP Clientに対応しているAIエージェントがawsdacを使って構成図を書くことができるというわけです。

今回の話ではこのようになります:

  • MCP Client: VSCode + Copilot Chat
  • MCP Server: awsdac-mcp-server
  • 外部ツール: awsdac

事前準備

私の開発環境はWindows11がベースです。今回は下記のものはインストール済みとして話を進めます。

  • エディタ: VSCode
  • バージョンマネージャー: proto
  • JavaScriptランタイム: Node.js (protoでインストール)
  • パッケージマネージャー: pnpm (protoでインストール)
  • Go言語 (scoopでインストール)

Diagram-as-codeのMCP Server Integrationのインストール

macOSであればbrew install awsdacした際に一緒にインストールされるようなのですが、そうではないのでgo installでawsdacとは別のコマンド(awsdac-mcp-server)をインストールする必要があります。

$ go install github.com/awslabs/diagram-as-code/cmd/awsdac-mcp-server@latest

次にMCP ClientとなるVSCodeで、使用できるMCPサーバーに上記のコマンドを追加します。

{
  "servers": {
    "awsdac": {
      "command": "path\\to\\awsdac-mcp-server.exe",   (※)
      "args": [],
      "type": "stdio"
    }
  }
}

(※ Windowsではこのようなパスで設定します: C:\\Users\\user\\go\\bin\\awsdac-mcp-server.exe)

VSCodeの拡張一覧にMCPサーバー - インストール済みという項目があり、そこにawsdacが入っているのを確認(下図①)したら、Copilot Chatでawsdacが使用できる状態か聞いてみましょう。(下図②)

VSCodeにMCPサーバーを登録

getDiagramAsCodeFormatなどの具体的な機能の名前が表示されたりしていれば、AI(Copilot Chat)がawsdacを扱えることを認識したことになります!

使ってみる

EC1台の例

VPCの中にパブリックサブネットが1つ、その中にEC2インスタンスが1つという簡単な構成です。CDKでこのように定義してみました。

const app = new cdk.App();
new BaseStack(app, 'BaseStack', {
  env: { region: 'ap-northeast-1' }
});
export class BaseStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const network = new Network(this, 'Network')
    new Compute(this, 'Compute', { network });
  }
}
export class Network extends Construct {
  public readonly vpc: ec2.Vpc;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    // VPCを作成
    this.vpc = new ec2.Vpc(this, 'Vpc', {
      maxAzs: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PublicSubnet',
          subnetType: ec2.SubnetType.PUBLIC,
          mapPublicIpOnLaunch: true,
        },
      ],
    });
  }
}
export interface ComputeConstructProps {
    readonly network: Network;
}

export class Compute extends Construct {
    public readonly instance: ec2.Instance;
    public readonly securityGroup: ec2.SecurityGroup;

    constructor(scope: Construct, id: string, props: ComputeConstructProps) {
        const { network } = props;

        super(scope, id);

        // セキュリティグループで22/tcpを開放
        this.securityGroup = new ec2.SecurityGroup(this, 'LinuxInstanceSecurityGroup', {
            description: 'Security group for EC2 Linux instance',
            vpc: network.vpc,
            allowAllOutbound: true,
        });
        // SSH接続を許可する
        this.securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow inbound SSH from SWX');

        // EC2インスタンスをtech-kadai-public-subnet-1に作成
        this.instance = new ec2.Instance(this, 'Instance', {
            vpc: network.vpc,
            vpcSubnets: {
                subnets: network.vpc.publicSubnets,
            },
            instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
            machineImage: ec2.MachineImage.latestAmazonLinux2023(),
            securityGroup: this.securityGroup,
        });
    }
}

この状態でVSCodeのCopilot Chatに「bin/cdk-dac.tsで作成しているBaseStackの内容を読み取り、AWSアーキテクチャー図を生成し、sample1-haiku-45.pngに出力してください。」と指示してみたところ、このような図が出力されました。

Claude Haiku 4.5での実行例

なんとなく雰囲気はあっているような気はしますが…

回答をしてきたモデルはClaude Haiku 4.5だったので、モデルを変えてstacks/base.tsファイルを読み取るところからやり直してもらいました。結果は次の通りです。

Claude Haiku 4.5 Claude Sonnet 4.5
Claude Haiku 4.5での実行例
Claude Sonnet 4.5での実行例
Gemini 3 Flash Gemini 3 Pro
Gemini 3 Flashでの実行例
Gemini 3 Proでの実行例

大差はないのですが、セキュリティグループが単体で表示されていたりリージョンとVPCが合体していたり、微妙な違いはあります。

Application Load BalancerとWebサーバー2台

ちょっと複雑にした例です。VPCにプライベートサブネットを2つ追加し、それぞれにWebサーバー(EC2インスタンス)を1台ずつ設置します。パブリックサブネットにApplication Load Balancerを設置し、インターネットからのHTTPリクエストをWebサーバーに転送する構成です。Webサーバーからのアウトバウンド用途でNAT Gatewayも1つ設置しています。

const app = new cdk.App();
new BaseStack(app, 'BaseStack', {
  env: { region: 'ap-northeast-1' }
});
export class BaseStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    const network = new Network(this, 'Network');
    new WebServer(this, 'WebServer', { network });
  }
}
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

export class Network extends Construct {
  public readonly vpc: ec2.Vpc;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    // VPCを作成
    this.vpc = new ec2.Vpc(this, 'Vpc', {
      maxAzs: 2,
      natGateways: 1,
      natGatewaySubnets: {
        subnetType: ec2.SubnetType.PUBLIC,
      },
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PublicSubnet',
          subnetType: ec2.SubnetType.PUBLIC,
          mapPublicIpOnLaunch: true,
        },
        {
          cidrMask: 24,
          name: 'PrivateSubnet',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });
  }
}
export interface WebServerConstructProps {
  readonly network: Network;
}

export class WebServer extends Construct {
  public readonly alb: elbv2.ApplicationLoadBalancer;

  constructor(scope: Construct, id: string, props: WebServerConstructProps) {
    const { network } = props;

    super(scope, id);

    // Application Load Balancerをパブリックサブネットに作成
    this.alb = new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', {
      vpc: network.vpc,
      vpcSubnets: {
        subnets: network.vpc.publicSubnets,
      },
      internetFacing: true,
    });

    // ターゲットグループを作成、HTTP(80)で待ち受け、ヘルスチェックを設定
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
      vpc: network.vpc,
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targetType: elbv2.TargetType.INSTANCE,
      healthCheck: {
        enabled: true,
        path: '/',
        protocol: elbv2.Protocol.HTTP,
      },
    });

    // HTTPリスナーを作成
    this.alb.addListener('HttpListener', {
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      defaultTargetGroups: [targetGroup],
    });

    // Webサーバー用セキュリティグループを作成
    const webServerSecurityGroup = new ec2.SecurityGroup(this, 'WebServerSecurityGroup', {
      vpc: network.vpc,
      description: 'Security group for Web Server EC2 instances',
      allowAllOutbound: true,
    });
    // ALBセキュリティグループからの80/tcpのみ許可
    webServerSecurityGroup.addIngressRule(
      ec2.Peer.securityGroupId(this.alb.connections.securityGroups[0].securityGroupId),
      ec2.Port.tcp(80),
      'Allow inbound HTTP from ALB',
    );

    // UserDataでApacheをインストール、自動起動を設定し、ホスト名表示インデックスページを作成
    const createUserData = (hostname: string): ec2.UserData => {
      return ec2.UserData.custom(`\
#cloud-config
hostname: ${hostname}
preserve_hostname: true
manage_etc_hosts: true
runcmd:
  - dnf install -y httpd
  - systemctl enable httpd
  - systemctl start httpd
  - [sh, -c, 'echo "<html><body><h1>Hostname: ${hostname}</h1></body></html>" > /var/www/html/index.html']
`);
    };

    // EC2インスタンス作成のカリー化関数
    const createWebServerInstance =
      (subnets: ec2.ISubnet[]) =>
      (id: string, instanceName: string): ec2.Instance => {
        return new ec2.Instance(this, id, {
          vpc: network.vpc,
          vpcSubnets: { subnets },
          instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
          machineImage: ec2.MachineImage.latestAmazonLinux2023(),
          securityGroup: webServerSecurityGroup,
          userData: createUserData(instanceName),
        });
      };

    // 2台のEC2インスタンスをプライベートサブネットに作成
    for (const [index, subnet] of network.vpc.privateSubnets.entries()) {
      const createInSubnet = createWebServerInstance([subnet]);
      const instance = createInSubnet(`WebServer${index + 1}`, `web0${index + 1}`);
      targetGroup.addTarget(new targets.InstanceTarget(instance, 80));
    }
  }
}

Copilot Chatでのエージェントへの指示は先程と同じです。先程と同様にモデルを変えて4パターン出力してみました。

Claude Haiku 4.5 Claude Sonnet 4.5
Claude Haiku 4.5での実行例
Claude Sonnet 4.5での実行例
Gemini 3 Flash Gemini 3 Pro
Gemini 3 Flashでの実行例
Gemini 3 Proでの実行例

パブリックサブネットやNAT Gatewayの表現の有無、リージョンなどで違いが出てきています。実線と破線が重なったりもしていますが、ざっくりとした構成は見えるように思います。

MCPを体験してわかったこと

今までAIがChatで回答すること、コードの補完や書き換えをするのは見てきましたが、MCPに対応していれば外部ツールも使えるようになり、できることが一気に増えて、アドバイザーから実務担当者(エージェント)に進化することを感じました。

今回の構成図の内容を見てみると、リージョンやInternet Gatewayは存在が表現されず、NAT GatewayにEC2のアイコンがついていたり、Linkの実線や破線が重なりまくってわかりづらかったりと図が乱れました。AWSサービスのアイコンが正しくないのはよくないですね。図の精度についてはモデルの選択やプロンプト、ツールのMCP定義などで改善できる可能性はあるかもしれません。

awsdac-mcp-serverは、現状ではCDKで書いたソースコードから構成図を起こすよりは、Usage Examplesのようにプロンプトで大まかな構成を伝えて図に起こしてもらうことを目的としたもののように思いました。

まとめ

「CDKから構成図を作る」という目的から始まりましたが、気づけば 「AIにどうやって外部ツールを使わせられるか」というMCPの奥深さに触れることができました。

CDKから構成図を起こす方法も含め、これからもこういった最新の「開発の自動化・効率化」を追いかけていきたいと思います。