6가지 AWS 추론 백엔드를 위한 단일 MCP 서버: Bedrock, SageMaker, GPU, Inferentia 및 Trainium
요약
본 글은 AWS 환경에서 Amazon Bedrock, SageMaker, vLLM(GPU), Inferentia2, Trainium 등 6가지 백엔드를 통합 관리하는 MCP(Master Control Program) 서버 구축 방법을 안내합니다. 이 단일 서버는 모든 추론 백엔드의 목록화, 시작, 상태 확인, 로그 읽기 등의 기능을 하나의 채팅 세션에서 제공하여 복잡성을 줄입니다.
핵심 포인트
- AWS의 6가지 추론 백엔드를 통합 관리하는 MCP 서버를 구현했습니다.
- Bedrock, SageMaker, vLLM(GPU), Inferentia2 등 다양한 환경을 단일 인터페이스로 제어합니다.
- JSON-RPC와 Python SDK를 사용하여 모든 기능을 표준화된 방식으로 제공합니다.
이 글은 AWS에서 여섯 가지 백엔드 추론 데모를 구동하는 MCP(Master Control Program) 서버에 대한 단계별 가이드를 제공합니다. 이 백엔드는 Amazon Bedrock, SageMaker 엔드포인트, EC2 g6 및 g5g의 vLLM, 그리고 Inferentia2와 Trainium에서의 Gemma 4입니다. 총 8개의 Python MCP 도구는 하나의 채팅 세션에서 모든 백엔드를 목록화하고, 시작하며, 상태를 확인하고, 로그를 읽고, 질문을 하고, 종료하는 기능을 수행합니다.
https://github.com/xbill9/aws-inference-strands
문제점은 무엇인가요?
이 서버 뒤에 있는 데모는 하나의 Strands 에이전트가 AWS에서 모델을 서비스하는 여섯 가지 방법을 앞에 두고 작동합니다. 이를 실행한다는 것은 최대 4개 리전에 걸쳐 5개의 청구 가능한 리소스를 사용하며, 그중 2개는 AWS Neuron 칩에 기반하고, 어떤 인스턴스도 인터넷에서 접근할 수 없다는 것을 의미합니다.
이 저장소에는 이미 모든 것을 위한 셸 스크립트가 있습니다: stage/up.sh, stage/forward.sh, stage/check.py, stage/ask.py 및 stage/down.sh. 각 스크립트는 터미널의 사람에게 출력합니다.
MCP 서버는 에이전트에게 동일한 작업을 수행하게 합니다. 이는 코드로 실행되는 산술 연산과 확인 기능을 가진 도구들을 사용하여
서버는 mcp_server.py라는 단일 파일로 구성되어 있으며 stdio를 통해 시작됩니다. 리포지토리 루트에 있는 .mcp.json 파일이 Claude Code에서 이를 등록합니다:
{
"mcpServers": {
"strands-demo": {
...
모든 MCP 클라이언트는 동일한 방식으로 서버를 시작할 수 있습니다. stdin에 JSON-RPC의 세 줄만 있으면 이 서버가 제공하는 기능을 확인할 수 있습니다:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"printf","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
...
{
"protocolVersion": "2025-11-25",
"capabilities": {"tools": {"listChanged": false}, "prompts": {"listChanged": false},
...
이 서버는 MCP Python SDK 2.1.1과 그 MCPServer 클래스를 사용하여 실행되며, 프로토콜 버전 2025-11-25를 협상합니다. tools/list는 여덟 개의 도구를 반환합니다.
2단계 — 데모 자체 코드 위에 단일 파일 구현하기
서버는 데모가 이미 사용하는 두 모듈을 가져와 그 주변에 AWS 호출을 추가합니다.
Claude Code / Strands MCPClient
│ stdio, JSON-RPC 2.0
▼
...
이 파일을 하나로 유지하는 네 가지 설계 규칙이 있습니다:
- 재사용 후 래핑.
ask_backend는 함수stage/ask.py,stage/check.py, 벤치마크 및 데모 2 호출의backends.chat_once를 호출합니다.count_ids는 인-프로세스 도구 호출의 함수demo1_counting.count를 호출합니다. 서버에서 오는 숫자와 스크립트에서 오는 숫자는 동일한 코드 라인에서 옵니다. - 실행 및 정리 단계(Launch and teardown run the stage scripts).
launch_backends는stage/up.sh를 시작하고teardown은stage/down.sh를 시작하여, MCP 경로와 문서화된 경로 모두 동일한 리소스를 생성하고 파괴합니다. - 인바운드 포트 없음. 모든 인스턴스는 인바운드 규칙이 없는 보안 그룹에 위치합니다. 해당 박스가 필요한 도구는 SSM Run Command를 통해 그 위에서 셸 명령을 실행합니다.
- 태그가 인벤토리입니다(The tag is the inventory).
stage/up.sh는 모든 인스턴스에ManagedBy=strands-demo-run태그를 지정하며, 서버의 모든 조회는us-east-2,us-east-1,us-west-2,us-west-1전반에 걸쳐 이 태그로 필터링됩니다. 용량 오류로 인해 다른 리전으로 떨어진 백엔드도 여전히 발견할 수 있습니다.
boto3는 동기적이므로, 각 호출은 asyncio.to_thread를 통해 워커 스레드에서 실행됩니다. 이를 통해 하나의 도구가 asyncio.gather를 사용하여 네 개의 리전을 한 번에 조회할 수 있게 합니다:
async def _instances() -> list[dict]:
per_region = await asyncio.gather(*[_region_instances(r) for r in REGIONS])
return [i for found in per_region for i in found]
3단계 — 읽기 전용, 쓰기 및 파괴적 (Read-Only, Write and Destructive)
모든 도구는 MCP 도구 어노테이션(tool annotations)을 가지고 있으므로, 클라이언트는 아무것도 호출하기 전에 인벤토리 조회인지, 비용 청구가 발생하는지, 아니면 삭제를 수행하는 것인지를 알 수 있습니다.
READ_ONLY = ToolAnnotations(readOnlyHint=True, idempotentHint=True)
WRITE = ToolAnnotations(destructiveHint=False)
DESTRUCTIVE = ToolAnnotations(destructiveHint=True)
tools/list가 반환하는 내용은 MCP 클라이언트를 통해 다시 읽어보면 다음과 같습니다:
| Tool | Annotation | Arguments | What it touches |
|---|---|---|---|
list_backends | read-only | none | EC2, SSM 및 SageMaker에서 4개 리전의 describe 호출 |
| ... | |||
| 이 기사의 나머지 부분은 데모 당일(demo day)에 사용되는 순서대로 도구를 다룹니다. |
Step 4 — list_backends: 무엇이 실행 중이고 무엇이 비용 청구 대상인지
list_backends는 태그가 지정된 인스턴스를 찾기 위해 네 개의 리전을 검색하고, 각 실행 중인 인스턴스가 체크인했는지 SSM에 문의하며, SageMaker에 엔드포인트 상태를 요청하고, 현재 비용이 청구되는 항목들을 합산합니다. 카운트는 도구 내에서 계산되므로 에이전트가 이를 인용합니다.
아무것도 실행되지 않았을 때:
{
"backends": [
{"backend": "bedrock", "where": "us-east-1 (managed)", "state": "always on", "ssm": "-"},
...
다섯 개의 AWS describe 호출(EC2당 1개, SageMaker당 1개)에 대해 한 번의 호출이 0.78초가 걸렸습니다. 실행 중인 인스턴스가 추가될 때마다 SSM 호출이 발생합니다.
ask_backend_configured는 다섯 개가 다운된 상태에서 여섯 개의 모든 백엔드를 나열합니다. 이는 demo.env 파일에 URL을 지정하고, stage/forward.sh 파일은 stage 디렉터리에 작성되며, 이 파일은 포워드(forwards)보다 오래 지속됩니다. 도구 설명서에 그렇게 명시되어 있으며, Step 8에서는 해당 백엔드 중 하나로의 호출이 반환하는 내용을 보여줍니다.
Step 5 — launch_backends 및 launch_log: 비용 청구를 시작하고, 실행 기록을 읽기
launch_backends는 g6, inf2, trn1, g5g, sm 중 아무거나 받아 이름들을 확인한 후, 이들로 백그라운드에서 stage/up.sh를 시작합니다:
launch_backends {"names": ["g6", "inf2"]}
started stage/up.sh g6 inf2 (pid 2362067); output in stage/up.log, read it with launch_log
호출은 즉시 반환됩니다. 스크립트는 AMI를 선택하고, 각 존을 순차적으로 시도한 다음, 다음 리전을 시도하며, 수행한 내용을 stage/up.log에 작성합니다. launch_log는 이 로그를 읽습니다:
strands-demo-g6: launched i-005ebaee08f191e7a us-east-2a
strands-demo-inf2: us-east-2에 용량이 없습니다 (subnet-0b0f0d29473b0a624), 다음 존을 시도합니다
strands-demo-inf2: i-0bf43d9b77b321ec3가 us-east-2b에 실행되었습니다
...
inf2는 첫 번째 Zone에는 용량이 없어 두 번째 Zone에서 시작되었습니다. 30초 후 list_backends를 실행하자, 어느 쪽도 SSM에 등록되지 않았음에도 불구하고 두 인스턴스가 모두 실행 및 청구되는 것이 확인되었습니다:
{"backend": "inf2", "where": "us-east-2 i-0bf43d9b77b321ec3 inf2.xlarge", "state": "running", "ssm": "not registered"},
{"backend": "g6", "where": "us-east-2 i-005ebaee08f191e7a g6.xlarge", "state": "running", "ssm": "not registered"},
...
...
Step 6 — backend_health: Box에 문의하기
네 개의 EC2 백엔드의 경우, backend_health는 태그를 통해 인스턴스를 찾고 SSM Run Command를 통해 하나의 셸 명령을 전송합니다. 이 명령어는 인스턴스의 포트에서 서버의 curl을 실행하여 HTTP 코드, 시간 및 본문을 출력합니다:
cmd = (f"curl -s -m 90 -o /tmp/strands-health.json -w '%{{http_code}} %{{time_total}}' "
f"-H 'content-type: application/json' -d {shlex.quote(body)} "
f"http://localhost:{box['port']}/v1/chat/completions; echo; cat /tmp/strands-health.json")
요청은 인스턴스를 벗어나지 않기 때문에, 이 확인 과정에는 포트 포워딩이나 인바운드 규칙이 필요하지 않습니다. Bedrock과 SageMaker는 직접 호출됩니다. SageMaker는 먼저 InService 상태인지 확인합니다.
발사(launch) 후 매분마다 폴링된 응답들은 각 인스턴스가 올라오는 과정을 추적합니다:
02:13:14Z g6 -> ❌ AWS InvalidInstanceId: Instances not in a valid state for account
02:14:21Z g6 -> ❌ RuntimeError: SSM Failed: 000 0.000140
...
...
각 실패는 다른 단계를 나타냅니다:
| 응답 | 의미 |
|---|---|
InvalidInstanceId | SSM 에이전트가 아직 등록되지 않았습니다. 따라서 박스에서 실행할 수 있는 것이 없습니다. |
| ... |
90초짜리 응답은 inf2에서만 나왔습니다. 이 서버는 컴파일된 모델을 NeuronCores에 로드하는 동안 8080 포트를 열고, 준비될 때까지 요청을 보류합니다.
g6는 발사 호출 후 12.2분이 지나서야, inf2는 20.4분이 지나서야 확인되었으며, 체크 간격은 1분이었습니다. g6의 첫 응답은 1.21초가 걸렸고, 다음 세 번의 응답은 0.02초에서 0.03초가 걸렸습니다.
Step 7 — backend_logs: 셸 없이 서비스 로그 확인
backend_logs는 SSM을 통해 각 백엔드에 대해 적절한 로그 명령을 실행합니다. g6의 vLLM 컨테이너와 inf2 및 trn1의 Neuron 컨테이너에는 docker logs가 사용되며, g5g에서는 journalctl -u vllm.service를 사용합니다. 컨테이너가 존재하지 않을 경우 cloud-init 로그로 폴백(fallback)하여 이미지 풀을 보여줍니다:
g6 i-005ebaee08f191e7a (us-east-2)
Error response from daemon: No such container: vllm
---
serving log not available yet; cloud-init:
...
g6의 첫 번째 상태 확인(health check)이 통과된 2분 후, 동일한 호출은 health check 요청을 포함하여 vLLM 자체 로그를 반환했습니다:
(APIServer pid=1) INFO: Application startup complete.
(EngineCore pid=108) WARNING 10-13 02:24:34 [jit_monitor.py:141] Triton kernel JIT compilation during inference: kernel_unified_attention. This causes a latency spike; consider extending warmup to cover this shape/config.
(APIServer pid=1) INFO: 172.17.0.1:34422 - "POST /v1/chat/completions HTTP/1.1" 200 OK
JIT 경고는 첫 번째 답변의 1.21초와 일치합니다. inf2에서는 로그가 로드 진행 상황을 보여준 후 결과를 반환했습니다:
HTTP listening on :8080 — /health=503 'loading' until ready; queue max=8, gen timeout 120.0s
...
READY in 473.8s — serving on :8080 (SLIM host, peak RSS 14.45 GB)
...
첫 번째 줄은 모델이 준비되기 전에 포트가 열렸음을 보여주는 90초간의 상태 확인 응답을 설명합니다.
Step 8 — ask_backend: 단일 시간 요청
ask_backend는 backends.chat_once를 통해 하나의 채팅 요청을 보내고, 텍스트, 백엔드 자체의 완료 토큰 수, 그리고 서버에서 측정한 시간을 반환합니다. 초당 토큰 수는 이 시간을 기준으로 그 수를 나눈 것이므로 네트워크와 프롬프트 처리가 모두 포함됩니다. 셀프 호스팅(Self-hosted) 백엔드는 SSM 포트 포워딩(stage/forward.sh가 여는)을 통해 접근할 수 있습니다.
포워드되기 전, 이전 세션에서 남겨진 demo.env와 함께:
ask_backend {"backend": "g6", "prompt": "hello"}
❌ URLError: <urlopen error [Errno 111] Connection refused>
stage/forward.sh가 demo.env를 다시 작성한 후, 새로운 서버 프로세스가 시작 시 이를 읽었습니다:
{"backend": "g6", "label": "vLLM on EC2 g6 (L4)", "text": "Hello! How can I help you today? 😊",
"completion_tokens": 11, "seconds": 0.14, "tokens_per_second": 78.6}
{"backend": "inf2", "label": "Inferentia2 (inf2, hand-ported)", "text": "Hello! How can I help you today?",
...
ask_backend {"backend": "trn1", "prompt": "hello"}
❌ trn1은 여기서 구성되지 않았습니다; 구성됨: bedrock, g6, inf2. stage/forward.sh를 실행하세요.
list_backends는 이제 ask_backend_configured: ["bedrock", "g6", "inf2"]을 보고하며, 두 인스턴스는 SSM에서 모두 Online 상태입니다. 이처럼 짧은 응답들은 대부분 프롬프트 처리와 네트워크에 관한 것이므로, 이 속도들은 각각 한 요청에 대한 것을 나타냅니다. 함께 읽으면 좋은 글에는 100 토큰 벤치마크가 있습니다.
단계 9 — count_ids: MCP를 넘어서는 데모 1의 엔진
데모 1은 11개의 ID 중 10 이상인 것이 몇 개인지 질문합니다. 답은 8개입니다. count_ids는 술어(predicate)를 받아 정확한 개수, 최소값 및 최대값을 반환하고 실행된 술어를 에코합니다:
count_ids {"op": ">=", "value": 10} -> {"where": "id >= 10", "count": 8, "min": 10, "max": 23}
count_ids {"op": ">", "value": 10} -> {"where": "id > 10", "count": 7, "min": 11, "max": 23}
count_ids {"op": "=>", "value": 10} -> {"error": "op은 ['<', '<=', '==', '>', '>='] 중 하나여야 합니다."}
두 번째 줄은 질문에서 요구하지 않은 술어에 대해 정확하고 출처가 명시된 7을 반환합니다. where 필드가 응답에 포함되어 있어 숫자가 제시된 것과 함께 술어를 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기