
Terraform MCP ServerをAmazon ECSにデプロイしてみた
2025年7月にいくつかのMCPサーバーがAWS Marketplaceでサブスクライブできるようになりました。
以前Terraform MCP Serverをサブスクライブして、Amazon Bedrock AgentCoreでデプロイしてみました。
Terraform MCP ServerをAWS Marketplaceでサブスクライブして、Amazon Bedrock AgentCoreを使ってセットアップしてみた | DevelopersIO
デプロイガイドではデプロイ先として、Amazon Bedrock AgentCoreのほかにECSとECS Anywhereを選択可能でした。
今回は、ECS上にデプロイしてみます。
構成
今回は検証のための最小限の構成で構築しました。
クライアントからECSタスクのパブリックIPに直接アクセスするようにします。
本番運用であれば、前段にALBを用意してECSタスクはプライベートサブネットに配置し、直接パブリックIPは付与しないことをおすすめします。
AWS MarketplaceでTerraform MCP Serverをサブスクライブ
手順は上記のブログをご確認ください。
セットアップのガイドで、コンテナのイメージURIが表示されます。
次の手順で使うため、控えておきます。
Remote MCPサーバホスト用のリソース作成(ECS等)
AWSリソースはTerraformで作成します。
以下のファイルを用意します。terraform.tfvars
のdocker imageは前の手順で出力されたものを使います。
docker_image = "1234567890.dkr.ecr.us-east-1.amazonaws.com/hashicorp/terraform-mcp-server:0.2.0" # mcp serverコンテナイメージURI
variable "aws_region" {
description = "AWS region where resources will be created"
type = string
default = "ap-northeast-1"
}
variable "docker_image" {
description = "Docker image for the container"
type = string
}
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Local values
locals {
prefix = "tf-mcp-remote-ecs"
}
# Data sources
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_caller_identity" "current" {}
# VPC Module
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = local.prefix
cidr = "10.0.0.0/16"
azs = data.aws_availability_zones.available.names
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
enable_nat_gateway = false
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Terraform = "true"
Name = "${local.prefix}"
}
}
# Security Group
resource "aws_security_group" "ecs_tasks" {
name_prefix = "${local.prefix}-ecs-tasks"
vpc_id = module.vpc.vpc_id
ingress {
description = "MCP Server Port"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.prefix}-ecs-tasks"
}
}
# IAM Roles
resource "aws_iam_role" "ecs_execution_role" {
name = "${local.prefix}-ecs-execution"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = {
Name = "${local.prefix}-ecs-execution"
}
}
resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" {
role = aws_iam_role.ecs_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role" "ecs_task_role" {
name = "${local.prefix}-ecs-task"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = {
Name = "${local.prefix}-ecs-task"
}
}
# ECS Resources
resource "aws_ecs_cluster" "main" {
name = local.prefix
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = "${local.prefix}"
}
}
resource "aws_ecs_task_definition" "main" {
family = local.prefix
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.ecs_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = jsonencode([
{
name = "terraform-mcp-server"
image = var.docker_image
cpu = 512
memory = 1024
essential = true
portMappings = [
{
containerPort = 80
protocol = "tcp"
}
]
environment = [
{
name = "MCP_SESSION_MODE"
value = "stateless"
},
{
name = "MCP_CORS_MODE"
value = "strict"
},
{
name = "MCP_ALLOWED_ORIGINS"
value = "http://127.0.0.1"
},
{
name = "TRANSPORT_MODE"
value = "streamable-http"
},
{
name = "TRANSPORT_HOST"
value = "0.0.0.0"
},
{
name = "TRANSPORT_PORT"
value = "80"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/${local.prefix}"
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
}
])
tags = {
Name = "${local.prefix}"
}
}
resource "aws_ecs_service" "main" {
name = local.prefix
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = module.vpc.public_subnets
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = true
}
depends_on = [aws_iam_role_policy_attachment.ecs_execution_role_policy]
tags = {
Name = "${local.prefix}"
}
}
resource "aws_cloudwatch_log_group" "ecs" {
name = "/ecs/${local.prefix}"
retention_in_days = 7
tags = {
Name = "${local.prefix}"
}
}
ECSに設定している環境変数等は以下を参考にしました。
Deploy the Terraform model context protocol (MCP) server | Terraform | HashiCorp Developer
Terraformを実行してリソースを作成します。
terraform init
terraform plan
terraform apply
ECSでタスクが起動したらOKです。
動作確認: Claude CodeからTerraform MCP Serverを呼び出す
今回はECSタスクのパブリックIPに直接アクセスする形で呼び出します。
ECSタスクのパブリックIPを確認します。
マネジメントコンソールでECS -> クラスター -> [作成したクラスター] -> [作成したECSサービス] -> タスクの順に選択します。
Healthエンドポイントにcurlしてみます。
curl http://<ECSタスクのパブリックIP>/health
以下のようにStatus OKが返ってくることを確認します。
{"status":"ok","service":"terraform-mcp-server","transport":"streamable-http"}%
次にClaude Codeから呼び出してみます。
.claude.json
に以下を追加します。
"mcpServers": {
"terraform-mcp-server-ecs": {
"type": "http",
"url": "http://<ECSタスクのパブリックIP>/mcp"
}
Claude Codeを起動して、/mcp
と入力します。
claude
/mcp
connected
になっていることが確認できました。
公式ドキュメント内のサンプルプロンプトを入力して、正常に呼び出せるか確認します。
Prompt an AI model connected to the Terraform MCP server | Terraform | HashiCorp Developer
I need help understanding what resources are available in the Google provider that are for AI
以下のようにMCP Serverを使って、レスポンスを返してくることを確認できました。
おわりに
デプロイガイドにあったので、ECSでのデプロイを試してみました。
ローカルから接続することを目的に最小限の構成で作成しました。
本番環境で運用するには、色々追加が必要です。
- ALBの導入
- ECSのPrivate Subnetへの移動
- 認証・認可の仕組みづくり
- ECSタスクのAuto Scaling設定
- etc...
Amazon Bedrock AgentCore Runtimeに比べると手間はかかりますが、より柔軟な構成を組むことが可能です。
Remote MCPサーバーのホスト先としては、Amazon Bedrock Agent Core(2025/8時点ではプレビュー)を検討して、要件が満たせなかったらECSを選択する形がよさそうですね。