본문 바로가기

AI 개발

LiteLLM Load Balancing 3편 — 프로덕션 배포: Redis 연동, Proxy 서버, 예산 관리

반응형

1편에서 라우팅 전략을, 2편에서 폴백과 장애 대응을 다뤘습니다. 코드는 완성됐지만 프로덕션에 올리면 달라지는 게 있습니다. 단일 프로세스로 돌리면 멀티 인스턴스 간 RPM/TPM 상태를 공유할 수 없고, API 키를 코드에 박으면 팀원에게 공유할 수 없고, 누가 얼마나 쓰는지 보이지 않으면 월말에 청구서로 알게 됩니다. 3편은 그 세 가지를 해결합니다. Redis로 멀티 인스턴스 상태 동기화, Docker/K8s Proxy 서버 배포, Virtual Key로 팀별 예산과 Rate Limit 관리, Langfuse·Prometheus 연동까지 — 프로덕션에서 LiteLLM이 실제로 어떻게 운영되는지 전부 다룹니다.


이 포스트 한 줄 요약 → Redis 없이 멀티 인스턴스 → 각 인스턴스가 독립적으로 RPM 카운트 → 실제 2배 트래픽 허용 → redis_host + redis_port + redis_password 설정 세 줄이 전부 → Proxy 이미지: docker.litellm.ai/berriai/litellm:main-stable (핀 필수) → 필수 인프라: PostgreSQL (Virtual Key·비용 추적) + Redis (로드밸런싱 상태) → master_key: 모든 관리 API의 관문 — sk-로 시작하는 강력한 랜덤 문자열 → Virtual Key: 실제 API 키 숨기고, 팀·사용자별 예산·RPM·모델 접근 제어 → 예산 다중 윈도우: 일별 $10 AND 월별 $100 동시 적용 가능 → K8s 권장: Pod 1개당 Uvicorn worker 1개, HPA로 수평 확장 → ⚠️ 2026년 3월 보안 사고 (v1.82.7~1.82.8) — 버전 핀 필수


왜 Redis가 필요한가 — 멀티 인스턴스 문제

단일 LiteLLM 프로세스는 RPM/TPM 카운터를 인메모리에 저장합니다. 인스턴스를 2개 띄우면 각자 독립적인 카운터를 갖습니다.

인스턴스 A: Azure 배포에 300 RPM 카운트
인스턴스 B: 같은 Azure 배포에 300 RPM 카운트
→ 실제로는 600 RPM — 한도의 2배가 Azure로 전송됨
→ 429 폭탄 발생

Redis를 붙이면 모든 인스턴스가 동일한 RPM/TPM 카운터를 공유합니다.

인스턴스 A → Redis: Azure 배포 RPM += 1
인스턴스 B → Redis: Azure 배포 RPM 조회 → 현재 값 기반 결정
→ 전체 인스턴스 합산이 한도 이내로 유지

config.yaml 완전 해부

LiteLLM Proxy의 모든 설정은 config.yaml 하나로 관리됩니다. 프로덕션 수준의 완전한 예시입니다.

# config.yaml

# ── 모델 등록 ───────────────────────────────────────────────
model_list:
  # 기본 고성능 모델 (Anthropic 직접)
  - model_name: claude-sonnet       # 클라이언트가 사용하는 이름
    litellm_params:
      model: claude-sonnet-4-6      # LiteLLM이 실제 호출하는 모델
      api_key: os.environ/ANTHROPIC_API_KEY   # 환경변수 참조
      rpm: 1000
      tpm: 200000
    model_info:
      id: claude-sonnet-direct
      order: 1

  # 같은 모델, Bedrock 경유 (멀티 리전)
  - model_name: claude-sonnet
    litellm_params:
      model: bedrock/anthropic.claude-sonnet-4-6-v1
      aws_region_name: us-east-1
      aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID
      aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY
      rpm: 1000
      tpm: 200000
    model_info:
      id: claude-sonnet-bedrock-east
      order: 2

  - model_name: claude-sonnet
    litellm_params:
      model: bedrock/anthropic.claude-sonnet-4-6-v1
      aws_region_name: eu-west-1
      aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID
      aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY
      rpm: 800
      tpm: 160000
    model_info:
      id: claude-sonnet-bedrock-eu
      order: 3

  # 교차 프로바이더 폴백 전용
  - model_name: claude-sonnet-fallback
    litellm_params:
      model: gpt-5.5
      api_key: os.environ/OPENAI_API_KEY
      rpm: 500
      tpm: 100000

  # 롱 컨텍스트 폴백 전용
  - model_name: claude-sonnet-long
    litellm_params:
      model: gemini-3.5-flash
      api_key: os.environ/GEMINI_API_KEY
      max_tokens: 1048576


# ── 라우터 설정 ─────────────────────────────────────────────
router_settings:
  routing_strategy: simple-shuffle
  num_retries: 1
  timeout: 20
  allowed_fails: 3
  cooldown_time: 60
  enable_pre_call_checks: true
  enable_weighted_failover: true

  # Redis 연동 (멀티 인스턴스 필수)
  redis_host: os.environ/REDIS_HOST
  redis_port: 6379
  redis_password: os.environ/REDIS_PASSWORD

  # 폴백 체인
  fallbacks:
    - claude-sonnet:
        - claude-sonnet-fallback
  context_window_fallbacks:
    - claude-sonnet:
        - claude-sonnet-long
    - claude-sonnet-fallback:
        - claude-sonnet-long
  default_fallbacks:
    - claude-sonnet-fallback

  # 모델 별칭 (하위 호환성)
  model_group_alias:
    gpt-4: claude-sonnet   # gpt-4 요청 → claude-sonnet 그룹으로 라우팅


# ── LiteLLM 전역 설정 ──────────────────────────────────────
litellm_settings:
  # 응답 캐싱
  cache: true
  cache_params:
    type: redis
    host: os.environ/REDIS_HOST
    port: 6379
    password: os.environ/REDIS_PASSWORD
    ttl: 3600          # 1시간 캐시
    supported_call_types:
      - acompletion
      - completion

  # 비용 추적
  success_callback:
    - langfuse          # Langfuse로 전체 추적 전송
    - prometheus        # Prometheus 메트릭 노출

  # 기본 파라미터
  drop_params: true     # 모델이 지원하지 않는 파라미터 자동 제거
  set_verbose: false    # 프로덕션에서 verbose 로그 끄기


# ── 프록시 서버 설정 ───────────────────────────────────────
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY   # 🚨 반드시 sk- 로 시작

  # PostgreSQL (Virtual Key, 비용 추적, 팀 관리 저장)
  database_url: os.environ/DATABASE_URL

  # 보안
  store_model_in_db: true    # UI에서 모델 추가 허용
  disable_spend_logs: false  # 비용 로그 활성화

  # 환경
  environment: production

환경변수 참조 방식: os.environ/변수명 형태를 쓰면 config.yaml에 실제 시크릿이 노출되지 않습니다. os.getenv("변수명")을 호출하는 것과 동일합니다.


Docker Compose 배포 — 단일 서버

# docker-compose.yml
version: "3.9"

services:
  litellm:
    # 🚨 보안: 버전 핀 필수 (2026년 3월 v1.82.7~1.82.8 공급망 사고)
    image: docker.litellm.ai/berriai/litellm:main-stable
    ports:
      - "4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml:ro   # 읽기 전용 마운트
    environment:
      - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
      - DATABASE_URL=postgresql://litellm:${POSTGRES_PASSWORD}@postgres:5432/litellm
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=${REDIS_PASSWORD}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - GEMINI_API_KEY=${GEMINI_API_KEY}
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
      # Langfuse 연동
      - LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY}
      - LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY}
      - LANGFUSE_HOST=${LANGFUSE_HOST}
    command:
      - "--config"
      - "/app/config.yaml"
      - "--port"
      - "4000"
      - "--num_workers"
      - "1"     # K8s는 1 권장, 단일 서버는 CPU 코어 수
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4000/health/liveliness"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: litellm
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
# .env
LITELLM_MASTER_KEY=sk-your-strong-random-master-key-here
POSTGRES_PASSWORD=strong-postgres-password
REDIS_PASSWORD=strong-redis-password
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=AI...
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://cloud.langfuse.com
DATABASE_URL=postgresql://litellm:${POSTGRES_PASSWORD}@postgres:5432/litellm

# 시작
docker compose up -d

# 상태 확인
curl http://localhost:4000/health/liveliness
# → {"status":"ok"}

Kubernetes 배포 — 멀티 인스턴스 고가용성

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: litellm
  namespace: ai-gateway
spec:
  replicas: 3    # 최소 2개 (고가용성), 3개 권장
  selector:
    matchLabels:
      app: litellm
  template:
    metadata:
      labels:
        app: litellm
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "4000"
        prometheus.io/path: "/metrics"
    spec:
      containers:
        - name: litellm
          # 🚨 latest 금지 — 특정 버전 핀
          image: docker.litellm.ai/berriai/litellm:main-stable
          ports:
            - containerPort: 4000
          args:
            - "--config"
            - "/app/config.yaml"
            - "--port"
            - "4000"
            - "--num_workers"
            - "1"   # K8s: Pod당 1 worker, HPA로 수평 확장
          env:
            - name: LITELLM_MASTER_KEY
              valueFrom:
                secretKeyRef:
                  name: litellm-secrets
                  key: master-key
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: litellm-secrets
                  key: database-url
            - name: REDIS_HOST
              value: "redis-service"
            - name: REDIS_PORT
              value: "6379"
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: litellm-secrets
                  key: redis-password
          volumeMounts:
            - name: config
              mountPath: /app/config.yaml
              subPath: config.yaml
          livenessProbe:
            httpGet:
              path: /health/liveliness
              port: 4000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 4000
            initialDelaySeconds: 10
            periodSeconds: 5
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "2Gi"
              cpu: "1000m"
      volumes:
        - name: config
          configMap:
            name: litellm-config
---
# HPA — 요청 수 기반 수평 확장
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: litellm-hpa
  namespace: ai-gateway
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: litellm
  minReplicas: 3
  maxReplicas: 20
  metrics:
    # CPU 50% 초과 시 스케일아웃
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50
    # 메모리 80% 초과 시 스케일아웃
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
---
# PodDisruptionBudget — 롤링 업데이트 중 가용성 보장
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: litellm-pdb
  namespace: ai-gateway
spec:
  minAvailable: 2   # 항상 최소 2개 Pod 유지
  selector:
    matchLabels:
      app: litellm

LiteLLM 공식 문서는 Pod당 Uvicorn worker 1개 + HPA 수평 확장 조합을 명시적으로 권장합니다. Worker를 늘리는 대신 Pod를 늘리는 것이 레이턴시 안정성과 HPA 임계값 튜닝에 더 효과적입니다.


Virtual Key — API 키 없이 팀과 모델 접근 제어

Virtual Key는 실제 LLM 프로바이더 API 키를 숨기고, 팀·사용자·서비스별로 독립적인 접근 제어와 예산을 적용하는 핵심 기능입니다.

# Master Key로 Virtual Key 생성
# 팀 A: Claude Sonnet만 허용, 일별 $50 한도, RPM 200
curl -X POST http://localhost:4000/key/generate \
  -H "Authorization: Bearer sk-your-master-key" \
  -H "Content-Type: application/json" \
  -d '{
    "max_budget": 50,
    "budget_duration": "1d",
    "models": ["claude-sonnet"],
    "tpm_limit": 100000,
    "rpm_limit": 200,
    "metadata": {"team": "team-a", "purpose": "production"},
    "key_alias": "team-a-production"
  }'

# 응답:
# {
#   "key": "sk-xxxxxxxxxxxxxxxxxxxxxxxx",  ← 이 키를 팀 A에 배포
#   "key_alias": "team-a-production",
#   "expires": null,
#   "max_budget": 50.0
# }
# 팀 A는 이 키로 Proxy를 사용
# 실제 Anthropic API 키는 모름
from openai import OpenAI

client = OpenAI(
    api_key="sk-xxxxxxxxxxxxxxxxxxxxxxxx",   # Virtual Key
    base_url="http://your-litellm-proxy:4000",
)

response = client.chat.completions.create(
    model="claude-sonnet",
    messages=[{"role": "user", "content": "안녕하세요"}],
)

Virtual Key가 해결하는 문제:

기존 방식:
  팀원 → 공유 API 키 → 누가 얼마나 쓰는지 모름
  키 유출 → 전체 계정 위험

Virtual Key 방식:
  팀원 → Virtual Key (팀별 고유) → 사용량 추적 가능
  키 유출 → 해당 키 즉시 비활성화 (실제 API 키 안전)
  예산 초과 → 해당 키만 429, 다른 팀 영향 없음

예산 관리 — 다중 윈도우 + 모델별 한도

# 다중 예산 윈도우: 일별 $10 AND 월별 $100
curl -X POST http://localhost:4000/key/generate \
  -H "Authorization: Bearer sk-master-key" \
  -H "Content-Type: application/json" \
  -d '{
    "key_alias": "dev-team-key",
    "models": ["claude-sonnet", "claude-sonnet-fallback"],
    "budget_duration": "1d",
    "max_budget": 10,
    "metadata": {
      "team": "dev",
      "monthly_budget": 100
    }
  }'

# 모델별 개별 예산
# (Enterprise 기능 — 특정 모델의 비용만 별도 추적)
curl -X POST http://localhost:4000/key/generate \
  -H "Authorization: Bearer sk-master-key" \
  -H "Content-Type: application/json" \
  -d '{
    "key_alias": "mixed-team-key",
    "model_max_budget": {
      "claude-sonnet": {
        "max_budget": 5,
        "budget_duration": "1d"
      },
      "claude-sonnet-fallback": {
        "max_budget": 50,
        "budget_duration": "30d"
      }
    }
  }'

예산 초과 시 동작:

예산 초과 → HTTP 429 + 에러 메시지:

{
  "error": {
    "message": "ExceededTokenBudget: Current spend for token: 10.02;
                Max Budget for Token: 10.0",
    "type": "budget_exceeded",
    "code": 429
  }
}

팀 단위 예산 설정:

# 팀 생성
curl -X POST http://localhost:4000/team/new \
  -H "Authorization: Bearer sk-master-key" \
  -H "Content-Type: application/json" \
  -d '{
    "team_alias": "engineering",
    "max_budget": 500,
    "budget_duration": "30d",
    "models": ["claude-sonnet", "claude-sonnet-fallback", "claude-sonnet-long"],
    "tpm_limit": 500000,
    "rpm_limit": 2000
  }'

# 팀에 속하는 키 생성
curl -X POST http://localhost:4000/key/generate \
  -H "Authorization: Bearer sk-master-key" \
  -H "Content-Type: application/json" \
  -d '{
    "team_id": "engineering-team-id",
    "key_alias": "engineer-john",
    "max_budget": 50,      # 개인 한도 (팀 한도의 부분집합)
    "budget_duration": "30d"
  }'

비용 추적 — 누가 얼마나 쓰는지 실시간 파악

# 팀별 지출 리포트
curl -X GET "http://localhost:4000/global/spend/report\
?start_date=2026-05-01\
&end_date=2026-05-26\
&group_by=team" \
  -H "Authorization: Bearer sk-master-key"

# 응답 예시:
# {
#   "results": [
#     {"team_id": "engineering", "spend": 247.83, "tokens": 24783000},
#     {"team_id": "product",     "spend": 89.12,  "tokens": 8912000}
#   ]
# }

# 특정 키 현재 상태 확인
curl -X GET "http://localhost:4000/key/info?key=sk-virtual-key" \
  -H "Authorization: Bearer sk-master-key"

# 일별 활동 추적
curl -X GET "http://localhost:4000/user/daily/activity\
?start_date=2026-05-20\
&end_date=2026-05-26" \
  -H "Authorization: Bearer sk-master-key"

요청에 커스텀 태그 추가 — 프로젝트·환경별 추적:

response = client.chat.completions.create(
    model="claude-sonnet",
    messages=[...],
    extra_headers={
        "x-litellm-tags": "project:recommendation-engine,env:production",
    }
)
# → Langfuse/Prometheus에서 태그별 비용 필터링 가능

Langfuse + Prometheus 모니터링 연동

# config.yaml 모니터링 섹션

litellm_settings:
  success_callback:
    - langfuse
    - prometheus

  failure_callback:
    - langfuse   # 실패 요청도 추적

  # Prometheus 메트릭 엔드포인트: GET /metrics
  # 노출되는 주요 메트릭:
  # litellm_requests_total{model, status_code}
  # litellm_request_duration_seconds{model, quantile}
  # litellm_tokens_total{model, type}
  # litellm_spend_total{model, team}
# Prometheus scrape config
scrape_configs:
  - job_name: 'litellm'
    static_configs:
      - targets: ['litellm-service:4000']
    metrics_path: '/metrics'
    scrape_interval: 15s

Grafana 대시보드 핵심 패널:

# 요청 성공률
sum(rate(litellm_requests_total{status_code="200"}[5m]))
/ sum(rate(litellm_requests_total[5m])) * 100

# 모델별 P95 레이턴시
histogram_quantile(0.95, 
  sum(rate(litellm_request_duration_seconds_bucket[5m])) by (le, model)
)

# 팀별 시간당 비용
sum(rate(litellm_spend_total[1h])) by (team)

# 폴백 발생률
sum(rate(litellm_requests_total{fallback="true"}[5m]))
/ sum(rate(litellm_requests_total[5m])) * 100

프로덕션 배포 체크리스트

인프라
□ PostgreSQL 연결 확인 (Virtual Key, 비용 추적 저장)
□ Redis 연결 확인 (멀티 인스턴스 RPM/TPM 동기화)
□ Docker 이미지 특정 버전으로 핀 (latest 사용 금지)
□ HTTPS/TLS 설정 (Ingress 또는 LoadBalancer)

보안
□ LITELLM_MASTER_KEY 강력한 랜덤 문자열 (sk- 시작)
□ 모든 API 키 환경변수로 관리 (config.yaml에 하드코딩 금지)
□ Virtual Key로 팀별 접근 분리 (공유 API 키 사용 금지)
□ LITELLM_SALT_KEY 설정 (저장된 크레덴셜 암호화)

고가용성
□ 최소 2개 replica (3개 권장)
□ HPA 설정 (CPU 50%, 메모리 80% 기준)
□ PodDisruptionBudget 설정 (롤링 업데이트 시 가용성)
□ Liveness/Readiness Probe 설정

비용 관리
□ 팀별 Virtual Key + 예산 한도 설정
□ 일별/월별 다중 예산 윈도우 적용
□ 예산 소진 알림 설정 (Slack/PagerDuty)

모니터링
□ Prometheus 메트릭 수집
□ Grafana 대시보드 (성공률, 레이턴시, 비용)
□ Langfuse 전체 추적 연동
□ 비용 초과 알림 설정
□ 폴백 발생률 추적

⚠️ 보안 사고 노트 (2026년 3월)

2026년 3월, LiteLLM v1.82.7과 v1.82.8에 공급망 보안 사고가 발생했습니다. 두 버전은 즉시 풀리고 v1.83.0에 클린 빌드가 배포됐습니다. 현재 이 버전을 사용하고 있다면 즉시 업그레이드가 필요합니다.

이 사고에서 얻어야 할 교훈은 두 가지입니다. Docker 이미지를 latest나 main-latest로 사용하면 보안 사고가 발생한 버전이 자동으로 배포될 수 있습니다. 특정 버전으로 핀하고, 업그레이드는 검증 후 수동으로 진행해야 합니다.

# ❌ 위험
image: docker.litellm.ai/berriai/litellm:latest

# ✅ 안전 (특정 stable 태그 또는 버전 핀)
image: docker.litellm.ai/berriai/litellm:main-stable
# 또는
image: docker.litellm.ai/berriai/litellm:v1.84.0

✅ 결론

항목 설정

멀티 인스턴스 RPM/TPM redis_host + redis_port + redis_password
API 키 보안 환경변수 참조 (os.environ/KEY)
팀별 접근 제어 Virtual Key + models 화이트리스트
비용 관리 max_budget + budget_duration + 다중 윈도우
K8s 성능 Pod당 1 worker + HPA 수평 확장
이미지 보안 특정 버전 핀 (latest 금지)
모니터링 Prometheus 메트릭 + Langfuse 추적
가용성 최소 3 replica + PDB + liveness probe

3편의 인프라 기반이 완성되면 LiteLLM은 단순한 로드밸런서를 넘어 팀 전체의 LLM 사용을 통제하는 게이트웨이가 됩니다. 4편에서는 시맨틱 라우팅, 멀티 리전 패턴, 실전 아키텍처 3가지를 다룹니다.

 

LiteLLM 시리즈 완결

  1. ✅ Router 구조와 라우팅 전략 6가지  https://cell-devlog.tistory.com/273
  2. ✅ 폴백 전략과 장애 대응 https://cell-devlog.tistory.com/274
  3. ✅ 프로덕션 배포: Redis + Proxy 서버 https://cell-devlog.tistory.com/275
  4. ✅ 고급 라우팅과 실전 아키텍처 https://cell-devlog.tistory.com/276
반응형