본문으로 건너뛰기

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 Pod]
↓
[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 KakaoCloud" --bare --python 3.10
uv add fastmcp==2.13.1 --no-sync

실행 후 pyproject.toml과 uv.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 옵션이 필수입니다. KakaoCloud의 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_NAME🖌︎Container Registry를 생성한 프로젝트 이름
ACCESS_KEY_ID🖌︎IAM 액세스 키 ID
ACCESS_SECRET_KEY🖌︎IAM 보안 액세스 키

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

이미지 태깅​

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

docker tag \
weather-mcp-server:latest \
${CR_URL}/${CR_REPO_NAME}/weather-mcp-server:latest
환경변수설명
CR_URL🖌︎Container Registry URL
CR_REPO_NAME🖌︎생성한 리포지토리 이름

이미지 푸시​

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

docker push ${CR_URL}/${CR_REPO_NAME}/weather-mcp-server:latest
환경변수설명
CR_URL🖌︎Container Registry URL
CR_REPO_NAME🖌︎생성한 리포지토리 이름

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

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

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

Terraform 파일 준비​

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

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

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 생성 템플릿
정보

Container Registry에서 이미지를 가져올 수 있도록 Kubernetes 노드 풀은 외부 통신이 가능한 서브넷에 생성해야 합니다. get_vpc.tf 파일에서 route_table_name 필터 값을 main으로 설정하여 퍼블릭 서브넷을 조회하는지 확인합니다.

data "kakaocloud_subnets" "subnet_kr_central_2_a" {
filter = [
{
name = "availability_zone"
value = "kr-central-2-a"
},
{
name = "vpc_id"
value = data.kakaocloud_vpcs.vpc_info.vpcs[0].id
},
{
name = "provisioning_status"
value = "ACTIVE"
},
{
name = "route_table_name"
value = "main"
}
]
}

data "kakaocloud_subnets" "subnet_kr_central_2_b" {
filter = [
{
name = "availability_zone"
value = "kr-central-2-b"
},
{
name = "vpc_id"
value = data.kakaocloud_vpcs.vpc_info.vpcs[0].id
},
{
name = "provisioning_status"
value = "ACTIVE"
},
{
name = "route_table_name"
value = "main"
}
]
}

변수 설정​

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

vpc_name         = "${VPC_NAME}"
ke_cluster_name = "mcp-test" # 생성할 클러스터 이름
node_flavor = "m2a.large" # 생성할 노드 인스턴스 타입
ke_nodepool_name = "node-pool-1" # 생성할 노드 풀 이름
ssh_key_name = "${SSH_KEY_NAME}"
환경변수설명
VPC_NAME🖌︎사전 준비에서 생성한 VPC 이름
SSH_KEY_NAME🖌︎사전 준비에서 생성한 SSH 키 이름

Terraform 초기화​

terraform init

실행 계획 확인​

terraform plan \
-var="access_key_id=${ACCESS_KEY_ID}" \
-var="access_key_secret=${ACCESS_SECRET_KEY}"
환경변수설명
ACCESS_KEY_ID🖌︎IAM 액세스 키 ID
ACCESS_SECRET_KEY🖌︎IAM 보안 액세스 키

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

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

인프라 생성​

terraform apply \
-var="access_key_id=${ACCESS_KEY_ID}" \
-var="access_key_secret=${ACCESS_SECRET_KEY}"
환경변수설명
ACCESS_KEY_ID🖌︎IAM 액세스 키 ID
ACCESS_SECRET_KEY🖌︎IAM 보안 액세스 키

확인 메시지가 나타나면 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}"
환경변수설명
ACCESS_KEY_ID🖌︎IAM 액세스 키 ID
ACCESS_SECRET_KEY🖌︎IAM 보안 액세스 키

실행 후 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}
환경변수설명
PROJECT_NAME🖌︎Container Registry 프로젝트 이름
ACCESS_KEY_ID🖌︎IAM 액세스 키 ID
ACCESS_SECRET_KEY🖌︎IAM 보안 액세스 키

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

Deployment 생성​

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

Deployment 생성
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-mcp-server
labels:
app: weather-mcp-server
spec:
replicas: 1
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"
EOF
환경변수설명
PROJECT_NAME🖌︎Container Registry 프로젝트 이름
CR_REPO_NAME🖌︎Container 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 테스트​

정보

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

기본 연결 테스트 (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_IP🖌︎Step 6에서 할당한 로드 밸런서 퍼블릭 IP

도구 목록 확인 (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": {}
}'
환경변수설명
LOAD_BALANCER_PUBLIC_IP🖌︎Step 6에서 할당한 로드 밸런서 퍼블릭 IP

get_alerts와 get_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"
}
}
}'
환경변수설명
LOAD_BALANCER_PUBLIC_IP🖌︎Step 6에서 할당한 로드 밸런서 퍼블릭 IP

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

MCP 클라이언트 연결​

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

Claude Desktop 설정 예시:

{
"mcpServers": {
"weather-mcp": {
"url": "http://${LOAD_BALANCER_PUBLIC_IP}/mcp"
}
}
}
환경변수설명
LOAD_BALANCER_PUBLIC_IP🖌︎Step 6에서 할당한 로드 밸런서 퍼블릭 IP