86bf2a9b0ef51f949ed9e4fb9c00028c3fcb9d65
response variable was scoped inside if(!bypassCache) block but referenced outside. Changed to const response in the R2 fetch section and conditionally cache based on bypassCache flag.
CF Multisite
Cloudflare Workers + R2 기반 멀티테넌트 정적 사이트 호스팅 플랫폼
아키텍처
┌─────────────────────────────────────────────────────────────────┐
│ Gitea (gitea.inouter.com) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ site-a │ │ site-b │ │ site-c │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ push │ push │ push │
└───────┼─────────────┼─────────────┼─────────────────────────────┘
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Gitea Actions Runner (jp1) │
│ aws s3 sync → R2 │
└───────────────────────────┬─────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare R2 Bucket │
│ /sites/site-a/index.html │
│ /sites/site-b/index.html │
│ /sites/site-c/index.html │
└───────────────────────────┬─────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Workers │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Cache │ │ Rate Limit │ │ Admin API │ │
│ │ (Edge) │ │ (KV) │ │ (REST) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└───────────────────────────┬─────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Custom Domains │
│ site-a.actions.it.com site-b.actions.it.com ... │
└─────────────────────────────────────────────────────────────────┘
주요 기능
멀티테넌트 호스팅
- 서브도메인 기반 고객 분리:
{customer}.actions.it.com - R2에 고객별 디렉토리 구조:
/sites/{customer}/ - 자동 index.html 라우팅
캐싱 전략
- Edge 캐시로 R2 요청 최소화 → 비용 절감
- 파일 타입별 TTL 최적화:
| 파일 타입 | 캐시 TTL |
|---|---|
| HTML | 1시간 |
| CSS, JS, JSON | 24시간 |
| 이미지 (PNG, JPG, GIF, SVG) | 7일 |
| 폰트 (WOFF, WOFF2, TTF) | 30일 |
Rate Limiting (티어별)
| 티어 | 분당 요청 | 일일 대역폭 | 월간 대역폭 |
|---|---|---|---|
| Free | 60 | 5GB | ~150GB |
| Basic | 300 | 50GB | ~1.5TB |
| Pro | 1,000 | 500GB | ~15TB |
사용량 추적 (KV)
- 고객별 일일 요청 수
- 고객별 일일 대역폭
- 분당 요청 수 (Rate Limit용)
- 7일간 데이터 보관
Admin API
모든 API는 Bearer 토큰 인증 필요:
curl -H "Authorization: Bearer $ADMIN_TOKEN" https://site.actions.it.com/api/...
엔드포인트
| Method | Endpoint | 설명 |
|---|---|---|
| GET | /api/usage/:customer?days=7 |
고객 사용량 조회 |
| GET | /api/customers |
전체 고객 목록 |
| PUT | /api/tier/:customer |
고객 티어 변경 |
| GET | /api/stats |
전체 통계 |
| DELETE | /api/customer/:customer |
고객 데이터 삭제 |
사용 예시
# 고객 사용량 조회
curl -H "Authorization: Bearer $TOKEN" \
"https://multisite-demo.actions.it.com/api/usage/multisite-demo?days=7"
# 티어 변경
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tier": "basic"}' \
"https://multisite-demo.actions.it.com/api/tier/customer-name"
# 전체 통계
curl -H "Authorization: Bearer $TOKEN" \
"https://multisite-demo.actions.it.com/api/stats"
설치 및 배포
1. 프로젝트 클론
cd ~/projects
git clone <repo> cf-multisite
cd cf-multisite
npm install
2. Cloudflare 설정
# wrangler 로그인
npx wrangler login
# KV 네임스페이스 생성
npx wrangler kv:namespace create USAGE
# R2 버킷 생성 (이미 있으면 스킵)
npx wrangler r2 bucket create multisite-bucket
3. wrangler.toml 설정
name = "cf-multisite"
main = "src/worker.js"
compatibility_date = "2024-12-01"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "multisite-bucket"
[[kv_namespaces]]
binding = "USAGE"
id = "<KV_NAMESPACE_ID>"
routes = [
{ pattern = "*.actions.it.com", zone_name = "actions.it.com" }
]
4. 시크릿 설정
# Admin API 토큰 생성 및 설정
openssl rand -hex 32 # 토큰 생성
npx wrangler secret put ADMIN_TOKEN
5. 배포
npm run deploy
# 또는
npx wrangler deploy
고객 사이트 추가
1. Gitea 저장소 생성
고객용 저장소를 Gitea에 생성합니다.
2. Workflow 파일 추가
.gitea/workflows/deploy.yml:
name: Deploy to CF Multisite
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install AWS CLI
run: |
curl -sL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
cd /tmp && unzip -q awscliv2.zip && sudo ./aws/install
- name: Deploy to R2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
run: |
SITE_ID="${{ github.event.repository.name }}"
aws s3 sync . "s3://multisite-bucket/sites/${SITE_ID}/" \
--endpoint-url "${R2_ENDPOINT}" \
--region auto \
--exclude ".git/*" \
--exclude ".gitea/*" \
--exclude "README.md" \
--delete
echo "Deployed to: https://${SITE_ID}.actions.it.com"
3. Secrets 설정 (Gitea Organization)
| Secret | 값 |
|---|---|
R2_ACCESS_KEY |
R2 API 토큰 Access Key |
R2_SECRET_KEY |
R2 API 토큰 Secret Key |
R2_ENDPOINT |
https://<account-id>.r2.cloudflarestorage.com |
4. Push → 자동 배포
git add .
git commit -m "Initial site"
git push origin main
# → https://{repo-name}.actions.it.com 에서 확인
로컬 개발
# 개발 서버 실행
npm run dev
# 테스트 URL
# http://localhost:8787?site=demo
# http://localhost:8787?site=multisite-demo
파일 구조
cf-multisite/
├── src/
│ └── worker.js # Workers 메인 코드
│ # - 라우팅
│ # - 캐싱
│ # - Rate Limiting
│ # - Admin API
├── scripts/
│ └── upload.js # 수동 R2 업로드 스크립트
├── sample-site/ # 샘플 사이트
│ ├── index.html
│ ├── about.html
│ ├── contact.html
│ └── style.css
├── .gitea/
│ └── workflows/
│ └── deploy.yml # Gitea Actions 템플릿
├── wrangler.toml # Workers 설정
├── package.json
└── README.md
인프라 구성
| 컴포넌트 | 위치 | 용도 |
|---|---|---|
| Gitea | gitea.inouter.com | Git 호스팅 |
| Gitea Runner | jp1 (Incus) | CI/CD 실행 |
| R2 Bucket | multisite-bucket | 정적 파일 저장 |
| KV Namespace | USAGE | 사용량 추적 |
| Workers | cf-multisite | 라우팅/캐싱/API |
| Domain | *.actions.it.com | 와일드카드 도메인 |
비용 구조
Cloudflare 무료 티어
| 항목 | 무료 한도 |
|---|---|
| Workers 요청 | 일 10만 건 |
| R2 저장 | 10GB |
| R2 Class A (쓰기) | 월 100만 건 |
| R2 Class B (읽기) | 월 1000만 건 |
| KV 읽기 | 일 10만 건 |
| KV 쓰기 | 일 1000건 |
예상 비용 (1000 고객 기준)
저장: 1000 × 50MB = 50GB → 초과 40GB × $0.015 = $0.60/월
읽기: 캐시 히트율 90% 가정 → 대부분 무료
Workers: 캐시 히트 시에도 실행됨 → 유료 플랜 권장 ($5/월)
모니터링
응답 헤더
| 헤더 | 설명 |
|---|---|
X-Cache: HIT/MISS |
캐시 상태 |
X-Customer |
고객 ID |
X-Tier |
고객 티어 |
X-RateLimit-Reason |
Rate Limit 사유 (rpm/bandwidth) |
사용량 확인
# 특정 고객
curl -H "Authorization: Bearer $TOKEN" \
"https://any.actions.it.com/api/usage/customer-name"
# 전체 현황
curl -H "Authorization: Bearer $TOKEN" \
"https://any.actions.it.com/api/stats"
확장 계획
클러스터링 옵션
| 방식 | 장점 | 적합한 규모 |
|---|---|---|
| Incus 단일 노드 | 간단, 저비용 | 현재 |
| Incus 클러스터 (jp1+kr1) | HA, 마이그레이션 | 중규모 |
| Kubernetes | 자동 스케일링 | 대규모 |
결제 연동 (검토 중)
| PG | 대상 | 비고 |
|---|---|---|
| Toss Payments | 국내 고객 | 보증보험 필요 |
| Stripe (일본 법인) | 해외/텔레그램 | 직접 연동 가능 |
크레덴셜 (Vault)
Vault: https://hcv.inouter.com
Path: secret/cf-multisite
- admin_token: Admin API 인증 토큰
- r2_access_key: R2 API Access Key
- r2_secret_key: R2 API Secret Key
관련 링크
- Cloudflare Dashboard: https://dash.cloudflare.com
- Gitea: https://gitea.inouter.com
- 샘플 사이트: https://multisite-demo.actions.it.com
Description
Languages
JavaScript
80.1%
HTML
12.5%
CSS
7.4%