DOCS/_archive/docs/architecture/Nginx_아키텍쳐.md
happybell80 725ad0876c fix: 문서 파일 실행 권한 제거
- 모든 .md, .html 파일 권한을 644로 정상화
- .gitignore 파일 권한도 644로 수정
- 문서 파일에 실행 권한은 불필요하고 보안상 바람직하지 않음
- deprecated 아이디어 폴더 생성 및 레벨별 UI 변경 아이디어 이동
2025-08-18 00:37:51 +09:00

18 KiB

Nginx 기반 로빙 컨테이너 아키텍처 설계

목차

  1. 배경 및 문제점
  2. Docker 이미지 최적화
  3. Nginx 리버스 프록시 아키텍처
  4. 컨테이너 동적 관리
  5. 비용 분석
  6. Docker 유료화 현황
  7. 아키텍처 성능 비교
  8. 로빙 컨테이너 아키텍처 설계
  9. 예제 구현

배경 및 문제점

초기 상황

  • 각 폴더(docs, frontend, api-base, test_api, test_meta-skill, test_front)의 git 상태 확인 및 동기화 완료
  • api-base 폴더에서 새로운 변경사항 발견:
    • Dockerfile 및 docker-compose.yml 추가
    • static/test.html 파일 추가
    • requirements.txt 업데이트

발견된 문제점

  • Docker 이미지 용량 과다: 1.54GB로 예상보다 크게 빌드됨
  • 고정 파라미터: run.py에서 모든 설정값이 하드코딩됨
  • 확장성 문제: 컨테이너 수 증가 시 nginx.conf 수동 관리 필요

Docker 이미지 최적화

문제 분석

기존 Dockerfile에서 불필요한 패키지 설치로 인한 용량 증가:

# 기존 문제가 있던 Dockerfile
FROM python:3.11-slim

RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

gcc 필요성 검증

requirements.txt 분석 결과:

fastapi>=0.104.0
uvicorn>=0.24.0
psycopg2-binary>=2.9.0  # 바이너리 버전 사용
chromadb>=0.4.0
pymupdf>=1.23.0
python-jose[cryptography]>=3.3.0
# ... 기타 패키지들

테스트 결과: gcc 없이도 모든 패키지가 정상 설치됨

  • 모든 패키지가 바이너리 wheel로 제공
  • psycopg2-binary 사용으로 컴파일 불필요

최적화된 Dockerfile

FROM python:3.11-slim

WORKDIR /app

# 필수 런타임 의존성만 설치
RUN apt-get update && apt-get install -y --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# requirements 먼저 복사 (캐시 활용)
COPY requirements.txt .
RUN pip install --no-cache-dir --no-compile -r requirements.txt

# 애플리케이션 코드 복사
COPY . .

# 비루트 사용자 생성
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# 포트 노출
EXPOSE 8000

# 환경변수로 설정 관리
ENV HOST=0.0.0.0
ENV PORT=8000
ENV RELOAD=false
ENV LOG_LEVEL=info

# 파라미터화된 실행
CMD ["sh", "-c", "python run.py --host $HOST --port $PORT"]

run.py 파라미터 외부화

기존 하드코딩된 설정:

# 기존 run.py
if __name__ == "__main__":
    uvicorn.run(
        "app.main:app",
        host="0.0.0.0",      # 고정값
        port=8000,           # 고정값
        reload=True,         # 고정값
        log_level="info"     # 고정값
    )

개선된 환경변수 기반 설정:

import os
import uvicorn
from app.main import app

if __name__ == "__main__":
    uvicorn.run(
        "app.main:app",
        host=os.getenv("HOST", "0.0.0.0"),
        port=int(os.getenv("PORT", "8000")),
        reload=os.getenv("RELOAD", "false").lower() == "true",
        log_level=os.getenv("LOG_LEVEL", "info")
    )

Nginx 리버스 프록시 아키텍처

기본 구조

[Client Request]
     ↓
 [Nginx (Reverse Proxy)]
     ↓                 ↓                  ↓
[Container A]     [Container B]     [Container C]
(e.g. FastAPI)    (e.g. Node.js)     (e.g. LangChain)

Nginx 설정 예시

http {
    upstream app1 {
        server container-a:8000;
    }

    upstream app2 {
        server container-b:8001;
    }

    server {
        listen 80;

        location /app1/ {
            proxy_pass http://app1/;
        }

        location /app2/ {
            proxy_pass http://app2/;
        }
    }
}

Docker Compose 구성

version: '3.8'
services:
  nginx:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - container-a
      - container-b

  container-a:
    build: ./app1
    expose:
      - "8000"

  container-b:
    build: ./app2
    expose:
      - "8001"

Nginx 컨테이너화 필요성

컨테이너로 감싸는 이유:

  • 이식성: 어디서든 동일한 환경으로 실행 가능
  • 버전 고정: Nginx 이미지 버전 명시로 예기치 않은 업데이트 방지
  • 구성 분리: nginx.conf 등 설정 파일을 외부 볼륨으로 관리
  • CI/CD 통합: 자동 배포 및 테스트 환경 구성 용이
  • DevOps 표준화: 다른 마이크로서비스와 동일한 방식으로 관리

컨테이너 동적 관리

nginx.conf 동적 갱신 문제

문제: 기본 nginx는 정적 설정 파일 기반으로 컨테이너 수 증가 시 자동 갱신 불가

해결 방법:

1. Docker + Nginx + Template 갱신 도구

# nginx-proxy 사용 예시
docker run -d -p 80:80 \
  -v /var/run/docker.sock:/tmp/docker.sock:ro \
  jwilder/nginx-proxy

2. Traefik 사용 (권장)

  • Docker 레이블 기반 동적 서비스 탐지
  • 자동 라우팅 처리
  • Let's Encrypt 자동 적용
services:
  traefik:
    image: traefik:v2.10
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

조건 기반 컨테이너 자동 제어

시나리오: 로빙 레벨업, 스킬 업데이트 시 자동 컨테이너 관리

구현 방식:

import docker

def handle_level_up(robeing_id, new_level):
    client = docker.from_env()
    
    # 기존 컨테이너 중단
    old_container = client.containers.get(f"robeing-{robeing_id}")
    old_container.stop()
    old_container.remove()
    
    # 새 레벨에 맞는 컨테이너 시작
    client.containers.run(
        f"robeing:level-{new_level}",
        name=f"robeing-{robeing_id}",
        detach=True,
        volumes={
            f"robeing-{robeing_id}-data": {
                "bind": "/app/data",
                "mode": "rw"
            }
        }
    )

자동화 조건 예시:

  • 로빙 레벨 5 도달 → GPT-4o 컨테이너 시작
  • 스킬X 업그레이드 → 기존 컨테이너 중단 후 새 이미지로 재배포
  • PDF 스킬 7일간 미사용 → 해당 컨테이너 자동 종료

비용 분석

컨테이너 자동화 비용

비용 절감 전략:

  • 이벤트 기반 실행: 사용 시에만 컨테이너 활성화
  • 레벨 제한: 고성능 스킬은 상위 레벨에만 허용
  • 경량 컨테이너: FastAPI, uvicorn 기반 최소화
  • 무료 티어 활용: Hetzner, Railway, Supabase 등

예산 범위 (월 기준, 1-3 스킬):

방식 예상 비용 비고
AWS ECS Fargate + S3 $3-8 idle auto-off 조건
Supabase Edge Function $0-5 기본 무료 + API 통제
Hetzner VPS + docker-compose €4-6 CX11 기준
Railway (1GB RAM) $0-5 스킬 2-3개

컨테이너 호스팅 서비스 비용

내부 전용 플랫폼 (로빙 등):

  • 인력: 1-2명 (DevOps + 백엔드)
  • 비용: 월 3-10만원
  • 구성: Hetzner VPS + Supabase + Docker

상업용 PaaS 플랫폼:

  • 인력: 5-10명 이상
  • 비용: 수천만원 이상
  • 예시: Railway, Render, Vercel 수준

Docker 유료화 현황

정책 요약 (2024-2025)

  • Docker Engine, Compose: 계속 무료
  • Docker Desktop: 연매출 $1천만 이상 기업만 유료
  • Docker Hub: Free 요금제 Pull 제한 (6시간당 100회)

요금 구조

플랜 가격 대상
Docker Personal 무료 개인/소규모 상업용
Docker Pro $9/월 개발자
Docker Team $15/월 협업팀
Docker Business $24/월 엔터프라이즈

결론: 스타트업/MVP 단계에서는 비용 거의 없음


아키텍처 성능 비교

속도 측면 비교

항목 Docker (컨테이너) VM (가상머신) 서버리스 (FaaS) 베어메탈
시작 속도 매우 빠름 (초 단위) 느림 (수십 초~수분) 보통~빠름 (콜드스타트) 느림 (OS 부팅)
처리 속도 (CPU) 거의 네이티브급 약간 느림 느림~중간 최상
IO 성능 빠름 (호스트 공유) 느림 (가상화 레이어) 느림~중간 최상
성능 일관성 중간~높음 높음 낮음 (콜드스타트) 최상
멀티 컨테이너 처리 뛰어남 복잡 불가 (함수 단위) 복잡

결론: 로빙 같은 동적 자원 할당과 상태 지속이 필요한 구조에서는 컨테이너 기반이 최적


로빙 컨테이너 아키텍처 설계

핵심 요구사항

  • 레벨업 시 자원 증가: 컨테이너 재시작으로 max 성능 향상
  • 기억/코드 유지: 볼륨 마운트로 데이터 지속성 보장
  • 대시보드 연동: 로그인, 상태, 아이템 권한, API 키 관리
  • 동적 라우팅: 컨테이너 변화에 따른 자동 프록시 업데이트

MVP 구성 요소

  • 로빙 컨테이너: 10개
  • 리버스 프록시: 1개 (Traefik)
  • 컨테이너 오케스트레이션: Docker Swarm
  • 제어 백엔드: FastAPI 기반
  • 대시보드: React/Vue 프론트엔드

아키텍처 다이어그램

[ User + 대시보드 ]
        ↓ REST/WebSocket
[ Control API Server ]
        ↓            ↓
[ Docker Engine (Swarm) ]  ←→ [ Redis / Postgres ]
        ↑
  [ Reverse Proxy (Traefik) ]
        ↓
   [ 로빙 컨테이너 1~10개 ]

레벨업 동작 흐름

  1. 로빙 경험치 증가 → 레벨업 판정 (PostgreSQL 반영)
  2. Control API가 변화 감지 (cron, hook, event)
  3. 기존 robeing-3 컨테이너 stop + remove
  4. 더 큰 자원 설정으로 재시작 (--memory, --cpu-shares 증가)
  5. 기존 /data 볼륨 마운트 유지 (기억/코드 보존)
  6. Traefik이 자동 라우팅 업데이트

예제 구현

Docker Swarm 기반 구성

# docker-compose.yml
version: '3.8'
services:
  traefik:
    image: traefik:v2.10
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--api.insecure=true"
    ports:
      - "80:80"
      - "8080:8080"  # 관리 대시보드
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    deploy:
      replicas: 1
      resources: 
        limits: 
          cpus: '0.5'
          memory: '512M'

  control-api:
    image: ro-being/control-api:latest
    deploy:
      replicas: 1
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/robeing
      - REDIS_URL=redis://redis:6379/0
      - DOCKER_SOCKET=/var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - traefik-public
    labels:
      - "traefik.http.routers.control-api.rule=Host(`api.robeing.local`)"

  postgres:
    image: postgres:15
    environment:
      - POSTGRES_DB=robeing
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - traefik-public

  redis:
    image: redis:7-alpine
    networks:
      - traefik-public

  # 로빙 컨테이너 템플릿
  robeing-1:
    image: robeing:level-1
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: '256M'
    volumes:
      - robeing-1-memory:/app/memory
      - robeing-1-code:/app/code
    networks:
      - traefik-public
    labels:
      - "traefik.http.routers.robeing1.rule=Host(`robeing1.local`)"
      - "traefik.http.services.robeing1.loadbalancer.server.port=8000"

networks:
  traefik-public:
    external: true

volumes:
  postgres_data:
  robeing-1-memory:
  robeing-1-code:

로빙 컨테이너 제어 API

# control_api.py
import docker
from fastapi import FastAPI
from sqlalchemy.orm import Session
from database import get_db
from models import Robeing

app = FastAPI()
client = docker.from_env()

@app.post("/robeing/{robeing_id}/level-up")
async def level_up_robeing(robeing_id: int, db: Session = Depends(get_db)):
    # 로빙 정보 조회
    robeing = db.query(Robeing).filter(Robeing.id == robeing_id).first()
    if not robeing:
        raise HTTPException(404, "Robeing not found")
    
    # 레벨업 처리
    new_level = robeing.level + 1
    robeing.level = new_level
    db.commit()
    
    # 컨테이너 재시작
    await restart_robeing_container(robeing_id, new_level)
    
    return {"message": f"Robeing {robeing_id} leveled up to {new_level}"}

async def restart_robeing_container(robeing_id: int, level: int):
    container_name = f"robeing-{robeing_id}"
    
    try:
        # 기존 컨테이너 중지 및 제거
        old_container = client.containers.get(container_name)
        old_container.stop()
        old_container.remove()
    except docker.errors.NotFound:
        pass
    
    # 레벨에 따른 자원 할당
    memory_limit = f"{256 * level}m"
    cpu_quota = 100000 * level  # CPU 할당량
    
    # 새 컨테이너 시작
    client.containers.run(
        image=f"robeing:level-{level}",
        name=container_name,
        detach=True,
        mem_limit=memory_limit,
        cpu_quota=cpu_quota,
        volumes={
            f"robeing-{robeing_id}-memory": {
                "bind": "/app/memory",
                "mode": "rw"
            },
            f"robeing-{robeing_id}-code": {
                "bind": "/app/code", 
                "mode": "rw"
            }
        },
        labels={
            f"traefik.http.routers.robeing{robeing_id}.rule": f"Host(`robeing{robeing_id}.local`)",
            f"traefik.http.services.robeing{robeing_id}.loadbalancer.server.port": "8000"
        },
        network="traefik-public"
    )

대시보드 연동 예시

# dashboard_api.py
@app.get("/dashboard/robeing/{robeing_id}/status")
async def get_robeing_status(robeing_id: int):
    container_name = f"robeing-{robeing_id}"
    
    try:
        container = client.containers.get(container_name)
        stats = container.stats(stream=False)
        
        return {
            "id": robeing_id,
            "status": container.status,
            "cpu_usage": calculate_cpu_percent(stats),
            "memory_usage": stats['memory_stats']['usage'],
            "memory_limit": stats['memory_stats']['limit'],
            "level": get_robeing_level(robeing_id),
            "uptime": container.attrs['State']['StartedAt']
        }
    except docker.errors.NotFound:
        return {"status": "not_found"}

@app.post("/dashboard/robeing/{robeing_id}/action")
async def control_robeing(robeing_id: int, action: str):
    container_name = f"robeing-{robeing_id}"
    container = client.containers.get(container_name)
    
    if action == "start":
        container.start()
    elif action == "stop":
        container.stop()
    elif action == "restart":
        container.restart()
    elif action == "pause":
        container.pause()
    elif action == "unpause":
        container.unpause()
    
    return {"message": f"Action {action} applied to robeing {robeing_id}"}

스킬 기반 컨테이너 관리

# 스킬 메타데이터 예시 (skill_metadata.yml)
skills:
  - name: PDF_Parser
    level_required: 5
    docker_image: robeing-skills/pdf-parser:latest
    resources:
      memory: "512m"
      cpu: "0.5"
    auto_shutdown: 3600  # 1시간 후 자동 종료
    
  - name: Advanced_LLM
    level_required: 10
    docker_image: robeing-skills/advanced-llm:latest
    resources:
      memory: "2g"
      cpu: "2.0"
    gpu_required: true

실시간 모니터링

# monitoring.py
import asyncio
import docker
from fastapi import WebSocket

@app.websocket("/ws/monitor/{robeing_id}")
async def monitor_robeing(websocket: WebSocket, robeing_id: int):
    await websocket.accept()
    container_name = f"robeing-{robeing_id}"
    
    try:
        container = client.containers.get(container_name)
        
        # 실시간 stats 스트리밍
        for stats in container.stats(stream=True):
            metrics = {
                "timestamp": datetime.now().isoformat(),
                "cpu_percent": calculate_cpu_percent(stats),
                "memory_usage": stats['memory_stats']['usage'],
                "memory_percent": calculate_memory_percent(stats),
                "network_rx": stats['networks']['eth0']['rx_bytes'],
                "network_tx": stats['networks']['eth0']['tx_bytes']
            }
            
            await websocket.send_json(metrics)
            await asyncio.sleep(1)
            
    except docker.errors.NotFound:
        await websocket.send_json({"error": "Container not found"})
    except Exception as e:
        await websocket.send_json({"error": str(e)})

결론 및 확장 방향

핵심 성과

  1. Docker 이미지 최적화: 1.54GB → 예상 400-500MB로 감소
  2. 동적 컨테이너 관리: 레벨업 시 자동 자원 재할당
  3. 비용 효율성: MVP 기준 월 10만원 이하로 운영 가능
  4. 확장 가능한 구조: Docker Swarm → Kubernetes 전환 가능

향후 확장 계획

  • Kubernetes 전환: StatefulSet + Custom Operator
  • 로빙 간 통신: gRPC 또는 WebSocket 기반 메시지 연동
  • 고급 모니터링: Prometheus + Grafana 연동
  • AI 기반 자원 예측: 사용 패턴 분석으로 선제적 스케일링

검증된 기술 스택

  • 컨테이너: Docker + Docker Swarm
  • 프록시: Traefik (동적 라우팅)
  • 백엔드: FastAPI + PostgreSQL + Redis
  • 프론트엔드: React/Vue (대시보드)
  • 모니터링: Docker API + WebSocket

이 아키텍처를 통해 로빙의 레벨 기반 진화, 기억 지속성, 유연한 스킬 업데이트 요구사항을 모두 충족할 수 있으며, MVP에서 시작하여 단계적으로 확장 가능한 구조를 제공합니다.