- Queue-based async architecture design - API spec (POST /provision, GET /status) - Implementation phases (5 steps, ~8 hours) - Troubleshooting guide Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
35 KiB
서버 프로비저닝 API 아키텍처
비동기 Queue 기반 서버 생성 시스템 설계 문서
📋 목차
개요
현재 상황:
- 서버 프로비저닝이 동기 처리 방식
- 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에 전송
// 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분 소요)";
메시지 구조:
interface ProvisionMessage {
order_id: number; // server_orders.id
user_id: number; // users.id
telegram_user_id: string; // Telegram Chat ID (알림용)
timestamp: number; // 전송 시각 (디버깅)
}
Consumer (메시지 처리)
역할: 백그라운드에서 실제 서버 생성 처리
// server-provision-worker/src/consumer.ts
export default {
async queue(batch: MessageBatch<ProvisionMessage>, env: Env): Promise<void> {
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
자동 재시도 설정:
# 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 처리:
// server-provision-worker/src/dlq-handler.ts
export default {
async queue(batch: MessageBatch<ProvisionMessage>, env: Env): Promise<void> {
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)
요청:
POST /provision
Content-Type: application/json
Authorization: Bearer {WEBHOOK_SECRET}
{
"order_id": 123,
"user_id": 456,
"telegram_user_id": "789012345"
}
응답 (성공):
{
"success": true,
"order_id": 123,
"message": "주문이 접수되었습니다. 서버 생성 중..."
}
응답 (실패):
{
"success": false,
"error": "주문을 찾을 수 없습니다."
}
에러 코드:
| 코드 | 설명 |
|---|---|
| 400 | 잘못된 요청 (필수 파라미터 누락) |
| 401 | 인증 실패 (WEBHOOK_SECRET 불일치) |
| 404 | 주문 없음 (order_id 불일치) |
| 500 | Queue 전송 실패 |
GET /status/:order_id (사용자 API)
목적: 주문 상태 조회
요청:
GET /status/123
Authorization: Bearer {WEBHOOK_SECRET}
응답:
{
"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 메시지 스키마
메시지 타입:
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
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)
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<Response> {
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<ProvisionMessage>, env: Env): Promise<void> {
const { default: consumer } = await import('./consumer');
return consumer.queue(batch, env);
},
};
async function handleProvision(request: Request, env: Env): Promise<Response> {
// 인증 검증
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<Response> {
// 인증 검증 (동일)
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)
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<ProvisionMessage>, env: Env): Promise<void> {
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),
`🎉 <b>서버 생성 완료!</b>\n\n` +
`• 사양: ${result.plan_label}\n` +
`• 리전: ${result.region}\n` +
`• IP: <code>${result.ip_address}</code>\n` +
`• Root 비밀번호: <code>${result.root_password}</code>\n\n` +
`⚠️ <b>비밀번호는 이 메시지에서만 확인 가능합니다.</b>\n` +
`분실 시 서버 콘솔에서 재설정하세요.\n\n` +
`SSH 접속: <code>ssh root@${result.ip_address}</code>`
);
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)
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<ProvisionMessage>, env: Env): Promise<void> {
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),
`❌ <b>서버 생성 실패</b> (주문 #${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 인프라 구축
작업:
-
Queue 생성
wrangler queues create server-provision-queue wrangler queues create provision-dlq -
wrangler.toml설정 추가[[queues.producers]] queue = "server-provision-queue" binding = "SERVER_PROVISION_QUEUE" -
환경 변수 설정
wrangler secret put WEBHOOK_SECRET wrangler secret put BOT_TOKEN wrangler secret put LINODE_API_KEY wrangler secret put VULTR_API_KEY
검증:
# Queue 목록 확인
wrangler queues list
# Queue 정보 확인
wrangler queues describe server-provision-queue
예상 소요: 30분
Phase 2: Producer 구현 (메시지 전송)
목표: Telegram 봇에서 Queue로 메시지 전송
작업:
-
src/index.ts(telegram-bot-workers) 수정- callback_query 핸들러에서
executeServerProvision()제거 - Queue 메시지 전송 로직 추가
// 기존 코드 (타임아웃 발생) 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분 소요)"; - callback_query 핸들러에서
-
타입 정의 추가 (
src/types.ts)export interface ProvisionMessage { order_id: number; user_id: number; telegram_user_id: string; timestamp: number; }
검증:
# 로컬 테스트
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 메시지를 받아 실제 서버 생성
작업:
-
새 Worker 프로젝트 생성
mkdir server-provision cd server-provision npm init -y npm install wrangler --save-dev -
src/consumer.ts구현- Queue 메시지 수신
executeServerProvision()호출 (기존 로직 재사용)- 성공 시 Telegram 알림
- 실패 시
message.retry()
-
src/server-provision.ts복사 (기존 파일 재사용)telegram-bot-workers/src/server-provision.ts→server-provision/src/
-
의존성 추가
npm install --save \ @cloudflare/workers-types \ # 기타 필요한 패키지
검증:
# 로컬 Consumer 실행
wrangler dev
# Queue에 테스트 메시지 전송 (Phase 2 사용)
# → Consumer가 자동으로 처리하는지 확인
# 로그 확인
wrangler tail
예상 소요: 2시간
Phase 4: Dead Letter Queue 처리
목표: 재시도 실패 시 최종 처리
작업:
-
src/dlq-handler.ts구현- DLQ 메시지 수신
server_orders.status→'failed'업데이트- 사용자 알림
- 관리자 알림 (
notifyAdmin)
-
wrangler.toml에 DLQ Consumer 추가[[queues.consumers]] queue = "provision-dlq" max_retries = 0 # DLQ는 재시도 없음
검증:
# 강제 실패 테스트 (API 키 제거)
wrangler secret delete LINODE_API_KEY
# Queue 메시지 전송 → 재시도 3회 → DLQ 전송 확인
# 로그 확인
wrangler tail --format pretty
예상 소요: 1.5시간
Phase 5: 통합 테스트 및 배포
목표: 전체 플로우 검증 및 프로덕션 배포
작업:
-
통합 테스트
- 정상 케이스: 주문 → Queue → 서버 생성 → 알림
- 실패 케이스: API 에러 → 재시도 → DLQ → 알림
- 타임아웃 케이스: Telegram Webhook 10초 내 응답 확인
-
모니터링 설정
- Cloudflare Dashboard → Queue 메트릭 확인
wrangler tail로그 확인- 관리자 알림 동작 확인
-
프로덕션 배포
# telegram-bot-workers 배포 (Producer) cd telegram-bot-workers wrangler deploy # server-provision-worker 배포 (Consumer) cd ../server-provision wrangler deploy -
문서 업데이트
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 사용
설정:
# telegram-bot-workers/wrangler.toml
[[services]]
binding = "SERVER_PROVISION_API"
service = "server-provision-worker"
environment = "production"
사용 예시:
// 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로 관리
설정:
# 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 메시지 폭주 방지
구현:
// 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 메트릭 확인:
- Cloudflare Dashboard 로그인
- Workers & Pages → Queues
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분: 성능 문제
로그 확인
실시간 로그 스트리밍:
# Producer 로그
cd telegram-bot-workers
wrangler tail --format pretty
# Consumer 로그
cd server-provision
wrangler tail --format pretty
로그 필터링:
# 에러만 확인
wrangler tail --format pretty | grep ERROR
# 특정 order_id 추적
wrangler tail --format pretty | grep "order_id: 123"
# JSON 형식 (파싱용)
wrangler tail --format json > logs.json
성공/실패 지표
DB 쿼리로 확인:
-- 상태별 주문 수
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;
로컬 실행:
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 바인딩 누락
해결:
# 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의 자동 재시도 메커니즘 무시
해결:
// ❌ 잘못된 패턴
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 핸들러 로직 누락
해결:
# 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 재시도 실패
해결:
// utils/optimistic-lock.ts에서 재시도 횟수 증가
export async function executeWithOptimisticLock<T>(
db: D1Database,
operation: (attempt: number) => Promise<T>,
maxRetries = 5 // 기본 3회 → 5회로 증가
): Promise<T> {
// ...
}
관리자 대응:
- 서버 생성 완료 확인 (Linode/Vultr 대시보드)
- DB에서 수동 잔액 차감
-- 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
해결:
# 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):
# 실시간 로그
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):
# 실시간 로그
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 상태:
# Queue 메트릭
wrangler queues describe server-provision-queue
# DLQ 메트릭
wrangler queues describe provision-dlq
# 수동으로 메시지 가져오기 (디버깅)
wrangler queues consumer server-provision-queue pull --batch-size 1
참고 자료
Cloudflare 공식 문서:
프로젝트 내부 문서:
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