Files
telegram-bot-workers/docs/server-provision-architecture.md
kappa 2494593b62 docs: add server provisioning architecture document
- 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>
2026-01-24 22:09:21 +09:00

1327 lines
35 KiB
Markdown

# 서버 프로비저닝 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<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
**자동 재시도 설정:**
```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<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)
**요청:**
```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<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)
```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<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)
```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<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 인프라 구축
**작업:**
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<T>(
db: D1Database,
operation: (attempt: number) => Promise<T>,
maxRetries = 5 // 기본 3회 → 5회로 증가
): Promise<T> {
// ...
}
```
**관리자 대응:**
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