将 MCP 服务部署到 AWS ECS 的方案¶
部署三个服务 (fetch-mcp, aws-doc-mcp, searxng-mcp)¶
1. 创建 ECR 仓库并推送镜像¶
# 设置变量
export AWS_PAGER=""
PROFILE="your-profile"
REGION="us-east-2"
ACCOUNT_ID=$(aws --profile $PROFILE sts get-caller-identity --query Account --output text)
ECR_REPO="$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
# 创建 ECR 仓库
aws --profile $PROFILE --region $REGION ecr create-repository --repository-name mcp-proxy-uv
aws --profile $PROFILE --region $REGION ecr create-repository --repository-name mcp-proxy-npx
# 登录 ECR
aws --profile $PROFILE --region $REGION ecr get-login-password | docker login --username AWS --password-stdin $ECR_REPO
# 构建镜像
docker build -t mcp-proxy-uv -f mcp-proxy-uv.Dockerfile .
docker build -t mcp-proxy-npx -f mcp-proxy-npx.Dockerfile .
# 标记镜像
docker tag mcp-proxy-uv:latest $ECR_REPO/mcp-proxy-uv:latest
docker tag mcp-proxy-npx:latest $ECR_REPO/mcp-proxy-npx:latest
# 推送镜像
docker push $ECR_REPO/mcp-proxy-uv:latest
docker push $ECR_REPO/mcp-proxy-npx:latest
2. 创建 ECS 集群¶
# 设置变量
CLUSTER_NAME="mcp-services"
# 创建集群
aws --profile $PROFILE --region $REGION ecs create-cluster \
--cluster-name $CLUSTER_NAME \
--capacity-providers FARGATE FARGATE_SPOT \
--default-capacity-provider-strategy capacityProvider=FARGATE_SPOT,weight=1
3. 网络配置¶
# 获取默认 VPC 信息
VPC_ID=$(aws --profile $PROFILE --region $REGION ec2 describe-vpcs \
--filters "Name=isDefault,Values=true" \
--query 'Vpcs[0].VpcId' \
--output text)
echo "VPC ID: $VPC_ID"
# 获取子网信息
SUBNETS=$(aws --profile $PROFILE --region $REGION ec2 describe-subnets \
--filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[?AvailabilityZone==`'"$REGION"'a` || AvailabilityZone==`'"$REGION"'b`].SubnetId' \
--output text)
SUBNET1=$(echo $SUBNETS | cut -d' ' -f1)
SUBNET2=$(echo $SUBNETS | cut -d' ' -f2)
echo "子网 ID: $SUBNET1, $SUBNET2"
# 创建安全组
SG_ID=$(aws --profile $PROFILE --region $REGION ec2 create-security-group \
--group-name mcp-services-sg \
--description "Security group for MCP services" \
--vpc-id $VPC_ID \
--query 'GroupId' \
--output text)
echo "安全组 ID: $SG_ID"
# 添加安全组规则 - 开放 80 端口(ALB path-based 路由)和 8808-8810(直接端口访问)
aws --profile $PROFILE --region $REGION ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
aws --profile $PROFILE --region $REGION ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp \
--port 8808-8810 \
--cidr 0.0.0.0/0
4. 创建负载均衡器和目标组¶
# 创建 ALB
ALB_ARN=$(aws --profile $PROFILE --region $REGION elbv2 create-load-balancer \
--name mcp-services-alb \
--subnets $SUBNET1 $SUBNET2 \
--security-groups $SG_ID \
--query 'LoadBalancers[0].LoadBalancerArn' \
--output text)
echo "ALB ARN: $ALB_ARN"
# 获取 ALB DNS 名称(后续访问使用)
ALB_DNS=$(aws --profile $PROFILE --region $REGION elbv2 describe-load-balancers \
--load-balancer-arns $ALB_ARN \
--query 'LoadBalancers[0].DNSName' \
--output text)
echo "ALB DNS: $ALB_DNS"
# 创建目标组
FETCH_TG_ARN=$(aws --profile $PROFILE --region $REGION elbv2 create-target-group \
--name fetch-mcp-tg \
--protocol HTTP \
--port 8808 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-path '/messages' \
--matcher HttpCode=200-399 \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
echo "Fetch MCP Target Group ARN: $FETCH_TG_ARN"
AWS_DOC_TG_ARN=$(aws --profile $PROFILE --region $REGION elbv2 create-target-group \
--name aws-doc-mcp-tg \
--protocol HTTP \
--port 8809 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-path '/messages' \
--matcher HttpCode=200-399 \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
echo "AWS Doc MCP Target Group ARN: $AWS_DOC_TG_ARN"
SEARXNG_TG_ARN=$(aws --profile $PROFILE --region $REGION elbv2 create-target-group \
--name searxng-mcp-tg \
--protocol HTTP \
--port 8810 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-path '/messages' \
--matcher HttpCode=200-399 \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
echo "SearXNG MCP Target Group ARN: $SEARXNG_TG_ARN"
# 创建 Port 80 监听器 - 使用 path-based 路由(推荐,无需自定义域名)
# 默认路由到 fetch-mcp
LISTENER_80_ARN=$(aws --profile $PROFILE --region $REGION elbv2 create-listener \
--load-balancer-arn $ALB_ARN \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=$FETCH_TG_ARN \
--query 'Listeners[0].ListenerArn' \
--output text)
echo "Port 80 Listener ARN: $LISTENER_80_ARN"
# 添加 path-based 路由规则
# /aws-doc/* 路由到 aws-doc-mcp
aws --profile $PROFILE --region $REGION elbv2 create-rule \
--listener-arn $LISTENER_80_ARN \
--priority 10 \
--conditions Field=path-pattern,Values="/aws-doc/*" \
--actions Type=forward,TargetGroupArn=$AWS_DOC_TG_ARN
# /searxng/* 路由到 searxng-mcp
aws --profile $PROFILE --region $REGION elbv2 create-rule \
--listener-arn $LISTENER_80_ARN \
--priority 20 \
--conditions Field=path-pattern,Values="/searxng/*" \
--actions Type=forward,TargetGroupArn=$SEARXNG_TG_ARN
可选方案:host-header 路由
如果你有自定义域名,也可以使用 host-header 路由。为每个服务创建独立端口监听器(8808/8809/8810),然后基于域名转发:
DOMAIN="ecs.aws.yourdomain.com" # 创建 8808 监听器,默认返回 403 aws --profile $PROFILE --region $REGION elbv2 create-listener \ --load-balancer-arn $ALB_ARN \ --protocol HTTP --port 8808 \ --default-actions Type=fixed-response,FixedResponseConfig="{StatusCode=403,ContentType=\"text/plain\",MessageBody=\"Hostname not allowed\"}" # 添加 host-header 规则 aws --profile $PROFILE --region $REGION elbv2 create-rule \ --listener-arn $FETCH_LISTENER_ARN --priority 10 \ --conditions Field=host-header,Values="fetch.$DOMAIN" \ --actions Type=forward,TargetGroupArn=$FETCH_TG_ARN
5. 创建 ECS 服务¶
首先创建任务定义文件:
fetch-mcp-task.json:
echo $ECR_REPO $REGION
envsubst > /tmp/fetch-mcp-task.json <<-EOF
{
"family": "fetch-mcp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"containerDefinitions": [
{
"name": "fetch-mcp",
"image": "${ECR_REPO}/mcp-proxy-uv:latest",
"portMappings": [
{
"containerPort": 8808,
"protocol": "tcp"
}
],
"command": ["--pass-environment", "--port=8808", "--sse-host", "0.0.0.0", "uvx", "mcp-server-fetch"],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/mcp-services",
"awslogs-region": "${REGION}",
"awslogs-stream-prefix": "fetch-mcp"
}
}
}
]
}
EOF
aws-doc-mcp-task.json:
echo $ECR_REPO $REGION
envsubst > /tmp/aws-doc-mcp-task.json <<-EOF
{
"family": "aws-doc-mcp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"containerDefinitions": [
{
"name": "aws-doc-mcp",
"image": "${ECR_REPO}/mcp-proxy-uv:latest",
"portMappings": [
{
"containerPort": 8809,
"protocol": "tcp"
}
],
"command": ["--pass-environment", "--port=8809", "--sse-host", "0.0.0.0", "--env", "FASTMCP_LOG_LEVEL", "ERROR", "uvx", "awslabs.aws-documentation-mcp-server@latest"],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/mcp-services",
"awslogs-region": "${REGION}",
"awslogs-stream-prefix": "aws-doc-mcp"
}
}
}
]
}
EOF
searxng-mcp-task.json:
echo $ECR_REPO $REGION
echo ${YOUR_SEARXNG_URL:=https://searx.yourdomain.com}
envsubst > /tmp/searxng-mcp-task.json <<-EOF
{
"family": "searxng-mcp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"containerDefinitions": [
{
"name": "searxng-mcp",
"image": "${ECR_REPO}/mcp-proxy-npx:latest",
"portMappings": [
{
"containerPort": 8810,
"protocol": "tcp"
}
],
"command": ["--pass-environment", "--port=8810", "--sse-host", "0.0.0.0", "--env", "SEARXNG_URL", "${YOUR_SEARXNG_URL}", "--", "npx", "-y", "mcp-searxng"],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/mcp-services",
"awslogs-region": "${REGION}",
"awslogs-stream-prefix": "searxng-mcp"
}
}
}
]
}
EOF
注册任务定义并创建服务:
echo $CLUSTER_NAME
# 使用已有的 ecsTaskExecutionRole,如果不存在则创建
EXECUTION_ROLE_ARN=$(aws --profile $PROFILE --region $REGION iam get-role \
--role-name ecsTaskExecutionRole \
--query 'Role.Arn' \
--output text 2>/dev/null)
if [ -z "$EXECUTION_ROLE_ARN" ]; then
# 创建 ECS 任务执行角色
cat > /tmp/task-execution-role-trust.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
EXECUTION_ROLE_ARN=$(aws --profile $PROFILE --region $REGION iam create-role \
--role-name ecsTaskExecutionRole \
--assume-role-policy-document file:///tmp/task-execution-role-trust.json \
--query 'Role.Arn' \
--output text)
aws --profile $PROFILE --region $REGION iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
fi
echo "Task Execution Role ARN: $EXECUTION_ROLE_ARN"
# 创建日志组
aws --profile $PROFILE --region $REGION logs create-log-group --log-group-name /ecs/mcp-services 2>/dev/null || true
# 注册任务定义
aws --profile $PROFILE --region $REGION ecs register-task-definition \
--cli-input-json file:///tmp/fetch-mcp-task.json \
--execution-role-arn $EXECUTION_ROLE_ARN
aws --profile $PROFILE --region $REGION ecs register-task-definition \
--cli-input-json file:///tmp/aws-doc-mcp-task.json \
--execution-role-arn $EXECUTION_ROLE_ARN
aws --profile $PROFILE --region $REGION ecs register-task-definition \
--cli-input-json file:///tmp/searxng-mcp-task.json \
--execution-role-arn $EXECUTION_ROLE_ARN
# 创建服务
aws --profile $PROFILE --region $REGION ecs create-service \
--cluster $CLUSTER_NAME \
--service-name fetch-mcp \
--task-definition fetch-mcp \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET1,$SUBNET2],securityGroups=[$SG_ID],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=$FETCH_TG_ARN,containerName=fetch-mcp,containerPort=8808"
aws --profile $PROFILE --region $REGION ecs create-service \
--cluster $CLUSTER_NAME \
--service-name aws-doc-mcp \
--task-definition aws-doc-mcp \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET1,$SUBNET2],securityGroups=[$SG_ID],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=$AWS_DOC_TG_ARN,containerName=aws-doc-mcp,containerPort=8809"
aws --profile $PROFILE --region $REGION ecs create-service \
--cluster $CLUSTER_NAME \
--service-name searxng-mcp \
--task-definition searxng-mcp \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET1,$SUBNET2],securityGroups=[$SG_ID],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=$SEARXNG_TG_ARN,containerName=searxng-mcp,containerPort=8810"
6. 验证部署¶
# 检查服务状态
aws --profile $PROFILE --region $REGION ecs describe-services \
--cluster $CLUSTER_NAME \
--services fetch-mcp aws-doc-mcp searxng-mcp \
--query 'services[*].[serviceName,runningCount,desiredCount]' \
--output table
# 获取 ALB DNS
echo "ALB DNS: $ALB_DNS"
服务访问地址(通过 ALB port 80 path-based 路由):
- fetch-mcp:
http://<ALB_DNS>/sse - aws-doc-mcp:
http://<ALB_DNS>/aws-doc/sse - searxng-mcp:
http://<ALB_DNS>/searxng/sse
7. 清理资源¶
# 停止服务(将 desired-count 设为 0)
aws --profile $PROFILE --region $REGION ecs update-service --cluster $CLUSTER_NAME --service fetch-mcp --desired-count 0
aws --profile $PROFILE --region $REGION ecs update-service --cluster $CLUSTER_NAME --service aws-doc-mcp --desired-count 0
aws --profile $PROFILE --region $REGION ecs update-service --cluster $CLUSTER_NAME --service searxng-mcp --desired-count 0
# 删除服务
aws --profile $PROFILE --region $REGION ecs delete-service --cluster $CLUSTER_NAME --service fetch-mcp
aws --profile $PROFILE --region $REGION ecs delete-service --cluster $CLUSTER_NAME --service aws-doc-mcp
aws --profile $PROFILE --region $REGION ecs delete-service --cluster $CLUSTER_NAME --service searxng-mcp
# 删除集群
aws --profile $PROFILE --region $REGION ecs delete-cluster --cluster $CLUSTER_NAME
# 删除 ALB 和目标组
aws --profile $PROFILE --region $REGION elbv2 delete-load-balancer --load-balancer-arn $ALB_ARN
aws --profile $PROFILE --region $REGION elbv2 delete-target-group --target-group-arn $FETCH_TG_ARN
aws --profile $PROFILE --region $REGION elbv2 delete-target-group --target-group-arn $AWS_DOC_TG_ARN
aws --profile $PROFILE --region $REGION elbv2 delete-target-group --target-group-arn $SEARXNG_TG_ARN
# 删除安全组
aws --profile $PROFILE --region $REGION ec2 delete-security-group --group-id $SG_ID
# 删除日志组
aws --profile $PROFILE --region $REGION logs delete-log-group --log-group-name /ecs/mcp-services