# 서버 프로비저닝 API 아키텍처 > 비동기 Queue 기반 서버 생성 시스템 설계 문서 ## 📋 목차 1. [개요](#개요) 2. [현재 문제점](#현재-문제점) 3. [목표 아키텍처](#목표-아키텍처) 4. [Cloudflare Queue 설명](#cloudflare-queue-설명) 5. [API 스펙](#api-스펙) 6. [Worker 구조](#worker-구조) 7. [구현 단계](#구현-단계) 8. [보안](#보안) 9. [모니터링](#모니터링) 10. [트러블슈팅](#트러블슈팅) --- ## 개요 **현재 상황:** - 서버 프로비저닝이 동기 처리 방식 - Telegram Webhook의 10초 타임아웃 제한 - Linode/Vultr API 응답 시간: 15-30초 - 타임아웃 발생 시 사용자 경험 저하 **목표:** - Cloudflare Queue 기반 비동기 처리 - 즉시 응답 (주문 접수 확인) - 백그라운드 서버 생성 - 완료 시 Telegram 메시지 알림 **기대 효과:** - ✅ Webhook 타임아웃 해결 - ✅ 사용자 경험 개선 (즉시 응답) - ✅ 시스템 안정성 향상 (재시도 메커니즘) - ✅ 확장성 확보 (Queue 기반) --- ## 현재 문제점 ### 동기 처리 방식의 한계 ``` 사용자 → Telegram Webhook → executeServerProvision() ↓ (15-30초) Linode/Vultr API ↓ 응답 타임아웃 ❌ ``` **구체적 문제:** | 문제 | 영향 | 예시 | |------|------|------| | **Webhook 10초 제한** | 응답 실패 | Linode API 15초 → Telegram 타임아웃 | | **사용자 대기 시간** | UX 저하 | "응답 없음" 메시지, 재시도 시도 | | **재시도 없음** | 일시적 장애 시 실패 | API 503 에러 → 주문 실패 | | **동시 처리 불가** | 성능 병목 | 여러 사용자 동시 주문 시 순차 처리 | **현재 코드 위치:** - `src/server-provision.ts:executeServerProvision()` - 동기 실행 - `src/index.ts` (callback_query 핸들러) - Webhook 내에서 직접 호출 --- ## 목표 아키텍처 ### 비동기 Queue 기반 흐름 ``` ┌──────────────────────────────────────────────────────────────┐ │ 사용자 요청 처리 (즉시 응답) │ └──────────────────────────────────────────────────────────────┘ 사용자 클릭 "확인" 버튼 ↓ telegram-bot-workers (Producer) 1. server_orders INSERT (status='pending') 2. Queue.send({ order_id, user_id }) 3. Telegram 즉시 응답: "주문 접수 (#123)" ↓ 사용자에게 즉시 응답 ✅ (1-2초) ┌──────────────────────────────────────────────────────────────┐ │ 백그라운드 서버 생성 (비동기 처리) │ └──────────────────────────────────────────────────────────────┘ Cloudflare Queue ↓ (메시지 전달) server-provision-worker (Consumer) 1. Queue 메시지 수신 2. server_orders 조회 (status='pending') 3. 잔액 재확인 4. Linode/Vultr API 호출 (15-30초) 5. 결제 처리 (Optimistic Locking) 6. server_orders 업데이트 (status='active') 7. user_servers INSERT 8. Telegram 알림: "서버 생성 완료 🎉" ↓ 완료 알림 전송 (백그라운드) ┌──────────────────────────────────────────────────────────────┐ │ 실패 처리 (자동 재시도) │ └──────────────────────────────────────────────────────────────┘ API 실패 (503, timeout 등) ↓ Queue 자동 재시도 (3회, 지수 백오프) ↓ 재시도 실패 시 → Dead Letter Queue ↓ 관리자 알림 (notifyAdmin) ``` --- ## Cloudflare Queue 설명 ### Queue란? **메시지 큐(Message Queue)**: 비동기 작업을 안전하게 전달하고 처리하는 시스템 ``` Producer (생산자) Queue (대기열) Consumer (소비자) ↓ ↓ ↓ 메시지 전송 → 메시지 저장 → 메시지 처리 ``` **Cloudflare Queue 특징:** - 완전 관리형 (서버리스) - 자동 재시도 (exponential backoff) - Dead Letter Queue 지원 - 무료 티어: 100만 요청/월 ### Producer (메시지 전송) **역할:** Telegram 봇이 주문을 Queue에 전송 ```typescript // telegram-bot-workers/src/index.ts (callback_query 핸들러) // 기존: 동기 실행 (타임아웃 발생) const result = await executeServerProvision(env, userId, telegramUserId, orderId); // 변경: Queue에 전송 (즉시 응답) await env.SERVER_PROVISION_QUEUE.send({ order_id: orderId, user_id: userId, telegram_user_id: telegramUserId, timestamp: Date.now(), }); return "📋 주문 접수 완료! 서버 생성 중입니다... (1-2분 소요)"; ``` **메시지 구조:** ```typescript interface ProvisionMessage { order_id: number; // server_orders.id user_id: number; // users.id telegram_user_id: string; // Telegram Chat ID (알림용) timestamp: number; // 전송 시각 (디버깅) } ``` ### Consumer (메시지 처리) **역할:** 백그라운드에서 실제 서버 생성 처리 ```typescript // server-provision-worker/src/consumer.ts export default { async queue(batch: MessageBatch, env: Env): Promise { for (const message of batch.messages) { const { order_id, user_id, telegram_user_id } = message.body; try { // 1. 실제 서버 생성 (기존 로직 재사용) const result = await executeServerProvision( env, user_id, telegram_user_id, order_id ); // 2. 성공 시 사용자 알림 if (result.success) { await sendMessage( env.BOT_TOKEN, Number(telegram_user_id), `🎉 서버 생성 완료!\n\n` + `• IP: ${result.ip_address}\n` + `• Root 비밀번호: ${result.root_password}\n\n` + `⚠️ 비밀번호는 이 메시지에서만 확인 가능합니다.` ); message.ack(); // 성공 확인 } else { throw new Error(result.error || 'Unknown error'); } } catch (error) { logger.error('서버 생성 실패', error as Error, { order_id }); // 3. 실패 시 재시도 (자동) message.retry(); // Queue가 자동으로 재시도 } } }, }; ``` ### 재시도 및 Dead Letter Queue **자동 재시도 설정:** ```toml # wrangler.toml (server-provision-worker) [[queues.consumers]] queue = "server-provision-queue" max_retries = 3 # 최대 3회 재시도 max_batch_size = 10 # 배치 크기 max_batch_timeout = 30 # 배치 타임아웃 (초) max_concurrency = 5 # 동시 처리 수 dead_letter_queue = "provision-dlq" # 실패한 메시지 전송 ``` **재시도 전략 (Exponential Backoff):** ``` 1회 실패 → 2초 후 재시도 2회 실패 → 4초 후 재시도 3회 실패 → 8초 후 재시도 4회 실패 → Dead Letter Queue로 전송 ``` **Dead Letter Queue 처리:** ```typescript // server-provision-worker/src/dlq-handler.ts export default { async queue(batch: MessageBatch, env: Env): Promise { for (const message of batch.messages) { const { order_id, telegram_user_id } = message.body; // 1. DB 업데이트 (status='failed') await env.DB.prepare( "UPDATE server_orders SET status = 'failed', error_message = ? WHERE id = ?" ).bind('최대 재시도 횟수 초과', order_id).run(); // 2. 사용자 알림 await sendMessage( env.BOT_TOKEN, Number(telegram_user_id), `❌ 서버 생성 실패 (주문 #${order_id})\n\n` + `일시적인 문제로 서버를 생성할 수 없습니다.\n` + `관리자에게 문의하세요.` ); // 3. 관리자 알림 await notifyAdmin( 'api_error', { service: 'server-provision', error: '서버 생성 최종 실패', context: `주문 ID: ${order_id}\n사용자: ${telegram_user_id}`, }, { telegram: { sendMessage }, adminId: env.SERVER_ADMIN_ID, env } ); message.ack(); // DLQ에서 제거 } }, }; ``` --- ## API 스펙 ### POST /provision (내부 API) **목적:** Queue 메시지 전송 (Producer) **요청:** ```http POST /provision Content-Type: application/json Authorization: Bearer {WEBHOOK_SECRET} { "order_id": 123, "user_id": 456, "telegram_user_id": "789012345" } ``` **응답 (성공):** ```json { "success": true, "order_id": 123, "message": "주문이 접수되었습니다. 서버 생성 중..." } ``` **응답 (실패):** ```json { "success": false, "error": "주문을 찾을 수 없습니다." } ``` **에러 코드:** | 코드 | 설명 | |------|------| | 400 | 잘못된 요청 (필수 파라미터 누락) | | 401 | 인증 실패 (WEBHOOK_SECRET 불일치) | | 404 | 주문 없음 (order_id 불일치) | | 500 | Queue 전송 실패 | ### GET /status/:order_id (사용자 API) **목적:** 주문 상태 조회 **요청:** ```http GET /status/123 Authorization: Bearer {WEBHOOK_SECRET} ``` **응답:** ```json { "order_id": 123, "status": "provisioning", "spec_id": 456, "region": "tokyo", "price_paid": 15000, "created_at": "2026-01-24T10:30:00Z", "updated_at": "2026-01-24T10:32:15Z", "ip_address": null, "error_message": null } ``` **상태 값:** | 상태 | 설명 | 사용자 메시지 | |------|------|--------------| | `pending` | 주문 접수 | "주문 대기 중" | | `provisioning` | 서버 생성 중 | "서버 생성 중... (1-2분 소요)" | | `active` | 생성 완료 | "서버 운영 중 ✅" | | `failed` | 생성 실패 | "서버 생성 실패 ❌" | | `cancelled` | 사용자 취소 | "주문 취소됨" | | `terminated` | 서버 종료 | "서버 종료됨" | ### Queue 메시지 스키마 **메시지 타입:** ```typescript interface ProvisionMessage { order_id: number; // server_orders.id (필수) user_id: number; // users.id (필수) telegram_user_id: string; // Telegram Chat ID (필수) timestamp: number; // Unix timestamp (선택) retry_count?: number; // 재시도 횟수 (내부 사용) } ``` **메시지 크기 제한:** - 최대 128KB (Cloudflare Queue 제한) - 현재 메시지: ~100 bytes **메시지 보관 기간:** - 기본: 4일 (96시간) - Dead Letter Queue: 14일 --- ## Worker 구조 ### 디렉토리 구조 ``` server-provision/ ├── wrangler.toml # Worker 설정 + Queue 바인딩 └── src/ ├── index.ts # Producer API (POST /provision, GET /status) ├── consumer.ts # Queue Consumer (서버 생성 처리) ├── dlq-handler.ts # Dead Letter Queue 핸들러 ├── server-provision.ts # 기존 로직 (재사용) ├── types.ts # 타입 정의 └── utils/ ├── logger.ts # 로깅 (기존 재사용) ├── optimistic-lock.ts # Optimistic Locking (기존 재사용) └── retry.ts # Retry 로직 (기존 재사용) ``` ### wrangler.toml ```toml name = "server-provision-worker" main = "src/index.ts" compatibility_date = "2024-01-01" # Queue Producer 설정 [[queues.producers]] queue = "server-provision-queue" binding = "SERVER_PROVISION_QUEUE" # Queue Consumer 설정 (이 Worker가 메시지 처리) [[queues.consumers]] queue = "server-provision-queue" max_retries = 3 max_batch_size = 10 max_batch_timeout = 30 max_concurrency = 5 dead_letter_queue = "provision-dlq" # Dead Letter Queue Consumer [[queues.consumers]] queue = "provision-dlq" max_retries = 0 # DLQ는 재시도 없음 # D1 Database 바인딩 (기존 telegram-conversations) [[d1_databases]] binding = "DB" database_name = "telegram-conversations" database_id = "YOUR_DATABASE_ID" # CLOUD_DB 바인딩 [[d1_databases]] binding = "CLOUD_DB" database_name = "cloud-instances-db" database_id = "YOUR_CLOUD_DB_ID" # KV Namespace 바인딩 (Rate Limiting 등) [[kv_namespaces]] binding = "RATE_LIMIT_KV" id = "YOUR_KV_ID" # 환경 변수 [vars] OPENAI_API_BASE = "https://gateway.ai.cloudflare.com/v1/..." SERVER_ADMIN_ID = "123456789" # Secrets (wrangler secret put으로 설정) # - BOT_TOKEN # - WEBHOOK_SECRET # - LINODE_API_KEY # - VULTR_API_KEY # - OPENAI_API_KEY ``` ### src/index.ts (Producer) ```typescript import { sendToQueue } from './queue-producer'; import type { Env, ProvisionMessage } from './types'; export default { // HTTP 요청 처리 (POST /provision, GET /status) async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); // POST /provision - Queue에 메시지 전송 if (request.method === 'POST' && url.pathname === '/provision') { return await handleProvision(request, env); } // GET /status/:order_id - 주문 상태 조회 if (request.method === 'GET' && url.pathname.startsWith('/status/')) { return await handleStatus(request, env); } return new Response('Not Found', { status: 404 }); }, // Queue Consumer (consumer.ts 파일에서 import) async queue(batch: MessageBatch, env: Env): Promise { const { default: consumer } = await import('./consumer'); return consumer.queue(batch, env); }, }; async function handleProvision(request: Request, env: Env): Promise { // 인증 검증 const authHeader = request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return new Response('Unauthorized', { status: 401 }); } const token = authHeader.substring(7); if (token !== env.WEBHOOK_SECRET) { return new Response('Forbidden', { status: 403 }); } // 요청 파싱 const body = await request.json() as { order_id: number; user_id: number; telegram_user_id: string; }; if (!body.order_id || !body.user_id || !body.telegram_user_id) { return new Response('Bad Request: Missing required fields', { status: 400 }); } // Queue에 메시지 전송 try { await env.SERVER_PROVISION_QUEUE.send({ order_id: body.order_id, user_id: body.user_id, telegram_user_id: body.telegram_user_id, timestamp: Date.now(), }); return new Response(JSON.stringify({ success: true, order_id: body.order_id, message: '주문이 접수되었습니다. 서버 생성 중...', }), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { return new Response(JSON.stringify({ success: false, error: 'Queue 전송 실패', }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } } async function handleStatus(request: Request, env: Env): Promise { // 인증 검증 (동일) const authHeader = request.headers.get('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return new Response('Unauthorized', { status: 401 }); } const token = authHeader.substring(7); if (token !== env.WEBHOOK_SECRET) { return new Response('Forbidden', { status: 403 }); } // order_id 추출 const url = new URL(request.url); const orderId = Number(url.pathname.split('/').pop()); if (!orderId) { return new Response('Bad Request: Invalid order_id', { status: 400 }); } // DB 조회 const order = await env.DB.prepare( `SELECT id, status, spec_id, region, price_paid, created_at, updated_at, ip_address, error_message FROM server_orders WHERE id = ?` ).bind(orderId).first(); if (!order) { return new Response('Not Found', { status: 404 }); } return new Response(JSON.stringify(order), { headers: { 'Content-Type': 'application/json' }, }); } ``` ### src/consumer.ts (Consumer) ```typescript import { createLogger } from './utils/logger'; import { executeServerProvision } from './server-provision'; import { sendMessage } from './telegram'; import type { Env, ProvisionMessage } from './types'; const logger = createLogger('provision-consumer'); export default { async queue(batch: MessageBatch, env: Env): Promise { logger.info('Queue 메시지 수신', { count: batch.messages.length }); for (const message of batch.messages) { const { order_id, user_id, telegram_user_id } = message.body; logger.info('서버 생성 시작', { order_id, user_id, telegram_user_id, attempt: message.attempts, }); try { // 1. 실제 서버 생성 (기존 로직 재사용) const result = await executeServerProvision( env, user_id, telegram_user_id, order_id ); if (result.success) { // 2. 성공 시 사용자 알림 await sendMessage( env.BOT_TOKEN, Number(telegram_user_id), `🎉 서버 생성 완료!\n\n` + `• 사양: ${result.plan_label}\n` + `• 리전: ${result.region}\n` + `• IP: ${result.ip_address}\n` + `• Root 비밀번호: ${result.root_password}\n\n` + `⚠️ 비밀번호는 이 메시지에서만 확인 가능합니다.\n` + `분실 시 서버 콘솔에서 재설정하세요.\n\n` + `SSH 접속: ssh root@${result.ip_address}` ); logger.info('서버 생성 성공', { order_id, instance_id: result.instance_id, ip_address: result.ip_address, }); message.ack(); // 성공 확인 } else { // 실패 케이스 (executeServerProvision 내부 에러) throw new Error(result.error || 'Unknown error'); } } catch (error) { logger.error('서버 생성 실패', error as Error, { order_id, attempt: message.attempts, }); // 재시도 (Queue가 자동으로 exponential backoff 적용) message.retry(); } } }, }; ``` ### src/dlq-handler.ts (Dead Letter Queue) ```typescript import { createLogger } from './utils/logger'; import { notifyAdmin } from './services/notification'; import { sendMessage } from './telegram'; import type { Env, ProvisionMessage } from './types'; const logger = createLogger('provision-dlq'); export default { async queue(batch: MessageBatch, env: Env): Promise { logger.warn('DLQ 메시지 수신', { count: batch.messages.length }); for (const message of batch.messages) { const { order_id, user_id, telegram_user_id } = message.body; logger.error('서버 생성 최종 실패', new Error('최대 재시도 횟수 초과'), { order_id, user_id, telegram_user_id, }); // 1. DB 업데이트 (status='failed') await env.DB.prepare( "UPDATE server_orders SET status = 'failed', error_message = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?" ).bind('최대 재시도 횟수 초과 (DLQ)', order_id).run(); // 2. 사용자 알림 await sendMessage( env.BOT_TOKEN, Number(telegram_user_id), `❌ 서버 생성 실패 (주문 #${order_id})\n\n` + `일시적인 문제로 서버를 생성할 수 없습니다.\n` + `관리자에게 문의하세요.` ); // 3. 관리자 알림 await notifyAdmin( 'api_error', { service: 'server-provision', error: '서버 생성 최종 실패 (DLQ)', context: `주문 ID: ${order_id}\n사용자 ID: ${user_id}\nTelegram ID: ${telegram_user_id}`, }, { telegram: { sendMessage: (chatId: number, text: string) => sendMessage(env.BOT_TOKEN, chatId, text), }, adminId: env.SERVER_ADMIN_ID || env.DEPOSIT_ADMIN_ID || '', env, } ); message.ack(); // DLQ에서 제거 } }, }; ``` --- ## 구현 단계 ### Phase 1: Queue 생성 및 기본 설정 **목표:** Cloudflare Queue 인프라 구축 **작업:** 1. Queue 생성 ```bash wrangler queues create server-provision-queue wrangler queues create provision-dlq ``` 2. `wrangler.toml` 설정 추가 ```toml [[queues.producers]] queue = "server-provision-queue" binding = "SERVER_PROVISION_QUEUE" ``` 3. 환경 변수 설정 ```bash wrangler secret put WEBHOOK_SECRET wrangler secret put BOT_TOKEN wrangler secret put LINODE_API_KEY wrangler secret put VULTR_API_KEY ``` **검증:** ```bash # Queue 목록 확인 wrangler queues list # Queue 정보 확인 wrangler queues describe server-provision-queue ``` **예상 소요:** 30분 --- ### Phase 2: Producer 구현 (메시지 전송) **목표:** Telegram 봇에서 Queue로 메시지 전송 **작업:** 1. `src/index.ts` (telegram-bot-workers) 수정 - callback_query 핸들러에서 `executeServerProvision()` 제거 - Queue 메시지 전송 로직 추가 ```typescript // 기존 코드 (타임아웃 발생) const result = await executeServerProvision(env, userId, telegramUserId, orderId); // 새 코드 (즉시 응답) await env.SERVER_PROVISION_QUEUE.send({ order_id: orderId, user_id: userId, telegram_user_id: telegramUserId, timestamp: Date.now(), }); return "📋 주문 접수 완료! 서버 생성 중입니다... (1-2분 소요)"; ``` 2. 타입 정의 추가 (`src/types.ts`) ```typescript export interface ProvisionMessage { order_id: number; user_id: number; telegram_user_id: string; timestamp: number; } ``` **검증:** ```bash # 로컬 테스트 wrangler dev # Queue에 메시지 전송 테스트 curl -X POST http://localhost:8787/provision \ -H "Content-Type: application/json" \ -H "Authorization: Bearer test-secret" \ -d '{"order_id":123,"user_id":456,"telegram_user_id":"789012345"}' # Queue 메시지 확인 wrangler queues consumer server-provision-queue pull ``` **예상 소요:** 1시간 --- ### Phase 3: Consumer 구현 (서버 생성) **목표:** Queue 메시지를 받아 실제 서버 생성 **작업:** 1. 새 Worker 프로젝트 생성 ```bash mkdir server-provision cd server-provision npm init -y npm install wrangler --save-dev ``` 2. `src/consumer.ts` 구현 - Queue 메시지 수신 - `executeServerProvision()` 호출 (기존 로직 재사용) - 성공 시 Telegram 알림 - 실패 시 `message.retry()` 3. `src/server-provision.ts` 복사 (기존 파일 재사용) - `telegram-bot-workers/src/server-provision.ts` → `server-provision/src/` 4. 의존성 추가 ```bash npm install --save \ @cloudflare/workers-types \ # 기타 필요한 패키지 ``` **검증:** ```bash # 로컬 Consumer 실행 wrangler dev # Queue에 테스트 메시지 전송 (Phase 2 사용) # → Consumer가 자동으로 처리하는지 확인 # 로그 확인 wrangler tail ``` **예상 소요:** 2시간 --- ### Phase 4: Dead Letter Queue 처리 **목표:** 재시도 실패 시 최종 처리 **작업:** 1. `src/dlq-handler.ts` 구현 - DLQ 메시지 수신 - `server_orders.status` → `'failed'` 업데이트 - 사용자 알림 - 관리자 알림 (`notifyAdmin`) 2. `wrangler.toml`에 DLQ Consumer 추가 ```toml [[queues.consumers]] queue = "provision-dlq" max_retries = 0 # DLQ는 재시도 없음 ``` **검증:** ```bash # 강제 실패 테스트 (API 키 제거) wrangler secret delete LINODE_API_KEY # Queue 메시지 전송 → 재시도 3회 → DLQ 전송 확인 # 로그 확인 wrangler tail --format pretty ``` **예상 소요:** 1.5시간 --- ### Phase 5: 통합 테스트 및 배포 **목표:** 전체 플로우 검증 및 프로덕션 배포 **작업:** 1. 통합 테스트 - 정상 케이스: 주문 → Queue → 서버 생성 → 알림 - 실패 케이스: API 에러 → 재시도 → DLQ → 알림 - 타임아웃 케이스: Telegram Webhook 10초 내 응답 확인 2. 모니터링 설정 - Cloudflare Dashboard → Queue 메트릭 확인 - `wrangler tail` 로그 확인 - 관리자 알림 동작 확인 3. 프로덕션 배포 ```bash # telegram-bot-workers 배포 (Producer) cd telegram-bot-workers wrangler deploy # server-provision-worker 배포 (Consumer) cd ../server-provision wrangler deploy ``` 4. 문서 업데이트 - `CLAUDE.md` - 새 아키텍처 반영 - `README.md` - 배포 가이드 추가 - `docs/server-provision-architecture.md` - 본 문서 최종 업데이트 **검증 체크리스트:** - [ ] Telegram 봇에서 서버 주문 시 즉시 응답 (1-2초) - [ ] 백그라운드 서버 생성 완료 (1-2분) - [ ] 완료 알림 Telegram 메시지 수신 - [ ] API 실패 시 자동 재시도 확인 (로그) - [ ] DLQ 처리 시 관리자 알림 수신 - [ ] DB `server_orders.status` 올바르게 업데이트 - [ ] Cloudflare Dashboard Queue 메트릭 정상 **예상 소요:** 3시간 --- ## 보안 ### Service Binding (Worker 간 통신) **문제:** 공개 URL로 Worker 호출 시 보안 위험 **해결:** Cloudflare Service Binding 사용 **설정:** ```toml # telegram-bot-workers/wrangler.toml [[services]] binding = "SERVER_PROVISION_API" service = "server-provision-worker" environment = "production" ``` **사용 예시:** ```typescript // telegram-bot-workers/src/index.ts // ❌ 공개 URL (보안 위험) const response = await fetch('https://server-provision.workers.dev/provision', { method: 'POST', headers: { 'Authorization': `Bearer ${env.WEBHOOK_SECRET}` }, body: JSON.stringify({ order_id, user_id, telegram_user_id }), }); // ✅ Service Binding (내부 통신) const response = await env.SERVER_PROVISION_API.fetch('https://internal/provision', { method: 'POST', body: JSON.stringify({ order_id, user_id, telegram_user_id }), }); ``` **장점:** - 외부 네트워크 노출 없음 - 인증 헤더 불필요 (내부 통신) - 속도 향상 (Cloudflare 내부 네트워크) ### API 키 관리 **원칙:** 모든 민감 정보는 Secrets로 관리 **설정:** ```bash # Telegram wrangler secret put BOT_TOKEN wrangler secret put WEBHOOK_SECRET # Cloud Provider wrangler secret put LINODE_API_KEY wrangler secret put VULTR_API_KEY # Admin wrangler secret put SERVER_ADMIN_ID ``` **절대 금지:** - ❌ 코드에 API 키 하드코딩 - ❌ `wrangler.toml`에 평문 저장 - ❌ Git에 `.env` 파일 커밋 ### Rate Limiting **목적:** Queue 메시지 폭주 방지 **구현:** ```typescript // telegram-bot-workers/src/index.ts (callback_query 핸들러) // 사용자별 주문 제한 (10개/시간) const rateLimitKey = `server_order_${userId}`; const orderCount = await env.RATE_LIMIT_KV.get(rateLimitKey); if (orderCount && Number(orderCount) >= 10) { return "⚠️ 시간당 주문 제한 (10개)을 초과했습니다."; } await env.RATE_LIMIT_KV.put( rateLimitKey, String((Number(orderCount) || 0) + 1), { expirationTtl: 3600 } // 1시간 ); // Queue 메시지 전송 await env.SERVER_PROVISION_QUEUE.send({ ... }); ``` --- ## 모니터링 ### Cloudflare Dashboard **Queue 메트릭 확인:** 1. Cloudflare Dashboard 로그인 2. Workers & Pages → Queues 3. `server-provision-queue` 선택 **주요 지표:** | 메트릭 | 설명 | 정상 범위 | |--------|------|----------| | **Messages Sent** | 전송된 메시지 수 | - | | **Messages Received** | 처리된 메시지 수 | Sent와 동일 | | **Messages Retried** | 재시도된 메시지 수 | <5% | | **Messages DLQ** | DLQ로 전송된 메시지 수 | <1% | | **Queue Depth** | 대기 중인 메시지 수 | <100 | | **Consumer Lag** | 처리 지연 시간 | <5분 | **알람 설정 (권장):** - Queue Depth > 500: 백로그 증가 - Messages DLQ > 10: 시스템 장애 가능성 - Consumer Lag > 10분: 성능 문제 ### 로그 확인 **실시간 로그 스트리밍:** ```bash # Producer 로그 cd telegram-bot-workers wrangler tail --format pretty # Consumer 로그 cd server-provision wrangler tail --format pretty ``` **로그 필터링:** ```bash # 에러만 확인 wrangler tail --format pretty | grep ERROR # 특정 order_id 추적 wrangler tail --format pretty | grep "order_id: 123" # JSON 형식 (파싱용) wrangler tail --format json > logs.json ``` ### 성공/실패 지표 **DB 쿼리로 확인:** ```sql -- 상태별 주문 수 SELECT status, COUNT(*) as count FROM server_orders WHERE created_at > datetime('now', '-1 day') GROUP BY status; -- 최근 실패 주문 SELECT id, error_message, created_at FROM server_orders WHERE status = 'failed' ORDER BY created_at DESC LIMIT 10; -- 평균 프로비저닝 시간 SELECT AVG((julianday(provisioned_at) - julianday(created_at)) * 24 * 60) as avg_minutes FROM server_orders WHERE status = 'active' AND provisioned_at IS NOT NULL; ``` **로컬 실행:** ```bash wrangler d1 execute telegram-conversations --command "SELECT ..." ``` **대시보드 구축 (선택):** - Cloudflare Analytics Engine 연동 - Grafana + Prometheus - Custom Admin Panel --- ## 트러블슈팅 ### 일반적인 문제 #### 1. Queue 메시지가 처리되지 않음 **증상:** - Telegram 봇에서 "주문 접수" 메시지 받음 - 서버 생성 완료 알림이 오지 않음 - Queue Depth가 계속 증가 **원인:** - Consumer Worker가 배포되지 않음 - Consumer Worker가 에러로 중단됨 - D1 Database 바인딩 누락 **해결:** ```bash # Consumer Worker 배포 확인 wrangler deployments list --name server-provision-worker # Consumer 로그 확인 wrangler tail --name server-provision-worker # Queue Consumer 설정 확인 wrangler queues list wrangler queues describe server-provision-queue ``` --- #### 2. 재시도가 즉시 발생함 (Exponential Backoff 안됨) **증상:** - 로그에 "재시도" 메시지가 1초 간격으로 연속 출력 - Queue 메트릭에서 Messages Retried 급증 **원인:** - `message.retry()` 대신 `message.ack()` + 재전송 사용 - Cloudflare Queue의 자동 재시도 메커니즘 무시 **해결:** ```typescript // ❌ 잘못된 패턴 try { await provision(); } catch (error) { // 즉시 재전송 (Exponential Backoff 없음) await env.SERVER_PROVISION_QUEUE.send(message.body); message.ack(); } // ✅ 올바른 패턴 try { await provision(); message.ack(); // 성공 시 } catch (error) { message.retry(); // 실패 시 (Queue가 자동으로 Exponential Backoff 적용) } ``` --- #### 3. DLQ 메시지가 무한 누적 **증상:** - DLQ에 메시지가 쌓이지만 처리되지 않음 - `server_orders.status`가 여전히 `provisioning` **원인:** - DLQ Consumer가 배포되지 않음 - DLQ 핸들러 로직 누락 **해결:** ```bash # DLQ Consumer 설정 확인 wrangler queues describe provision-dlq # wrangler.toml에 DLQ Consumer 추가 확인 [[queues.consumers]] queue = "provision-dlq" max_retries = 0 # DLQ 메시지 수동 처리 (임시) wrangler queues consumer provision-dlq pull ``` --- #### 4. Optimistic Locking 에러 (잔액 차감 실패) **증상:** - 서버는 생성되었지만 잔액이 차감되지 않음 - `server_orders.status` = `failed` - 로그: "Version mismatch on balance update" **원인:** - 동시에 여러 출금 요청 발생 - Optimistic Locking 재시도 실패 **해결:** ```typescript // utils/optimistic-lock.ts에서 재시도 횟수 증가 export async function executeWithOptimisticLock( db: D1Database, operation: (attempt: number) => Promise, maxRetries = 5 // 기본 3회 → 5회로 증가 ): Promise { // ... } ``` **관리자 대응:** 1. 서버 생성 완료 확인 (Linode/Vultr 대시보드) 2. DB에서 수동 잔액 차감 ```sql -- user_deposits.balance 수동 차감 UPDATE user_deposits SET balance = balance - 15000 WHERE user_id = 123; -- deposit_transactions 기록 INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at) VALUES (123, 'withdrawal', 15000, 'confirmed', '수동 처리: 서버 #456', CURRENT_TIMESTAMP); -- server_orders 상태 업데이트 UPDATE server_orders SET status = 'active' WHERE id = 456; ``` --- #### 5. Telegram 알림이 전송되지 않음 **증상:** - 서버 생성 완료되었지만 Telegram 메시지 없음 - DB `server_orders.status` = `active` **원인:** - `BOT_TOKEN` 만료/잘못됨 - `telegram_user_id` 형식 오류 (문자열 vs 숫자) - Telegram API Rate Limit **해결:** ```bash # BOT_TOKEN 재설정 wrangler secret put BOT_TOKEN # Consumer 로그 확인 wrangler tail --name server-provision-worker | grep "Telegram" # 수동 알림 전송 (임시) curl -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \ -d "chat_id=123456789" \ -d "text=서버 생성 완료 (#456)" ``` --- ### 로그 명령어 **Producer (telegram-bot-workers):** ```bash # 실시간 로그 wrangler tail --name telegram-summary-bot --format pretty # JSON 로그 (파싱용) wrangler tail --name telegram-summary-bot --format json # 특정 사용자 추적 wrangler tail --name telegram-summary-bot | grep "telegram_user_id: 123456789" ``` **Consumer (server-provision-worker):** ```bash # 실시간 로그 wrangler tail --name server-provision-worker --format pretty # 에러만 확인 wrangler tail --name server-provision-worker | grep -E "ERROR|failed" # 특정 order_id 추적 wrangler tail --name server-provision-worker | grep "order_id: 123" ``` **Queue 상태:** ```bash # Queue 메트릭 wrangler queues describe server-provision-queue # DLQ 메트릭 wrangler queues describe provision-dlq # 수동으로 메시지 가져오기 (디버깅) wrangler queues consumer server-provision-queue pull --batch-size 1 ``` --- ## 참고 자료 **Cloudflare 공식 문서:** - [Queues Documentation](https://developers.cloudflare.com/queues/) - [Queue Consumer](https://developers.cloudflare.com/queues/configuration/consumer-concurrency/) - [Dead Letter Queues](https://developers.cloudflare.com/queues/configuration/dead-letter-queues/) - [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) **프로젝트 내부 문서:** - `CLAUDE.md` - 개발자 가이드 - `README.md` - 사용자 가이드 - `docs/ARCHITECTURE.md` - 시스템 아키텍처 **관련 코드:** - `src/server-provision.ts` - 기존 프로비저닝 로직 - `src/services/linode-api.ts` - Linode API 클라이언트 - `src/services/vultr-api.ts` - Vultr API 클라이언트 - `utils/optimistic-lock.ts` - Optimistic Locking 유틸리티 - `utils/retry.ts` - Retry 로직 --- **문서 작성일:** 2026-01-24 **작성자:** Claude Code (Coder Agent) **버전:** 1.0 **최종 업데이트:** 2026-01-24