본문으로 건너뛰기

Kubernetes Engine에 MCP Server 배포하기

Model Context Protocol(MCP) Server를 카카오클라우드 Kubernetes Engine에 배포하여 AI 애플리케이션과 연동할 수 있습니다.

기본 정보

시나리오 소개

이 튜토리얼에서는 Model Context Protocol(MCP) Server를 카카오클라우드 Kubernetes Engine에 배포하고, 로컬 MCP 클라이언트(예: Claude Desktop)에서 원격 서버에 접속하여 테스트하는 방법을 안내합니다.

MCP는 AI 애플리케이션이 다양한 데이터 소스 및 도구와 통신할 수 있도록 하는 개방형 프로토콜입니다. 이 튜토리얼에서는 미국 국립기상청(NWS) API를 활용한 날씨 조회 서버를 예제로 사용합니다.

MCP Transport 방식

MCP는 다양한 transport 방식을 지원하지만, 이 튜토리얼에서는 Streamable HTTP (SSE) 만을 사용합니다. Streamable HTTP는 Server-Sent Events를 활용하여 클라이언트-서버 간 양방향 통신을 가능하게 하며, 클라우드 환경의 HTTP 기반 Load Balancer와 호환성이 높습니다.

정보

stdio 방식은 로컬 프로세스 간 통신에 적합하지만, 원격 Kubernetes 환경에서는 사용할 수 없습니다.

주요 단계

  1. MCP Server 애플리케이션 준비 및 컨테이너화
  2. Container Registry에 이미지 업로드
  3. Kubernetes 클러스터 생성 및 MCP Server 배포
  4. 외부 접근 설정 및 테스트

아키텍처 개요

[MCP Client (Claude Desktop)]
↓ HTTPS
[Load Balancer (Public IP)]

[Kubernetes Service]

[MCP Server Pods (2 replicas)]

[NWS Weather API]

이 시나리오는 MCP 기반의 AI 도구 서버를 클라우드 환경에 배포하고 운영하려는 개발자를 위한 실전 가이드입니다.

시작하기 전에

1. 네트워크 환경 구축

카카오클라우드에서 Kubernetes 클러스터를 생성하기 위해서는 VPC 네트워크가 필요합니다. 다중 가용 영역에서 NAT 인스턴스를 이용한 네트워크 구축 문서를 참조하여 네트워크 환경을 구축하세요.

구축이 완료되면 다음 정보를 확인합니다:

  • VPC 이름 (예: main)
  • Subnet 정보 (kr-central-2-a, kr-central-2-b)

2. Container Registry 준비

컨테이너 이미지를 저장할 Container Registry를 생성합니다. 리포지토리 생성 및 관리 문서를 참조하여 레지스트리를 설정하세요.

생성 후 다음 정보를 확인합니다:

  • 프로젝트 이름 (예: my-project)
  • 리포지토리 이름 (예: mcp-repository)
  • Registry URL: {PROJECT_NAME}.kr-central-2.kcr.dev

3. SSH 키 페어 생성

Kubernetes 노드에 접근하기 위한 SSH 키 페어를 생성합니다. 키 페어 생성 및 관리 문서를 참조하여 키 페어를 생성하세요.

생성한 키 페어 이름을 확인합니다 (예: mcp-test-key).

시작하기

Step 1. MCP Server 애플리케이션 준비

미국 국립기상청(NWS) API를 활용한 날씨 조회 MCP Server를 준비합니다. 이 서버는 두 가지 주요 기능을 제공합니다:

  • get_alerts: 미국 주별 기상 경보 조회
  • get_forecast: 위도/경도 기반 날씨 예보 조회

프로젝트 디렉토리 생성

mkdir weather-mcp-server
cd weather-mcp-server

예제 코드 다운로드

아래 내용을 server.py 파일로 저장합니다.

import asyncio
import logging
import os
from typing import Any
import httpx
from fastmcp import FastMCP

logger = logging.getLogger(__name__)
logging.basicConfig(format="[%(levelname)s]: %(message)s", level=logging.INFO)

# Initialize FastMCP server
mcp = FastMCP("weather MCP Server on Kakaocloud")

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"


async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None


def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get("event", "Unknown")}
Area: {props.get("areaDesc", "Unknown")}
Severity: {props.get("severity", "Unknown")}
Description: {props.get("description", "No description available")}
Instructions: {props.get("instruction", "No specific instructions provided")}
"""


@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.

Args:
state: Two-letter US state code (e.g. CA, NY)
"""
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)

if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."

if not data["features"]:
return "No active alerts for this state."

alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get weather forecast for a location.

Args:
latitude: Latitude of the location
longitude: Longitude of the location
"""
# First get the forecast grid endpoint
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)

if not points_data:
return "Unable to fetch forecast data for this location."

# Get the forecast URL from the points response
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)

if not forecast_data:
return "Unable to fetch detailed forecast."

# Format the periods into a readable forecast
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]: # Only show next 5 periods
forecast = f"""
{period["name"]}:
Temperature: {period["temperature"]}°{period["temperatureUnit"]}
Wind: {period["windSpeed"]} {period["windDirection"]}
Forecast: {period["detailedForecast"]}
"""
forecasts.append(forecast)

return "\n---\n".join(forecasts)


def main():
try:
port = int(os.getenv("PORT", "8000"))
logger.info(f"Starting MCP server on port {port}")

# Run the server
asyncio.run(
mcp.run_http_async(
transport="streamable-http",
host="0.0.0.0",
port=port,
)
)
except Exception as e:
logger.error(f"Failed to start MCP server: {e}", exc_info=True)
raise


if __name__ == "__main__":
main()

Python 환경 구성

uv를 사용하여 프로젝트 환경을 초기화합니다.

uv init --name "weather-mcp-server" --description "Weather MCP server for Kakao Cloud" --bare --python 3.10
uv add fastmcp==2.13.1 --no-sync

실행 후 pyproject.tomluv.lock 파일이 생성됩니다.

로컬 테스트 (선택 사항)

uv run server.py

다른 터미널에서 초기화 요청을 보냅니다:

curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "curl-test",
"version": "1.0"
}
}
}'

정상 응답이 오면 Ctrl+C로 종료합니다.

Step 2. 컨테이너 이미지 빌드

Dockerfile 작성

프로젝트 루트에 다음 내용으로 Dockerfile을 생성합니다.

FROM python:3.10-slim

# Install uv
RUN pip install uv

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 파일 및 소스 코드 복사
COPY uv.lock pyproject.toml server.py /app/

# 의존성 설치
RUN uv sync

# 포트 노출
EXPOSE 8000

# 애플리케이션 실행
CMD ["uv", "run", "server.py"]

Docker 이미지 빌드

AMD64 아키텍처로 이미지를 빌드합니다.

docker build --platform linux/amd64 -t weather-mcp-server:latest .
정보

macOS Apple Silicon(M1/M2) 사용자는 --platform linux/amd64 옵션이 필수입니다. Kakao Cloud의 Kubernetes 노드는 AMD64 아키텍처를 사용합니다.

로컬 테스트 (선택 사항)

빌드한 이미지를 로컬에서 실행하여 확인합니다.

docker run --rm -p 8000:8000 weather-mcp-server:latest

다른 터미널에서 초기화 요청을 보냅니다:

curl -X POST http://127.0.0.1:8000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "curl-test",
"version": "1.0"
}
}
}'

정상 응답이 오면 Ctrl+C로 종료합니다.

Step 3. Container Registry에 이미지 푸시

빌드한 Docker 이미지를 카카오클라우드 Container Registry에 업로드합니다.

Container Registry 인증

CR_URL={PROJECT_NAME}.kr-central-2.kcr.dev
docker login $CR_URL \
--username {ACCESS_KEY_ID} \
--password {ACCESS_SECRET_KEY}
파라미터설명예시
PROJECT_NAMEContainer Registry를 생성한 프로젝트 이름my-project
ACCESS_KEY_IDIAM 액세스 키 IDAKIA...
ACCESS_SECRET_KEYIAM 보안 액세스 키wJal...

로그인 성공 시 Login Succeeded 메시지가 표시됩니다.

이미지 태깅

Container Registry에 푸시하기 위해 이미지에 태그를 지정합니다.

docker tag \
weather-mcp-server:latest \
{CR_URL}/{CR_REPO_NAME}/weather-mcp-server:latest
파라미터설명예시
CR_URLContainer Registry URLmy-project.kr-central-2.kcr.dev
CR_REPO_NAME생성한 리포지토리 이름tutorial

이미지 푸시

태그된 이미지를 Container Registry에 업로드합니다.

docker push {CR_URL}/{CR_REPO_NAME}/weather-mcp-server:latest

푸시가 완료되면 카카오클라우드 콘솔의 Container Pack > Container Registry에서 이미지를 확인할 수 있습니다.

Step 4. Terraform으로 Kubernetes 클러스터 생성

Terraform을 사용하여 Kubernetes Engine 클러스터와 노드 풀을 생성합니다.

Terraform 파일 준비

작업 디렉토리를 생성하고 제공된 Terraform 파일을 다운로드합니다.

cd ..  # weather-mcp-server 디렉토리에서 나옴
mkdir kc-terraform
cd kc-terraform

다음 파일들을 다운로드하여 kc-terraform 디렉토리에 저장합니다:

  • main.tf: Terraform 프로바이더 설정
  • variables.tf: 변수 정의
  • terraform.tfvars: 변수 값 설정
  • get_vpc.tf: VPC 정보 조회
  • create_ke.tf: Kubernetes 클러스터 생성
  • create_np.tf: 노드 풀 생성
  • get_kubeconfig.tf.origin: kubeconfig 생성 템플릿

변수 설정

terraform.tfvars 파일을 편집하여 환경에 맞게 값을 수정합니다.

vpc_name         = {VPC_NAME}      # VPC 이름
ke_cluster_name = "mcp-test" # 생성할 클러스터 이름
node_flavor = "m2a.large" # 생성할 노드 인스턴스 타입
ke_nodepool_name = "node-pool-1" # 생성할 노드 풀 이름
ssh_key_name = {SSH_KEY_NAME} # SSH 키 이름
파라미터설명예시
VPC_NAME사전 준비에서 생성한 VPC 이름main
SSH_KEY_NAME사전 준비에서 생성한 SSH 키 이름mcp-test-key

Terraform 초기화

terraform init

실행 계획 확인

terraform plan \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"

생성될 리소스 목록이 표시됩니다:

  • VPC 및 Subnet 조회 (기존 리소스)
  • Kubernetes 클러스터 1개
  • 노드 풀 1개 (2개 노드)

인프라 생성

terraform apply \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"

확인 메시지가 나타나면 yes를 입력합니다.

정보

클러스터 및 노드 생성에는 시간이 소요될 수 있습니다.

리소스 생성 성공 시 Apply complete! 메시지가 표시됩니다.

kubeconfig 생성

클러스터 접근을 위한 kubeconfig 파일을 생성합니다.

cp get_kubeconfig.tf.origin get_kubeconfig.tf
terraform plan \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"
terraform apply \
-var="access_key_id={ACCESS_KEY_ID}" \
-var="access_key_secret={ACCESS_SECRET_KEY}"

실행 후 kubeconfig-mcp-test.yaml 파일이 생성됩니다.

클러스터 접근 확인

생성된 kubeconfig를 사용하여 클러스터에 접근합니다.

export KUBECONFIG=$PWD/kubeconfig-mcp-test.yaml
kubectl get nodes

예상 출력:

NAME             STATUS   ROLES    AGE   VERSION
host-10-x-x-x Ready <none> 5m v1.31.x
host-10-y-y-y Ready <none> 5m v1.31.x

Step 5. Kubernetes에 MCP Server 배포

Container Registry 인증 Secret 생성

Kubernetes가 Container Registry에서 이미지를 가져올 수 있도록 인증 정보를 저장합니다.

CR_URL={PROJECT_NAME}.kr-central-2.kcr.dev
kubectl create secret docker-registry kc-cr-secret \
--docker-server=${CR_URL} \
--docker-username={ACCESS_KEY_ID} \
--docker-password={ACCESS_SECRET_KEY}

리소스 생성 성공 시 secret/kc-cr-secret created 메시지가 표시됩니다.

Deployment 생성

MCP Server를 배포하는 Deployment를 생성합니다.

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-mcp-server
labels:
app: weather-mcp-server
spec:
replicas: 2
selector:
matchLabels:
app: weather-mcp-server
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: weather-mcp-server
spec:
imagePullSecrets:
- name: kc-cr-secret
containers:
- name: mcp-server
image: {PROJECT_NAME}.kr-central-2.kcr.dev/{CR_REPO_NAME}/weather-mcp-server:latest
ports:
- name: http
containerPort: 8000
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
EOF
파라미터설명
PROJECT_NAMEContainer Registry 프로젝트 이름
CR_REPO_NAMEContainer Registry 리포지토리 이름

배포 상태 확인

Pod 생성 상태를 확인합니다.

kubectl get pods -l app=weather-mcp-server -w

모든 Pod가 Running 상태가 될 때까지 대기합니다.

Service 생성

외부에서 MCP Server에 접근할 수 있도록 LoadBalancer 타입의 Service를 생성합니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: weather-mcp-service
labels:
app: weather-mcp-server
spec:
type: LoadBalancer
selector:
app: weather-mcp-server
ports:
- protocol: TCP
port: 80
targetPort: 8000
name: http
EOF

Service 생성 확인:

kubectl get service weather-mcp-service -w

초기에는 EXTERNAL-IP<pending> 상태로 표시됩니다. EXTERNAL-IP가 할당될 때까지 대기합니다.

Step 6. 외부 접근 설정

퍼블릭 IP 연결

LoadBalancer Service가 생성되면 카카오클라우드 콘솔에서 자동으로 로드 밸런서가 생성됩니다.

  1. 카카오클라우드 콘솔에서 Load Balancing으로 이동합니다.
  2. 생성된 로드 밸런서 목록에서 이름이 weather-mcp-service에 해당하는 항목을 찾습니다.
  3. 로드 밸런서 우측의 [⋮] 메뉴 > 퍼블릭 IP 연결을 클릭합니다.
  4. 사용 가능한 퍼블릭 IP를 선택하거나 새로 생성하여 할당합니다.

할당된 퍼블릭 IP를 확인합니다.

Step 7. MCP Server 테스트

기본 연결 테스트 (curl 사용)

MCP 프로토콜을 사용하여 서버 초기화를 테스트하고, 응답 헤더의 mcp-session-id를 저장합니다.

MCP_SESSION_ID=$(
curl -s -D - -o /dev/null \
-X POST http://{LOAD_BALANCER_PUBLIC_IP}/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {
"name": "curl-test",
"version": "1.0"
}
}
}' \
| awk 'BEGIN{IGNORECASE=1}
/^mcp-session-id:/ {
sub(/^[^:]*:[[:space:]]*/, "", $0);
sub(/\r$/, "", $0);
print; exit
}'
)

echo $MCP_SESSION_ID
파라미터설명예시
LOAD_BALANCER_PUBLIC_IPStep 6에서 할당한 로드 밸런서 퍼블릭 IP123.456.789.10

도구 목록 확인 (curl 사용)

curl -X POST http://{LOAD_BALANCER_PUBLIC_IP}/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
-d '{
"jsonrpc": "2.0",
"id": "list-1",
"method": "tools/list",
"params": {}
}'

get_alertsget_forecast 도구가 표시되어야 합니다.

기능 테스트 (curl 사용)

MCP Server의 도구를 직접 호출하여 테스트합니다.

캘리포니아 주 기상 경보 조회:

curl -X POST http://{LOAD_BALANCER_PUBLIC_IP}/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
-d '{
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/call",
"params": {
"name": "get_alerts",
"arguments": {
"state": "CA"
}
}
}'

MCP Server가 정상 작동하면 실시간 날씨 정보가 반환됩니다.

MCP 클라이언트 연결

배포된 MCP Server는 다음과 같이 MCP 클라이언트에 연결할 수 있습니다.

Claude Desktop 설정 예시:

{
"mcpServers": {
"weather-mcp": {
"url": "http://{LOAD_BALANCER_PUBLIC_IP}/mcp"
}
}
}
정보

MCP 클라이언트마다 HTTP transport 지원 수준이 다를 수 있습니다. 안정적인 연동 테스트는 아래의 curl 명령어를 사용하세요.