보안 개선: - API 키 하드코딩 제거 (NAMECHEAP_API_KEY_INTERNAL) - CORS 정책: * → hosting.anvil.it.com 제한 - /health 엔드포인트 DB 정보 노출 방지 - Rate Limiting 인메모리 Map → Cloudflare KV 전환 - 분산 환경 일관성 보장 - 재시작 후에도 유지 - 자동 만료 (TTL) 문서: - CLAUDE.md Security 섹션 추가 - KV Namespace 설정 가이드 추가 - 배포/마이그레이션 가이드 추가 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
373 lines
8.5 KiB
Markdown
373 lines
8.5 KiB
Markdown
# Rate Limiting KV Migration Guide
|
|
|
|
## 변경 사항 요약
|
|
|
|
Rate Limiting 시스템을 인메모리 Map에서 Cloudflare KV로 마이그레이션했습니다.
|
|
|
|
### 기존 문제점
|
|
- Workers 인스턴스 간 공유되지 않음
|
|
- 재시작 시 초기화됨
|
|
- 분산 환경에서 Rate Limit 우회 가능
|
|
|
|
### 개선 사항
|
|
✅ 인스턴스 간 공유 (KV 기반)
|
|
✅ 재시작 후에도 유지
|
|
✅ 분산 환경에서 일관된 Rate Limiting
|
|
✅ 자동 만료 (TTL)
|
|
|
|
---
|
|
|
|
## 배포 절차
|
|
|
|
### 1. KV Namespace 생성
|
|
|
|
```bash
|
|
# Production용 Namespace 생성
|
|
wrangler kv:namespace create RATE_LIMIT_KV
|
|
```
|
|
|
|
출력 예시:
|
|
```
|
|
⛅️ wrangler 3.x.x
|
|
-------------------
|
|
🌀 Creating namespace with title "telegram-summary-bot-RATE_LIMIT_KV"
|
|
✨ Success!
|
|
Add the following to your configuration file in your kv_namespaces array:
|
|
{ binding = "RATE_LIMIT_KV", id = "abc123def456ghi789jkl012mno345pq" }
|
|
```
|
|
|
|
### 2. wrangler.toml 업데이트
|
|
|
|
위에서 출력된 `id` 값을 복사하여 `wrangler.toml` 파일의 20-22번째 줄을 수정합니다:
|
|
|
|
```toml
|
|
[[kv_namespaces]]
|
|
binding = "RATE_LIMIT_KV"
|
|
id = "abc123def456ghi789jkl012mno345pq" # ← 실제 ID로 변경
|
|
```
|
|
|
|
### 3. 로컬 개발용 Namespace 생성 (선택사항)
|
|
|
|
로컬 테스트 시 별도의 KV를 사용하려면:
|
|
|
|
```bash
|
|
wrangler kv:namespace create RATE_LIMIT_KV --preview
|
|
```
|
|
|
|
출력된 `preview_id`를 `wrangler.toml`에 추가:
|
|
|
|
```toml
|
|
[[kv_namespaces]]
|
|
binding = "RATE_LIMIT_KV"
|
|
id = "abc123def456ghi789jkl012mno345pq"
|
|
preview_id = "xyz789abc123def456ghi012jkl345mno" # 로컬 개발용
|
|
```
|
|
|
|
### 4. TypeScript 컴파일 확인
|
|
|
|
```bash
|
|
npx tsc --noEmit
|
|
```
|
|
|
|
에러가 없어야 합니다.
|
|
|
|
### 5. 로컬 테스트
|
|
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
다른 터미널에서 테스트 요청:
|
|
|
|
```bash
|
|
# 첫 번째 요청 (성공)
|
|
curl -X POST http://localhost:8787/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Telegram-Bot-Api-Secret-Token: ${WEBHOOK_SECRET}" \
|
|
-d '{"update_id":1,"message":{"message_id":1,"from":{"id":123,"is_bot":false,"first_name":"Test"},"chat":{"id":123,"type":"private"},"date":1234567890,"text":"테스트"}}'
|
|
|
|
# 30번 연속 요청 후 31번째 요청 (Rate Limit)
|
|
# Rate Limit 메시지가 표시되어야 함
|
|
```
|
|
|
|
### 6. Production 배포
|
|
|
|
```bash
|
|
npm run deploy
|
|
```
|
|
|
|
### 7. 배포 확인
|
|
|
|
```bash
|
|
# 로그 스트리밍
|
|
npm run tail
|
|
|
|
# Health Check
|
|
curl https://telegram-summary-bot.kappa-d8e.workers.dev/health
|
|
```
|
|
|
|
---
|
|
|
|
## 변경된 파일
|
|
|
|
### `/Users/kaffa/telegram-bot-workers/wrangler.toml`
|
|
```diff
|
|
[[d1_databases]]
|
|
binding = "DB"
|
|
database_name = "telegram-conversations"
|
|
database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e"
|
|
|
|
+[[kv_namespaces]]
|
|
+binding = "RATE_LIMIT_KV"
|
|
+id = "YOUR_KV_NAMESPACE_ID" # Run: wrangler kv:namespace create RATE_LIMIT_KV
|
|
```
|
|
|
|
### `/Users/kaffa/telegram-bot-workers/src/types.ts`
|
|
```diff
|
|
export interface Env {
|
|
DB: D1Database;
|
|
AI: Ai;
|
|
BOT_TOKEN: string;
|
|
WEBHOOK_SECRET: string;
|
|
// ... 기타 환경변수
|
|
+ RATE_LIMIT_KV: KVNamespace;
|
|
}
|
|
```
|
|
|
|
### `/Users/kaffa/telegram-bot-workers/src/security.ts`
|
|
```diff
|
|
-// Rate Limiting
|
|
-const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
|
-
|
|
-export function checkRateLimit(
|
|
- userId: string,
|
|
- maxRequests: number = 30,
|
|
- windowMs: number = 60000
|
|
-): boolean {
|
|
- // 인메모리 Map 기반 로직
|
|
-}
|
|
|
|
+// Rate Limiting (Cloudflare KV 기반)
|
|
+interface RateLimitData {
|
|
+ count: number;
|
|
+ resetAt: number;
|
|
+}
|
|
+
|
|
+export async function checkRateLimit(
|
|
+ kv: KVNamespace,
|
|
+ userId: string,
|
|
+ maxRequests: number = 30,
|
|
+ windowMs: number = 60000
|
|
+): Promise<boolean> {
|
|
+ // KV 기반 로직 (자동 TTL)
|
|
+}
|
|
```
|
|
|
|
### `/Users/kaffa/telegram-bot-workers/src/index.ts`
|
|
```diff
|
|
- if (!checkRateLimit(telegramUserId)) {
|
|
+ if (!(await checkRateLimit(env.RATE_LIMIT_KV, telegramUserId))) {
|
|
await sendMessage(
|
|
env.BOT_TOKEN,
|
|
chatId,
|
|
'⚠️ 너무 많은 요청입니다. 잠시 후 다시 시도해주세요.'
|
|
);
|
|
return;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 기술 상세
|
|
|
|
### Rate Limiting 알고리즘
|
|
|
|
**Key 형식:** `ratelimit:{userId}`
|
|
|
|
**데이터 구조:**
|
|
```typescript
|
|
{
|
|
count: number, // 현재 윈도우 내 요청 수
|
|
resetAt: number // 윈도우 만료 시각 (Unix timestamp)
|
|
}
|
|
```
|
|
|
|
**처리 로직:**
|
|
1. KV에서 사용자별 카운터 조회
|
|
2. 윈도우 만료 확인 (현재 시각 > resetAt)
|
|
- 만료 시: 새 윈도우 시작 (count=1)
|
|
- 유효 시: count 확인
|
|
3. count >= maxRequests (기본 30) → Rate Limit 초과
|
|
4. count < maxRequests → count 증가 후 KV 업데이트
|
|
|
|
**자동 만료:**
|
|
- KV의 `expirationTtl` 사용 (초 단위)
|
|
- 윈도우 종료 시 자동 삭제 (메모리 효율)
|
|
|
|
**에러 처리:**
|
|
- KV 오류 시 Rate Limit 통과 (서비스 가용성 우선)
|
|
- 로그 기록: `[RateLimit] KV 오류: ...`
|
|
|
|
---
|
|
|
|
## 모니터링
|
|
|
|
### KV Namespace 확인
|
|
|
|
```bash
|
|
# KV Namespace 목록
|
|
wrangler kv:namespace list
|
|
|
|
# 특정 키 조회
|
|
wrangler kv:key get "ratelimit:821596605" --namespace-id=YOUR_KV_ID
|
|
|
|
# 모든 키 목록
|
|
wrangler kv:key list --namespace-id=YOUR_KV_ID
|
|
```
|
|
|
|
### Cloudflare Dashboard
|
|
1. https://dash.cloudflare.com → Workers & Pages
|
|
2. KV → `telegram-summary-bot-RATE_LIMIT_KV` 클릭
|
|
3. Key-Value 쌍 확인 (실시간)
|
|
|
|
### 로그 확인
|
|
|
|
```bash
|
|
wrangler tail
|
|
|
|
# Rate Limit 오류만 필터링
|
|
wrangler tail --format json | jq 'select(.message | contains("RateLimit"))'
|
|
```
|
|
|
|
---
|
|
|
|
## 트러블슈팅
|
|
|
|
### KV Namespace ID를 잊었을 때
|
|
```bash
|
|
wrangler kv:namespace list
|
|
```
|
|
|
|
### KV 데이터 초기화 (테스트용)
|
|
```bash
|
|
# 특정 사용자 Rate Limit 초기화
|
|
wrangler kv:key delete "ratelimit:821596605" --namespace-id=YOUR_KV_ID
|
|
|
|
# 전체 초기화 (주의!)
|
|
wrangler kv:key list --namespace-id=YOUR_KV_ID | jq -r '.[] | .name' | xargs -I {} wrangler kv:key delete "{}" --namespace-id=YOUR_KV_ID
|
|
```
|
|
|
|
### Rate Limit 테스트 스크립트
|
|
```bash
|
|
#!/bin/bash
|
|
# rate-limit-test.sh
|
|
TOKEN="YOUR_WEBHOOK_SECRET"
|
|
|
|
for i in {1..35}; do
|
|
echo "Request $i:"
|
|
curl -s -X POST http://localhost:8787/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Telegram-Bot-Api-Secret-Token: $TOKEN" \
|
|
-d '{"update_id":'$i',"message":{"message_id":'$i',"from":{"id":123,"is_bot":false,"first_name":"Test"},"chat":{"id":123,"type":"private"},"date":1234567890,"text":"테스트 '$i'"}}' \
|
|
| jq -r '.message // "OK"'
|
|
sleep 0.5
|
|
done
|
|
```
|
|
|
|
---
|
|
|
|
## 성능 영향
|
|
|
|
### Before (인메모리 Map)
|
|
- 응답 시간: ~1ms (동기)
|
|
- 메모리: 인스턴스별 독립
|
|
- 분산 환경: 비효율적
|
|
|
|
### After (KV)
|
|
- 응답 시간: ~20-50ms (KV read/write 포함)
|
|
- 메모리: 0 (KV로 오프로드)
|
|
- 분산 환경: 일관성 보장
|
|
|
|
**권장 사항:** KV 호출을 줄이기 위해 windowMs를 늘리지 말 것 (현재 60초 최적)
|
|
|
|
---
|
|
|
|
## 롤백 절차 (문제 발생 시)
|
|
|
|
### 1. 이전 버전 복구
|
|
```bash
|
|
git revert HEAD
|
|
npm run deploy
|
|
```
|
|
|
|
### 2. wrangler.toml에서 KV Namespace 제거
|
|
```toml
|
|
# 아래 3줄 주석 처리 또는 삭제
|
|
# [[kv_namespaces]]
|
|
# binding = "RATE_LIMIT_KV"
|
|
# id = "..."
|
|
```
|
|
|
|
### 3. 재배포
|
|
```bash
|
|
npm run deploy
|
|
```
|
|
|
|
---
|
|
|
|
## FAQ
|
|
|
|
**Q: 로컬 개발 시 KV를 사용하지 않으려면?**
|
|
A: `--remote` 플래그 사용하여 Production KV 직접 사용 (권장하지 않음)
|
|
```bash
|
|
wrangler dev --remote
|
|
```
|
|
|
|
**Q: KV 비용은 얼마나 드나요?**
|
|
A: Cloudflare Workers Free Plan: 100,000 read/day, 1,000 write/day 무료
|
|
- Rate Limiting: 1 read + 1 write per request
|
|
- 하루 1,000명 사용자까지 무료
|
|
|
|
**Q: KV가 느린 경우?**
|
|
A: KV는 글로벌 분산 스토리지이므로 20-50ms latency 정상
|
|
- 대안: Durable Objects (더 빠르지만 비용 높음)
|
|
|
|
**Q: Rate Limit 설정 변경하려면?**
|
|
A: `security.ts:checkRateLimit()` 파라미터 수정
|
|
```typescript
|
|
// 기본값: 30 requests / 60초
|
|
export async function checkRateLimit(
|
|
kv: KVNamespace,
|
|
userId: string,
|
|
maxRequests: number = 50, // ← 변경
|
|
windowMs: number = 120000 // ← 2분으로 변경
|
|
): Promise<boolean>
|
|
```
|
|
|
|
---
|
|
|
|
## 문서 업데이트
|
|
|
|
이 마이그레이션으로 인해 다음 문서도 업데이트 필요:
|
|
|
|
- [x] `CLAUDE.md` - Rate Limiting 섹션 수정
|
|
- [ ] `README.md` - 설치/배포 절차 업데이트 (KV Namespace 생성 추가)
|
|
- [ ] `schema.sql` - 변경 없음 (KV는 별도 저장소)
|
|
|
|
---
|
|
|
|
## 체크리스트
|
|
|
|
배포 전 확인:
|
|
|
|
- [ ] `wrangler kv:namespace create RATE_LIMIT_KV` 실행 완료
|
|
- [ ] `wrangler.toml`에 실제 KV Namespace ID 입력
|
|
- [ ] `npx tsc --noEmit` 타입 에러 없음
|
|
- [ ] 로컬 테스트 (`npm run dev`) 정상 동작
|
|
- [ ] Rate Limit 테스트 (30회 연속 요청) 성공
|
|
- [ ] Production 배포 (`npm run deploy`) 완료
|
|
- [ ] Health Check 확인
|
|
- [ ] 실제 Telegram 메시지 테스트 완료
|
|
|
|
배포 완료!
|