Claude DesktopにTerraformファイルから事前AWSコスト見積もりさせてみた

# この記事の内容

Claude Desktopに導入したMCPサーバーのツールを利用して、プロジェクト内のTerraformファイルの内容を分析させ、構築前AWSインフラのコストを見積もりさせました。

# 使用するMCPサーバーとツール

今回の見積もりにあたって、Claude Desktopに登録するMCPサーバーは

- `AWS Cost Analysis MCP Server`
    - 使用ツール：`analyze_terraform_project`
    - 今回の用途：コスト見積
- `Claude Code MCP`
    - 使用ツール：`Read`
    - 今回の用途：ローカルファイル読込

です。導入方法については、今回の本題から逸れるのと、他に詳しく書いてくださっている方がいるので、そちらの記事を参照ください。

## 1. AWS Cost Analysis MCP Server

https://github.com/awslabs/mcp

AWSサービスのコスト見積を事前作成してくれたり、見積に必要な情報を提供してくれます。導入前の計画を目的としたものであり、実際の費用を反映するものではないとのことです。

また、[ドキュメント](https://awslabs.github.io/mcp/servers/cost-analysis-mcp-server/)には以下の記述があります。

> Generate comprehensive cost estimates based on your IaC implementation.
『IaC実装に基づいて包括的なコスト見積りを生成します』
> 

ツールの中に`analyze_terraform_project`が存在しており、ツールのdescriptionでも

> Analyze a Terraform project to identify AWS services.
> 

と記述されているので、これを使います。

## 2. Claude Code MCP

Claude Desktopは通常、ローカルにあるファイルを読むことができないので、ローカルにあるファイルにアクセスするためにClaude Code MCPの`Read`というツールを使用します。

余談ですが、Claude Code をMCPサーバーとして登録しておけば、クレジット消費なしでClaude Codeの機能を使えるので、便利です。

# 対象のインフラ

## 構成図

以下のAWSインフラに対してコスト見積をかけます。

![architecture copy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4072363/87239efe-8a72-4d9b-92b9-768ca6c4d9f9.png)

## Terraformファイル
AWSリソースに直接関係しない`local.tf`, `provider.tf`,`output.tf`などは省略しています。

### 1. cloudwatch_log.tf 
```terraform
# CloudWatch Logs グループ
resource "aws_cloudwatch_log_group" "batch_logs" {
  name              = "/ecs/${local.project_name}"
  retention_in_days = local.env.log_retention_in_days
}
```

### 2. ect.tf

```terraform
# ECR リポジトリを作成
resource "aws_ecr_repository" "batch_repo" {
  name                 = "${local.project_name}-batch"
  force_delete         = true    
  image_tag_mutability = "MUTABLE" 

  image_scanning_configuration {
    scan_on_push = true
  }
}
```
### 3. ecs_task.tf

```terraform
# 統計バッチ用タスク定義
resource "aws_ecs_task_definition" "stats_task" {
  family                   = "${local.project_name}-stats-batch"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = local.env.container_base_config.cpu
  memory                   = local.env.container_base_config.memory
  execution_role_arn       = aws_iam_role.task_execution_role.arn
  task_role_arn            = aws_iam_role.task_role.arn

  container_definitions = jsonencode([
    merge(local.env.container_base_config, {
      name    = "stats-batch"
      image   = "${aws_ecr_repository.batch_repo.repository_url}:latest"
      command = split(" ", local.env.batch_stats_params)
    })
  ])
}

# エクスポートバッチ用タスク定義
resource "aws_ecs_task_definition" "export_task" {
  family                   = "${local.project_name}-export-batch"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = local.env.container_base_config.cpu
  memory                   = local.env.container_base_config.memory
  execution_role_arn       = aws_iam_role.task_execution_role.arn
  task_role_arn            = aws_iam_role.task_role.arn

  container_definitions = jsonencode([
    merge(local.env.container_base_config, {
      name    = "export-batch"
      image   = "${aws_ecr_repository.batch_repo.repository_url}:latest"
      command = split(" ", local.env.batch_export_params)
    })
  ])
}

# クリーンアップバッチ用タスク定義
resource "aws_ecs_task_definition" "cleanup_task" {
  family                   = "${local.project_name}-cleanup-batch"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = local.env.container_base_config.cpu
  memory                   = local.env.container_base_config.memory
  execution_role_arn       = aws_iam_role.task_execution_role.arn
  task_role_arn            = aws_iam_role.task_role.arn

  container_definitions = jsonencode([
    merge(local.env.container_base_config, {
      name    = "cleanup-batch"
      image   = "${aws_ecr_repository.batch_repo.repository_url}:latest"
      command = split(" ", local.env.batch_cleanup_params)
    })
  ])
}
```
### 4. ecs.tf

```terraform
# ECSクラスター
resource "aws_ecs_cluster" "batch_cluster" {
  name = local.project_name

  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}
```

### 5. eventbridge_scheduler.tf

``` terraform
# クリーンアップバッチ用のスケジューラ
resource "aws_scheduler_schedule" "cleanup_schedule" {
  name                = "${local.project_name}-cleanup-schedule"
  description         = "Cleanup batch"
  schedule_expression = "cron(0 3 * * ? *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = aws_ecs_cluster.batch_cluster.arn
    role_arn = aws_iam_role.scheduler_role.arn

    ecs_parameters {
      task_definition_arn = aws_ecs_task_definition.cleanup_task.arn
      launch_type         = "FARGATE"
      task_count          = 1

      network_configuration {
        subnets          = [aws_subnet.batch_private_subnet.id]
        security_groups  = [aws_security_group.batch_tasks.id]
        assign_public_ip = false
      }
    }
  }
}

# エクスポートバッチ用のスケジューラ
resource "aws_scheduler_schedule" "export_schedule" {
  name                = "${local.project_name}-export-schedule"
  description         = "Export batch"
  schedule_expression = "cron(0 3 * * ? *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = aws_ecs_cluster.batch_cluster.arn
    role_arn = aws_iam_role.scheduler_role.arn

    ecs_parameters {
      task_definition_arn = aws_ecs_task_definition.export_task.arn
      launch_type         = "FARGATE"
      task_count          = 1

      network_configuration {
        subnets          = [aws_subnet.batch_private_subnet.id]
        security_groups  = [aws_security_group.batch_tasks.id]
        assign_public_ip = false
      }
    }
  }
}

# 統計バッチ用のスケジューラ
resource "aws_scheduler_schedule" "stats_schedule" {
  name                = "${local.project_name}-stats-schedule"
  description         = "Stats batch"
  schedule_expression = "cron(0 6 * * ? *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = aws_ecs_cluster.batch_cluster.arn
    role_arn = aws_iam_role.scheduler_role.arn

    ecs_parameters {
      task_definition_arn = aws_ecs_task_definition.stats_task.arn
      launch_type         = "FARGATE"
      task_count          = 1

      network_configuration {
        subnets          = [aws_subnet.batch_private_subnet.id]
        security_groups  = [aws_security_group.batch_tasks.id]
        assign_public_ip = false
      }
    }
  }
}
```

### 6. iam.tf

``` terraform
# EventBridge Scheduler用のIAMロール
resource "aws_iam_role" "scheduler_role" {
  name = "${local.project_name}-scheduler-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "scheduler.amazonaws.com"
        }
      }
    ]
  })
}

# EventBridge Scheduler用のポリシー
resource "aws_iam_policy" "scheduler_policy" {
  name        = "${local.project_name}-batch-scheduler-policy"
  description = "Policy for EventBridge Scheduler to run ECS tasks"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = "ecs:RunTask"
        Resource = [
          aws_ecs_task_definition.cleanup_task.arn,
          aws_ecs_task_definition.export_task.arn,
          aws_ecs_task_definition.stats_task.arn
        ]
        Condition = {
          ArnLike = {
            "ecs:cluster" = aws_ecs_cluster.batch_cluster.arn
          }
        }
      },
      {
        Effect = "Allow"
        Action = "iam:PassRole"
        Resource = [
          aws_iam_role.task_execution_role.arn,
          aws_iam_role.task_role.arn
        ]
      }
    ]
  })
}

# EventBridge Scheduler用のポリシーアタッチメント
resource "aws_iam_role_policy_attachment" "scheduler_policy_attachment" {
  role       = aws_iam_role.scheduler_role.name
  policy_arn = aws_iam_policy.scheduler_policy.arn
}

# タスク実行ロール
resource "aws_iam_role" "task_execution_role" {
  name = "${local.project_name}-batch-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      }
    ]
  })
}

# タスク実行ロールポリシーのアタッチ
resource "aws_iam_role_policy_attachment" "task_execution_role_policy" {
  role       = aws_iam_role.task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

resource "aws_iam_role_policy_attachment" "task_ecr_readonly_policy" {
  role       = aws_iam_role.task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}

# タスクロール
resource "aws_iam_role" "task_role" {
  name = "${local.project_name}-batch-task-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      }
    ]
  })
}

# タスクロールポリシー
resource "aws_iam_policy" "batch_task_policy" {
  name        = "${local.project_name}-batch-task-policy"
  description = "Policy for batch task roles"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket"
        ]
        Resource = [
          "arn:aws:s3:::${local.project_name}-exports/*",
          "arn:aws:s3:::${local.project_name}-exports"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "${aws_cloudwatch_log_group.batch_logs.arn}:*"
      },
      {
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage"
        ]
        Resource = "*"
      }
    ]
  })
}

# タスクロールポリシーのアタッチ
resource "aws_iam_role_policy_attachment" "task_role_policy" {
  role       = aws_iam_role.task_role.name
  policy_arn = aws_iam_policy.batch_task_policy.arn
}
```

### 7. security_groups.tf

``` terraform
# バッチ処理用セキュリティグループ
resource "aws_security_group" "batch_tasks" {
  name        = "${local.project_name}-batch-tasks-sg"
  description = "Security group for batch processing tasks"
  vpc_id      = aws_vpc.batch_vpc.id

  # アウトバウンドトラフィックを許可
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# VPCエンドポイント用のセキュリティグループ
resource "aws_security_group" "vpc_endpoint_sg" {
  name        = "${local.project_name}-vpc-endpoint-sg"
  description = "Security group for VPC Endpoints"
  vpc_id      = aws_vpc.batch_vpc.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.batch_tasks.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

### 8.vpc.tf

```terraform
variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "private_subnet_cidrs" {
  type    = string
  default = "10.0.1.0/24"
}

variable "availability_zones" {
  type    = string
  default = "ap-northeast-1a"
}

# VPC
resource "aws_vpc" "batch_vpc" {
  cidr_block = var.vpc_cidr

  enable_dns_support   = true
  enable_dns_hostnames = true
}

# プライベートサブネット
resource "aws_subnet" "batch_private_subnet" {
  vpc_id                  = aws_vpc.batch_vpc.id
  cidr_block              = var.private_subnet_cidrs
  availability_zone       = var.availability_zones
  map_public_ip_on_launch = false
}

# プライベートルートテーブル
resource "aws_route_table" "batch_private_route_table" {
  vpc_id = aws_vpc.batch_vpc.id
}

# プライベートルートテーブルの関連付け
resource "aws_route_table_association" "batch_private_route_table_association" {
  subnet_id      = aws_subnet.batch_private_subnet.id
  route_table_id = aws_route_table.batch_private_route_table.id
}

# CloudWatch Logs用のVPCエンドポイント
resource "aws_vpc_endpoint" "logs" {
  vpc_id              = aws_vpc.batch_vpc.id
  service_name        = "com.amazonaws.ap-northeast-1.logs"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.batch_private_subnet.id]
  security_group_ids  = [aws_security_group.vpc_endpoint_sg.id]
  private_dns_enabled = true
}

# ECR API用のVPCエンドポイント
resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = aws_vpc.batch_vpc.id
  service_name        = "com.amazonaws.ap-northeast-1.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.batch_private_subnet.id]
  security_group_ids  = [aws_security_group.vpc_endpoint_sg.id]
  private_dns_enabled = true
}

# ECR Docker用のVPCエンドポイント
resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = aws_vpc.batch_vpc.id
  service_name        = "com.amazonaws.ap-northeast-1.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.batch_private_subnet.id]
  security_group_ids  = [aws_security_group.vpc_endpoint_sg.id]
  private_dns_enabled = true
}
```


# 結果

## プロンプト

以下のプロンプトを投げました。

>Terraform ファイルの分析をお願いします。
> 1. まず、claude_code.Read ツールを使用して "/mnt/c/private/connectrpc-aws-backend/terraform/modules/batch" ディレクトリ内のファイルを読み込んでください。
> 2. 次に、awslabs.cost-analysis-mcp-server.analyze_terraform_project ツールを使用して "C:/private/connectrpc-aws-backend/terraform/modules/batch" パスのプロジェクトを分析し、インフラストラクチャのコスト見積もりを作成してください。
> 3. 両方のツールの出力に基づいて、月間コストの詳細な内訳を提供してください。

## 結果

> Terraformファイルが見つかりました。それぞれのファイルを読み込んでいきます。まず主要なファイルから見ていきましょう。
>
> ～　Read実行ログ　～
>
> 次に、Terraformプロジェクトを分析して、AWSサービスの使用状況とコスト見積もりを作成します。
>
> ～　analyze_terraform_project実行ログ　～
>
> 次に、更に詳しいコスト情報を取得するために、個々のAWSサービスの価格情報を取得します。
>
> ～　get_pricing_from_web実行ログ　～

順番に各MCPのツールが実行されていることが分かります。

> ## アーキテクチャの概要
> 分析したTerraformファイルに基づき、以下のようなAWSサービスを使用したサーバーレスバッチ処理アーキテクチャが構成されています。
>
> ### 主要コンポーネント
> - Amazon ECS (Fargate): 3種類のバッチタスク（統計、エクスポート、クリーンアップ）を実行
> - Amazon EventBridge Scheduler: 各タスクを定期的にスケジュール実行
> - Amazon CloudWatch Logs: タスクのログを保存
> - Amazon ECR: バッチ処理用のコンテナイメージを保存
> - Amazon VPC: プライベートサブネット、セキュリティグループ、VPCエンドポイント（CloudWatch、ECR用）
> ## インフラストラクチャのサイズと仕様
> 
> ### ECS Fargateのリソース構成
>
> - CPU: 各タスクに256 vCPU（最小サイズ）
> - メモリ: 各タスクに512 MB（最小サイズ）
> - 実行頻度:
>    - 統計バッチ: 毎日6時に実行
>    - エクスポートバッチ: 毎日3時に実行
>    - クリーンアップバッチ: 毎日3時に実行
>
> ### ネットワーク構成
>
> - プライベートサブネット（CIDR: 10.0.1.0/24）
> - セキュリティグループ
> - VPCエンドポイント: CloudWatch Logs、ECR API、ECR DKR
> - インターネット接続なし（プライベートサブネット内で完結）

Terraformファイルの分析自体はできていそうです。そのあと、各セクションで詳細な見積内容が提示されたので、以下順番に見ていきます。

### 1. AWS Fargate

> 各バッチタスクは一日一回実行され、実行時間は平均5分と想定します。
> 
> - CPU: 0.25 vCPU × \\$0.000011244/秒 × 300秒 × 30回 ≈ \\$0.025
> - メモリ: 0.5 GB × \\$0.000001235/GB/秒 × 300秒 × 30回 ≈ \\$0.002
>   - 月額: \\$0.027 × 3 ≈ \\$0.081

当然ですが、タスクの実行時間は仮置きで計算されます。実用の際は、おおよそのタスク時間はプロンプトに含めて投げるとより正確な見積になると思われます。

### 2. Amazon ECR

> ECRの使用料金は以下のとおりです:
>
> - イメージストレージ: 1 GB × \\$0.10/GB/月 ≈ \\$0.10
>   - 月額: \\$0.10

特に問題なさそうです。

### 3. CloudWatch Logs

> ログの生成量は、各タスク実行ごとに0.1 MBと想定します。
> 
> - ログ取り込み: 9 MB (0.1MB × 3タスク × 30日) × \\$0.50/GB/月 ≈ \\$0.00 (無料枠内)
> - ログストレージ: 9 MB × \\$0.03/GB/月 ≈ \\$0.00 (無料枠内)
>   - 月額: \\$0.00 (無料枠内)

5GBまでは無料範囲内なので、無料枠内として無料と計算されています。
なので、他リソースの関係上無料枠内で計算してほしくない場合は予め指定しておく必要がありそうです。

### 4. EventBridge Scheduler

> - スケジュール呼び出し: 90回 (3スケジュール × 30日) × \\$1.00/百万回 ≈ \\$0.00 (無料枠内)
>   - 月額: \\$0.00 (無料枠内)

月間1,400万回までは無料範囲内なので、無料枠内として無料と計算されています。
こちらも、他リソースの関係上無料枠内で計算してほしくない場合は予め指定しておく必要がありそうです。

### 5. VPC

> VPCエンドポイントのみコスト発生します。
>
> - VPCエンドポイント (インターフェース型): 3個 × 720時間 × \\$0.01/時間 ≈ \\$21.60
>   - 月額: \\$21.60

VPCエンドポイントの処理データ量に応じた料金が見積もりに含まれていません。
なので、『処理データ量に伴う料金は考慮しないのですか？』と投げました。
すると、以下の結果が返ってきました。

> - VPCエンドポイント (インターフェース型): 3個 × 720時間 × \\$0.01/時間 ≈ \\$21.60
> - データ処理 (CloudWatch Logs): 9 MB × \\$0.01/GB ≈ \\$0.01（最小課金）
> - データ処理 (ECR API): 3 MB (API呼び出し) × \\$0.01/GB ≈ $0.01（最小課金）
> - データ処理 (ECR Docker): 300 MB (イメージ取得) × \\$0.01/GB ≈ \\$0.03
>   - 月額: \\$22.10

### 6. 総コスト

> - AWS Fargate: \\$0.081
> - Amazon ECR: \\$0.10
> - Amazon CloudWatch Logs: \\$0.00 (無料枠内)
> - Amazon EventBridge Scheduler: \\$0.00 (無料枠内)
> - Amazon VPC: \\$22.10
>   - 総額: \\$22.281

# まとめ

今回は単純な構成だったので、大きなズレはなく、おおまかな費用を算出できました。
あくまで参考として最初に費用規模を概算する用途には良いかなと思います。

ただ、一度、プロンプトを投げて終わりではなく、考慮漏れの要件などを含めて何度か壁打ちすると、結果も細かくなり、自分の視点も拡がるので、そのように使うのがよさそうです。