이미지 로딩 중...
AI Generated
2025. 11. 8. · 3 Views
RAG 시스템 프로덕션 배포 완벽 가이드
RAG 시스템을 실제 프로덕션 환경에 안전하게 배포하기 위한 실전 가이드입니다. 인프라 구성부터 모니터링, 스케일링, 보안까지 운영에 필요한 모든 것을 다룹니다.
목차
- Docker 컨테이너화 - RAG 애플리케이션 패키징
- Kubernetes 배포 - 오케스트레이션과 자동 스케일링
- 프로메테우스와 그라파나 - 모니터링과 알림
- 로깅과 트레이싱 - 분산 추적과 디버깅
- 시크릿 관리 - 환경 변수와 보안
- CI/CD 파이프라인 - 자동화된 배포
- 로드 밸런싱과 인그레스 - 트래픽 관리
1. Docker 컨테이너화 - RAG 애플리케이션 패키징
시작하며
여러분이 로컬에서 완벽하게 작동하던 RAG 시스템을 서버에 배포하려고 할 때 이런 상황을 겪어본 적 있나요? 의존성 버전이 맞지 않아 임베딩 모델이 로드되지 않거나, 환경 변수 설정이 꼬여서 벡터 DB 연결이 실패하는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 개발 환경과 프로덕션 환경의 차이, 팀원들 간의 환경 불일치, 그리고 복잡한 ML 라이브러리 의존성 관리가 주요 원인입니다.
이로 인해 배포 시간이 길어지고 예상치 못한 장애가 발생할 수 있습니다. 바로 이럴 때 필요한 것이 Docker 컨테이너화입니다.
Docker를 사용하면 RAG 시스템의 모든 의존성을 하나의 이미지로 패키징하여 어떤 환경에서도 동일하게 실행할 수 있습니다.
개요
간단히 말해서, Docker 컨테이너화는 여러분의 RAG 애플리케이션과 모든 의존성을 하나의 독립적인 실행 환경으로 묶는 것입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, RAG 시스템은 특히 PyTorch, Transformers, FAISS, ChromaDB 등 다양한 ML 라이브러리를 사용하기 때문에 의존성 관리가 복잡합니다.
예를 들어, GPU 버전의 PyTorch를 사용하는 경우 CUDA 버전까지 정확히 맞춰야 하는데, Docker를 사용하면 이 모든 것을 한 번에 패키징할 수 있습니다. 전통적인 방법과의 비교를 해보면, 기존에는 서버에 직접 Python과 라이브러리를 설치하고 환경 변수를 수동으로 설정했다면, 이제는 한 번 만든 Docker 이미지를 어디서든 실행만 하면 됩니다.
Docker의 핵심 특징은 환경 일관성 보장, 격리된 실행 환경 제공, 그리고 빠른 배포와 롤백입니다. 이러한 특징들이 RAG 시스템처럼 복잡한 ML 파이프라인을 운영할 때 안정성과 효율성을 크게 높여줍니다.
코드 예제
# Dockerfile - RAG 애플리케이션을 위한 멀티스테이지 빌드
FROM python:3.11-slim as builder
# 의존성 설치를 위한 빌드 도구
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc g++
# 의존성을 먼저 설치하여 캐시 활용
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# 실제 런타임 이미지
FROM python:3.11-slim
# 빌더에서 설치한 Python 패키지 복사
COPY --from=builder /root/.local /root/.local
# 애플리케이션 코드 복사
WORKDIR /app
COPY . .
# PATH에 로컬 bin 추가
ENV PATH=/root/.local/bin:$PATH
# 헬스체크 엔드포인트 설정
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
# 비root 유저로 실행 (보안)
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
# 애플리케이션 실행
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
설명
이것이 하는 일: 이 Dockerfile은 RAG 애플리케이션을 프로덕션 환경에서 실행할 수 있는 최적화된 Docker 이미지로 만듭니다. 멀티스테이지 빌드를 통해 이미지 크기를 줄이고, 보안을 강화하며, 효율적인 캐싱을 활용합니다.
첫 번째로, builder 스테이지에서는 Python 패키지를 컴파일하고 설치합니다. requirements.txt를 먼저 복사하여 의존성 설치 단계를 캐싱하므로, 코드만 변경했을 때는 의존성을 다시 설치하지 않아 빌드 시간이 크게 단축됩니다.
--no-cache-dir 옵션으로 pip 캐시를 저장하지 않아 이미지 크기를 줄입니다. 그 다음으로, 실제 런타임 이미지에서는 빌더에서 설치한 패키지만 복사해옵니다.
이렇게 하면 gcc, g++ 같은 빌드 도구가 최종 이미지에 포함되지 않아 이미지 크기가 수백 MB 줄어들고 공격 표면도 감소합니다. HEALTHCHECK 명령으로 컨테이너의 건강 상태를 주기적으로 확인하여 Kubernetes나 Docker Swarm이 자동으로 비정상 컨테이너를 재시작할 수 있습니다.
마지막으로, 보안을 위해 비root 유저로 애플리케이션을 실행합니다. 만약 컨테이너 내부에서 취약점이 악용되더라도 호스트 시스템으로의 권한 상승을 방지할 수 있습니다.
uvicorn으로 FastAPI 애플리케이션을 실행하며, 0.0.0.0으로 바인딩하여 컨테이너 외부에서 접근할 수 있게 합니다. 여러분이 이 코드를 사용하면 약 500MB 이하의 최적화된 이미지를 만들 수 있으며, 빌드 시간도 캐싱으로 인해 2-3배 빨라집니다.
또한 HEALTHCHECK를 통해 오케스트레이션 도구가 자동으로 장애를 감지하고 복구할 수 있어 시스템 안정성이 크게 향상됩니다.
실전 팁
💡 .dockerignore 파일을 반드시 사용하세요. pycache, .git, tests, .env 같은 불필요한 파일을 제외하면 빌드가 빨라지고 이미지 크기도 줄어듭니다.
💡 큰 ML 모델 파일은 이미지에 포함하지 말고 볼륨 마운트나 S3에서 다운로드하세요. 그렇지 않으면 이미지가 수 GB가 되어 배포가 느려집니다.
💡 requirements.txt를 base, dev, prod로 분리하여 프로덕션에는 필요한 패키지만 설치하세요. pytest, black 같은 개발 도구는 prod 이미지에서 제외해야 합니다.
💡 Docker 빌드 시 --build-arg를 사용해 민감한 정보를 전달하지 마세요. 빌드 히스토리에 남아 보안 위험이 됩니다. 대신 런타임에 환경 변수나 시크릿을 주입하세요.
💡 멀티 아키텍처 지원이 필요하면 docker buildx를 사용해 ARM64와 AMD64 이미지를 동시에 빌드하세요. M1 Mac과 AWS Graviton 인스턴스 모두에서 사용할 수 있습니다.
2. Kubernetes 배포 - 오케스트레이션과 자동 스케일링
시작하며
여러분이 RAG 서비스를 운영하다가 갑자기 트래픽이 10배 증가했을 때 이런 상황을 겪어본 적 있나요? 서버가 과부하로 다운되고, 수동으로 인스턴스를 추가하려니 시간이 너무 오래 걸려 사용자들이 타임아웃 에러를 겪는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 RAG 시스템은 임베딩 생성과 벡터 검색이 CPU/GPU 집약적이기 때문에 트래픽 변화에 따라 리소스를 동적으로 조절해야 합니다.
수동 관리로는 빠른 대응이 불가능하고, 장애 발생 시 복구도 느립니다. 바로 이럴 때 필요한 것이 Kubernetes 오케스트레이션입니다.
Kubernetes를 사용하면 트래픽에 따라 자동으로 Pod를 늘리거나 줄이고, 장애가 발생한 컨테이너를 즉시 재시작하며, 무중단 배포를 쉽게 구현할 수 있습니다.
개요
간단히 말해서, Kubernetes는 여러분의 컨테이너화된 RAG 애플리케이션을 자동으로 배포하고, 스케일링하고, 관리해주는 오케스트레이션 플랫폼입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, RAG 서비스는 사용량 패턴이 불규칙합니다.
업무 시간에는 트래픽이 많고 심야에는 적을 수 있는데, Kubernetes의 Horizontal Pod Autoscaler(HPA)를 사용하면 CPU/메모리 사용률에 따라 자동으로 인스턴스를 조절하여 비용을 최적화할 수 있습니다. 예를 들어, 평균 CPU 사용률 70%를 유지하도록 설정하면 트래픽이 증가할 때 자동으로 Pod가 추가되고, 감소하면 줄어듭니다.
전통적인 방법과의 비교를 해보면, 기존에는 VM에 직접 배포하고 로드 밸런서를 수동으로 설정했다면, 이제는 YAML 매니페스트 하나로 배포, 서비스 노출, 오토스케일링을 모두 선언적으로 정의할 수 있습니다. Kubernetes의 핵심 특징은 자동 복구(Self-healing), 수평 스케일링, 롤링 업데이트, 그리고 서비스 디스커버리입니다.
이러한 특징들이 RAG 시스템의 가용성을 99.9% 이상으로 유지하고, 배포 리스크를 최소화하며, 운영 부담을 크게 줄여줍니다.
코드 예제
# kubernetes/deployment.yaml - RAG 서비스 배포 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-service
labels:
app: rag-service
spec:
replicas: 3 # 초기 Pod 개수
strategy:
type: RollingUpdate # 무중단 배포
rollingUpdate:
maxSurge: 1 # 업데이트 시 추가 생성 가능한 Pod
maxUnavailable: 0 # 업데이트 중 항상 가용한 상태 유지
selector:
matchLabels:
app: rag-service
template:
metadata:
labels:
app: rag-service
spec:
containers:
- name: rag-api
image: myregistry/rag-service:v1.2.0
ports:
- containerPort: 8000
env:
- name: VECTOR_DB_URL
valueFrom:
secretKeyRef: # 민감 정보는 Secret에서 주입
name: rag-secrets
key: vector-db-url
resources:
requests: # 최소 필요 리소스
memory: "2Gi"
cpu: "1000m"
limits: # 최대 사용 가능 리소스
memory: "4Gi"
cpu: "2000m"
livenessProbe: # 컨테이너 생존 확인
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe: # 트래픽 수신 준비 확인
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
---
# HPA 설정 - 자동 스케일링
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: rag-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: rag-service
minReplicas: 3 # 최소 Pod 개수
maxReplicas: 20 # 최대 Pod 개수
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 70% 유지 목표
설명
이것이 하는 일: 이 Kubernetes 매니페스트는 RAG 서비스를 고가용성으로 배포하고, 트래픽에 따라 자동으로 스케일링하며, 장애 발생 시 자동으로 복구하는 설정을 정의합니다. 첫 번째로, Deployment 리소스는 3개의 Pod 레플리카로 시작하여 트래픽을 분산시킵니다.
RollingUpdate 전략을 사용하면 새 버전을 배포할 때 기존 Pod를 하나씩 교체하므로 서비스 중단 없이 업데이트할 수 있습니다. maxUnavailable: 0으로 설정하여 업데이트 중에도 항상 3개의 Pod가 실행 중임을 보장합니다.
만약 새 버전에 문제가 있으면 readinessProbe가 실패하여 트래픽이 라우팅되지 않고, kubectl rollout undo로 즉시 이전 버전으로 롤백할 수 있습니다. 그 다음으로, resources 설정으로 각 Pod의 리소스를 제한합니다.
requests는 Kubernetes 스케줄러가 Pod를 배치할 노드를 선택할 때 사용하고, limits는 Pod가 초과 사용하지 못하도록 제한합니다. RAG 시스템은 임베딩 생성 시 메모리를 많이 사용하므로 충분한 메모리를 할당해야 OOM(Out of Memory) 에러를 방지할 수 있습니다.
livenessProbe는 Pod가 응답하지 않으면 재시작하고, readinessProbe는 초기화가 완료될 때까지 트래픽을 보내지 않습니다. 세 번째로, HorizontalPodAutoscaler는 CPU 사용률을 모니터링하여 자동으로 Pod 개수를 조절합니다.
CPU 사용률이 70%를 초과하면 Pod를 추가하고, 낮아지면 줄입니다. 예를 들어, 갑자기 사용자가 많아져서 평균 CPU가 90%까지 올라가면 HPA가 자동으로 Pod를 6개, 9개로 늘려 부하를 분산시킵니다.
트래픽이 줄어들면 다시 3개로 축소하여 비용을 절감합니다. 여러분이 이 설정을 사용하면 수동 개입 없이 트래픽 변화에 대응할 수 있고, 하드웨어 장애나 소프트웨어 버그로 Pod가 죽어도 Kubernetes가 자동으로 재시작하여 다운타임을 최소화합니다.
또한 배포 시 카나리 배포나 블루-그린 배포를 쉽게 구현하여 프로덕션 리스크를 줄일 수 있습니다.
실전 팁
💡 PodDisruptionBudget(PDB)를 설정하여 자발적 중단 시 최소 가용 Pod 개수를 보장하세요. 예를 들어 minAvailable: 2로 설정하면 노드 유지보수 시에도 항상 2개 이상의 Pod가 실행됩니다.
💡 Affinity와 Anti-affinity를 사용해 Pod를 여러 가용 영역(AZ)에 분산 배치하세요. 한 AZ가 다운되어도 서비스가 계속 동작합니다.
💡 HPA에 메모리 메트릭도 추가하세요. RAG 시스템은 CPU뿐만 아니라 메모리 사용량도 높으므로 둘 다 모니터링해야 정확한 스케일링이 가능합니다.
💡 ConfigMap을 사용해 애플리케이션 설정을 컨테이너와 분리하세요. 프롬프트 템플릿이나 모델 설정을 ConfigMap에 저장하면 이미지 재빌드 없이 설정을 변경할 수 있습니다.
💡 Namespace를 사용해 dev, staging, prod 환경을 분리하고, ResourceQuota로 각 환경의 리소스 사용량을 제한하세요. 개발 환경이 프로덕션 리소스를 잠식하는 것을 방지합니다.
3. 프로메테우스와 그라파나 - 모니터링과 알림
시작하며
여러분이 RAG 서비스를 운영하는데 갑자기 응답 시간이 느려지거나 오류율이 증가할 때 이런 상황을 겪어본 적 있나요? 사용자들이 불만을 제기하고 나서야 문제를 발견하고, 원인을 찾기 위해 로그를 뒤지느라 몇 시간을 허비하는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. RAG 시스템은 LLM API 호출, 벡터 검색, 임베딩 생성 등 여러 단계를 거치므로 어느 단계에서 병목이 발생했는지 파악하기 어렵습니다.
실시간 모니터링 없이는 문제가 심각해진 후에야 대응하게 되어 사용자 경험이 나빠지고 신뢰도가 떨어집니다. 바로 이럴 때 필요한 것이 프로메테우스와 그라파나를 활용한 모니터링 시스템입니다.
이를 사용하면 시스템의 모든 메트릭을 실시간으로 수집하고 시각화하여 문제를 조기에 발견하고, 임계값을 초과하면 자동으로 알림을 받을 수 있습니다.
개요
간단히 말해서, 프로메테우스는 시계열 메트릭을 수집하고 저장하는 모니터링 시스템이고, 그라파나는 이 메트릭을 시각화하고 알림을 설정하는 대시보드 도구입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, RAG 시스템의 성능과 안정성을 유지하려면 요청 처리 시간, 벡터 검색 레이턴시, LLM API 오류율, 메모리 사용량 등 다양한 메트릭을 지속적으로 모니터링해야 합니다.
예를 들어, 벡터 검색 시간이 평소 100ms에서 갑자기 500ms로 증가하면 인덱스에 문제가 생긴 것이므로 즉시 조치를 취해야 합니다. 프로메테우스는 이런 메트릭을 15초마다 수집하여 이상 징후를 실시간으로 감지합니다.
전통적인 방법과의 비교를 해보면, 기존에는 로그 파일을 수동으로 분석하거나 애플리케이션 내부에 간단한 카운터만 두었다면, 이제는 표준화된 메트릭 포맷(OpenMetrics)으로 데이터를 수집하고 PromQL이라는 강력한 쿼리 언어로 복잡한 분석을 수행할 수 있습니다. 프로메테우스의 핵심 특징은 Pull 기반 메트릭 수집, 다차원 데이터 모델, 그리고 Alertmanager를 통한 유연한 알림 라우팅입니다.
그라파나는 아름다운 대시보드, 다양한 데이터 소스 지원, 그리고 팀 협업 기능을 제공합니다. 이러한 특징들이 RAG 시스템의 관찰성(Observability)을 극대화하여 문제 발견부터 해결까지의 시간(MTTR)을 크게 단축시킵니다.
코드 예제
# main.py - FastAPI 애플리케이션에 프로메테우스 메트릭 추가
from fastapi import FastAPI
from prometheus_client import Counter, Histogram, Gauge, generate_latest
from prometheus_client import CONTENT_TYPE_LATEST
from fastapi.responses import Response
import time
app = FastAPI()
# 메트릭 정의
request_count = Counter(
'rag_requests_total',
'Total RAG requests',
['endpoint', 'status'] # 레이블로 세분화
)
response_time = Histogram(
'rag_response_seconds',
'Response time in seconds',
['endpoint'],
buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0] # 레이턴시 분포 확인
)
vector_search_time = Histogram(
'vector_search_seconds',
'Vector search latency',
buckets=[0.01, 0.05, 0.1, 0.2, 0.5]
)
active_requests = Gauge(
'rag_active_requests',
'Number of requests currently being processed'
)
@app.post("/query")
async def query_rag(question: str):
active_requests.inc() # 활성 요청 증가
start_time = time.time()
try:
# 벡터 검색 시간 측정
search_start = time.time()
results = await vector_db.search(question)
vector_search_time.observe(time.time() - search_start)
# LLM 호출 및 응답 생성
answer = await generate_answer(results, question)
request_count.labels(endpoint='/query', status='success').inc()
return {"answer": answer}
except Exception as e:
request_count.labels(endpoint='/query', status='error').inc()
raise
finally:
response_time.labels(endpoint='/query').observe(time.time() - start_time)
active_requests.dec() # 활성 요청 감소
# 프로메테우스 메트릭 엔드포인트
@app.get("/metrics")
async def metrics():
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
설명
이것이 하는 일: 이 코드는 FastAPI 애플리케이션에 프로메테우스 메트릭을 추가하여 RAG 시스템의 요청 수, 응답 시간, 벡터 검색 레이턴시, 동시 요청 수를 추적할 수 있게 합니다. 첫 번째로, Counter, Histogram, Gauge라는 세 가지 메트릭 타입을 정의합니다.
Counter는 누적 값을 추적하므로 전체 요청 수나 에러 발생 횟수를 세는 데 사용합니다. labels를 사용하여 엔드포인트별, 상태별로 메트릭을 분리하면 어떤 API에서 에러가 많이 발생하는지 정확히 파악할 수 있습니다.
Histogram은 값의 분포를 측정하므로 응답 시간의 백분위수(P50, P95, P99)를 계산할 수 있습니다. 예를 들어, 평균 응답 시간은 낮아도 P99가 높다면 일부 사용자가 매우 느린 응답을 경험하고 있다는 의미입니다.
그 다음으로, 실제 요청 처리 과정에서 메트릭을 기록합니다. active_requests.inc()로 요청 시작 시 동시 요청 수를 증가시키고, finally 블록에서 dec()로 감소시켜 항상 정확한 값을 유지합니다.
vector_search_time을 별도로 측정하여 전체 응답 시간 중 벡터 검색이 차지하는 비율을 알 수 있습니다. 만약 벡터 검색 시간이 전체의 80%를 차지한다면 인덱스 최적화나 하드웨어 업그레이드가 필요하다는 것을 알 수 있습니다.
세 번째로, /metrics 엔드포인트를 통해 프로메테우스가 메트릭을 스크랩할 수 있게 합니다. 프로메테우스는 이 엔드포인트를 15초마다 호출하여 최신 메트릭을 수집합니다.
수집된 데이터는 시계열 데이터베이스에 저장되어 PromQL로 쿼리할 수 있습니다. 예를 들어, rate(rag_requests_total[5m])는 지난 5분간의 초당 요청 수를 계산하고, histogram_quantile(0.99, rag_response_seconds_bucket)는 P99 레이턴시를 구합니다.
여러분이 이 코드를 사용하면 그라파나 대시보드에서 실시간으로 시스템 상태를 모니터링할 수 있습니다. 에러율이 5%를 초과하거나 P95 레이턴시가 3초를 넘으면 Slack이나 PagerDuty로 알림을 받도록 설정하여 문제를 조기에 발견하고 대응할 수 있습니다.
또한 과거 데이터를 분석하여 트래픽 패턴을 파악하고 용량 계획을 수립할 수 있습니다.
실전 팁
💡 비즈니스 메트릭도 함께 수집하세요. 기술 메트릭 외에 '답변 만족도', '검색된 문서 수', '사용된 토큰 수' 같은 비즈니스 지표를 추적하면 ROI를 측정하고 개선 방향을 찾을 수 있습니다.
💡 Alertmanager에서 알림 그룹화와 억제(inhibition)를 설정하세요. 한 번에 100개의 알림을 받는 대신, 관련 알림을 그룹화하고 상위 알림이 발생하면 하위 알림을 억제하여 알림 피로도를 줄입니다.
💡 Recording rules를 사용해 자주 사용하는 복잡한 쿼리를 사전 계산하세요. 예를 들어, P99 레이턴시 계산을 매번 하는 대신 1분마다 미리 계산해두면 대시보드 로딩이 빨라집니다.
💡 메트릭 카디널리티를 조심하세요. 사용자 ID나 요청 ID를 레이블로 사용하면 메트릭 개수가 폭발적으로 증가해 프로메테우스 메모리가 부족해집니다. 레이블은 카디널리티가 낮은 값(엔드포인트, 상태 코드 등)만 사용하세요.
💡 그라파나 대시보드를 JSON으로 버전 관리하세요. Git에 저장하면 대시보드 변경 이력을 추적하고, 실수로 삭제해도 복구할 수 있으며, 팀원들과 공유하기 쉽습니다.
4. 로깅과 트레이싱 - 분산 추적과 디버깅
시작하며
여러분이 RAG 시스템에서 특정 요청이 실패했을 때 이런 상황을 겪어본 적 있나요? 에러 로그는 있는데 그 요청이 벡터 DB, LLM API, 캐시 레이어 중 어디서 실패했는지 알 수 없어서 각 컴포넌트의 로그를 일일이 뒤지며 타임스탬프를 맞춰가며 추적하는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. RAG 시스템은 여러 마이크로서비스와 외부 API가 연계되어 작동하므로 하나의 사용자 요청이 수십 개의 내부 호출로 분산됩니다.
전통적인 로그만으로는 요청의 전체 흐름을 파악하기 어렵고, 병목이나 실패 지점을 찾는 데 많은 시간이 소요됩니다. 바로 이럴 때 필요한 것이 구조화된 로깅과 분산 트레이싱입니다.
구조화된 로깅으로 로그를 쉽게 검색하고 필터링할 수 있으며, 분산 트레이싱으로 하나의 요청이 시스템을 통과하는 전체 경로를 시각화하여 문제를 빠르게 진단할 수 있습니다.
개요
간단히 말해서, 구조화된 로깅은 로그를 사람이 읽는 텍스트가 아닌 JSON 같은 구조화된 포맷으로 기록하는 것이고, 분산 트레이싱은 요청이 여러 서비스를 거치는 전체 흐름을 추적하는 기술입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, RAG 시스템은 사용자 질문 → 임베딩 생성 → 벡터 검색 → 컨텍스트 구성 → LLM 호출 → 응답 생성이라는 복잡한 파이프라인을 거칩니다.
각 단계에서 발생하는 로그에 trace_id와 span_id를 포함시키면 흩어진 로그들을 하나의 요청으로 묶어서 볼 수 있습니다. 예를 들어, OpenTelemetry를 사용하면 Jaeger 같은 트레이싱 백엔드에서 요청의 폭포수 차트(waterfall chart)를 보고 어느 단계가 느린지 한눈에 파악할 수 있습니다.
전통적인 방법과의 비교를 해보면, 기존에는 "Error: Vector search failed"처럼 단순한 문자열 로그를 남겼다면, 이제는 {"level": "error", "service": "vector-search", "trace_id": "abc123", "duration_ms": 523, "error": "timeout"}처럼 JSON으로 기록하여 ELK 스택이나 Loki에서 쉽게 쿼리하고 집계할 수 있습니다. 구조화된 로깅의 핵심 특징은 일관된 포맷, 컨텍스트 정보 포함, 그리고 자동화된 분석입니다.
분산 트레이싱의 핵심 특징은 요청 전체 경로 시각화, 각 단계의 레이턴시 측정, 그리고 에러 전파 추적입니다. 이러한 특징들이 복잡한 RAG 시스템의 문제를 몇 분 안에 찾아내고 성능 병목을 정확히 파악할 수 있게 해줍니다.
코드 예제
# logging_config.py - 구조화된 로깅과 OpenTelemetry 트레이싱
import structlog
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
# OpenTelemetry 설정
trace.set_tracer_provider(TracerProvider())
jaeger_exporter = JaegerExporter(
agent_host_name="jaeger", # Kubernetes 서비스명
agent_port=6831,
)
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(jaeger_exporter)
)
# 구조화된 로깅 설정
structlog.configure(
processors=[
structlog.stdlib.add_log_level, # 로그 레벨 추가
structlog.processors.TimeStamper(fmt="iso"), # ISO 타임스탬프
structlog.processors.StackInfoRenderer(), # 스택 트레이스
structlog.processors.format_exc_info, # 예외 정보 포맷팅
structlog.processors.JSONRenderer() # JSON 출력
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
)
logger = structlog.get_logger()
# FastAPI 애플리케이션에 자동 트레이싱 추가
from fastapi import FastAPI
app = FastAPI()
FastAPIInstrumentor.instrument_app(app) # 모든 엔드포인트 자동 추적
@app.post("/query")
async def query_rag(question: str):
tracer = trace.get_tracer(__name__)
# 커스텀 span으로 벡터 검색 추적
with tracer.start_as_current_span("vector_search") as span:
span.set_attribute("question_length", len(question))
logger.info(
"starting_vector_search",
question=question[:50], # 질문 일부만 로그
trace_id=format(span.get_span_context().trace_id, '032x')
)
results = await vector_db.search(question)
span.set_attribute("results_count", len(results))
# LLM 호출 추적
with tracer.start_as_current_span("llm_generation") as span:
answer = await llm.generate(results, question)
span.set_attribute("answer_length", len(answer))
logger.info(
"generation_complete",
results_used=len(results),
answer_length=len(answer),
trace_id=format(span.get_span_context().trace_id, '032x')
)
return {"answer": answer}
설명
이것이 하는 일: 이 코드는 OpenTelemetry를 사용하여 RAG 시스템의 모든 요청을 추적하고, structlog로 구조화된 로그를 JSON 형식으로 기록하여 관찰성을 극대화합니다. 첫 번째로, OpenTelemetry Tracer를 설정하여 Jaeger 백엔드로 trace 데이터를 전송합니다.
FastAPIInstrumentor는 모든 HTTP 요청에 자동으로 trace를 생성하므로 코드 수정 없이 기본적인 추적이 가능합니다. 하지만 RAG 시스템의 내부 동작을 상세히 보려면 커스텀 span을 추가해야 합니다.
tracer.start_as_current_span()으로 벡터 검색과 LLM 호출을 별도 span으로 만들면 Jaeger UI에서 각 단계의 시작 시간, 종료 시간, 소요 시간을 시각적으로 확인할 수 있습니다. 그 다음으로, span에 attributes를 추가하여 컨텍스트 정보를 기록합니다.
question_length, results_count, answer_length 같은 메타데이터를 저장하면 느린 요청을 분석할 때 "질문이 길어서 느렸는지, 검색 결과가 많아서 느렸는지" 같은 인사이트를 얻을 수 있습니다. 예를 들어, 평균 응답 시간이 1초인데 어떤 요청은 10초가 걸렸다면 Jaeger에서 해당 trace를 열어보고 어느 span이 오래 걸렸는지 확인하면 됩니다.
세 번째로, structlog로 구조화된 로그를 기록합니다. trace_id를 로그에 포함시키면 Grafana Loki나 Elasticsearch에서 특정 trace의 모든 로그를 한 번에 찾을 수 있습니다.
JSON 형식으로 로그를 저장하면 "지난 1시간 동안 results_count가 0인 요청 개수"처럼 복잡한 쿼리를 쉽게 실행할 수 있습니다. 또한 structlog의 bind() 메서드로 user_id나 session_id를 컨텍스트에 추가하면 해당 사용자의 모든 로그를 추적할 수 있습니다.
여러분이 이 코드를 사용하면 프로덕션 환경에서 에러가 발생했을 때 Jaeger에서 trace를 보고 정확히 어느 단계에서 실패했는지 몇 초 안에 파악할 수 있습니다. 예를 들어, 벡터 검색 span이 500ms인데 LLM 호출 span이 5초라면 LLM API에 문제가 있다는 것을 즉시 알 수 있습니다.
또한 trace_id로 연관된 모든 로그를 모아서 보면 요청의 전체 스토리를 이해할 수 있어 디버깅 시간이 몇 시간에서 몇 분으로 단축됩니다.
실전 팁
💡 민감한 정보를 로그에 기록하지 마세요. 사용자 질문 전체나 LLM 응답을 로그에 남기면 개인정보 유출 위험이 있습니다. 질문 길이나 해시값만 기록하거나, 로그 필터링으로 자동 마스킹하세요.
💡 샘플링을 적절히 설정하세요. 모든 요청을 추적하면 Jaeger 저장소가 금방 차고 네트워크 오버헤드도 큽니다. 에러 요청은 100% 추적하고, 정상 요청은 1-10%만 샘플링하세요.
💡 로그 레벨을 환경별로 다르게 설정하세요. 개발 환경에서는 DEBUG 레벨로 상세히 로깅하고, 프로덕션에서는 INFO 레벨로 필요한 것만 기록하여 로그 볼륨과 비용을 줄이세요.
💡 Baggage를 사용해 trace 간에 컨텍스트를 전달하세요. 예를 들어, A/B 테스트 그룹 정보를 baggage에 넣으면 모든 하위 서비스에서 해당 정보를 사용할 수 있습니다.
💡 로그 집계 도구(Loki, Elasticsearch)를 사용해 여러 Pod의 로그를 중앙화하세요. Pod가 재시작되면 로컬 로그는 사라지므로 중앙 저장소에 로그를 전송해야 장애 후 분석이 가능합니다.
5. 시크릿 관리 - 환경 변수와 보안
시작하며
여러분이 RAG 시스템을 배포할 때 이런 상황을 겪어본 적 있나요? OpenAI API 키를 코드에 하드코딩하거나 .env 파일에 넣어서 실수로 Git에 커밋하여 GitHub에 노출되고, 며칠 후 수백 달러의 무단 사용 요금 청구를 받는 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. RAG 시스템은 LLM API 키, 벡터 DB 접속 정보, 데이터베이스 비밀번호 등 많은 민감한 정보를 사용합니다.
이런 시크릿이 코드나 Docker 이미지에 포함되면 보안 위험이 크고, 키를 변경할 때마다 코드를 수정하고 재배포해야 하는 불편함도 있습니다. 바로 이럴 때 필요한 것이 Kubernetes Secrets와 외부 시크릿 관리 도구입니다.
이를 사용하면 민감한 정보를 코드와 분리하여 안전하게 저장하고, 런타임에 동적으로 주입하며, 접근 권한을 세밀하게 제어할 수 있습니다.
개요
간단히 말해서, 시크릿 관리는 API 키, 비밀번호, 인증서 같은 민감한 정보를 안전하게 저장하고 애플리케이션에 전달하는 프로세스입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 프로덕션 환경에서는 개발자도 실제 API 키를 모르는 것이 이상적입니다.
DevOps나 보안 팀만 HashiCorp Vault나 AWS Secrets Manager에 시크릿을 저장하고, 애플리케이션은 런타임에 환경 변수로 받아서 사용합니다. 예를 들어, OpenAI API 키를 변경해야 할 때 코드를 수정할 필요 없이 Vault에서 값만 업데이트하면 되므로 배포 리스크가 없고 키 순환(rotation)도 쉽습니다.
전통적인 방법과의 비교를 해보면, 기존에는 .env 파일을 서버에 수동으로 복사하거나 환경 변수를 셸 스크립트에 하드코딩했다면, 이제는 External Secrets Operator를 사용해 Kubernetes가 자동으로 Vault에서 시크릿을 가져와서 Pod에 주입할 수 있습니다. 시크릿 관리의 핵심 특징은 암호화된 저장, 접근 제어, 감사 로깅, 그리고 자동 순환입니다.
이러한 특징들이 RAG 시스템의 보안을 강화하고, 규제 준수(SOC2, GDPR 등)를 용이하게 하며, 키 노출 사고 발생 시 영향 범위를 최소화합니다.
코드 예제
# kubernetes/secrets.yaml - Kubernetes Secret과 External Secrets
# 방법 1: Kubernetes Native Secret (개발/테스트용)
apiVersion: v1
kind: Secret
metadata:
name: rag-secrets
type: Opaque
stringData: # base64 인코딩 자동 처리
OPENAI_API_KEY: "sk-proj-..."
VECTOR_DB_PASSWORD: "secure_password_123"
---
# 방법 2: External Secrets Operator (프로덕션 권장)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rag-secrets
spec:
refreshInterval: 1h # 1시간마다 Vault에서 최신 값 가져오기
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: rag-secrets # 생성될 Kubernetes Secret 이름
creationPolicy: Owner
data:
- secretKey: OPENAI_API_KEY # Kubernetes Secret의 키
remoteRef:
key: rag/production # Vault 경로
property: openai_api_key # Vault 내 속성
- secretKey: VECTOR_DB_PASSWORD
remoteRef:
key: rag/production
property: vector_db_password
---
# SecretStore 설정 - Vault 연결 정보
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.company.com"
path: "secret" # Vault의 마운트 경로
version: "v2"
auth:
kubernetes: # Kubernetes 서비스 어카운트로 인증
mountPath: "kubernetes"
role: "rag-service"
serviceAccountRef:
name: rag-service-sa
---
# Deployment에서 Secret 사용
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-service
spec:
template:
spec:
serviceAccountName: rag-service-sa # Vault 인증용
containers:
- name: rag-api
image: myregistry/rag-service:v1.0.0
env:
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: rag-secrets # Secret 이름
key: OPENAI_API_KEY # Secret 내 키
# 또는 전체 Secret을 파일로 마운트
volumeMounts:
- name: secrets
mountPath: /app/secrets
readOnly: true
volumes:
- name: secrets
secret:
secretName: rag-secrets
설명
이것이 하는 일: 이 설정은 RAG 시스템의 모든 민감한 정보를 Kubernetes Secret으로 관리하되, External Secrets Operator를 통해 HashiCorp Vault 같은 외부 시크릿 저장소와 동기화하여 보안을 강화합니다. 첫 번째로, Kubernetes Native Secret은 간단하지만 etcd에 base64로만 인코딩되어 저장되므로 암호화가 약합니다.
개발 환경에서는 괜찮지만 프로덕션에서는 etcd 암호화를 활성화하거나 External Secrets를 사용해야 합니다. stringData를 사용하면 평문으로 작성해도 Kubernetes가 자동으로 base64 인코딩하므로 편리합니다.
하지만 이 YAML 파일을 Git에 커밋하면 안 되며, CI/CD 파이프라인에서 동적으로 생성하거나 Sealed Secrets로 암호화해야 합니다. 그 다음으로, External Secrets Operator는 Vault에 저장된 시크릿을 주기적으로 가져와 Kubernetes Secret을 자동 업데이트합니다.
refreshInterval: 1h로 설정하면 1시간마다 최신 값을 확인하므로, Vault에서 API 키를 변경하면 최대 1시간 내에 모든 Pod가 새 키를 사용하게 됩니다. 이를 통해 키 순환 정책(예: 90일마다 변경)을 쉽게 시행할 수 있습니다.
Vault는 접근 로그를 기록하므로 누가 언제 어떤 시크릿에 접근했는지 감사할 수 있습니다. 세 번째로, Deployment에서 Secret을 사용하는 방법은 두 가지입니다.
환경 변수로 주입하면 코드에서 os.getenv("OPENAI_API_KEY")로 간단히 읽을 수 있지만, 환경 변수는 프로세스 목록에서 보일 수 있어 보안상 약간 불리합니다. 파일로 마운트하면 /app/secrets/OPENAI_API_KEY 파일을 읽어야 하지만 더 안전하고, 시크릿이 업데이트될 때 파일 내용도 자동으로 바뀌므로 애플리케이션이 재시작 없이 새 값을 읽을 수 있습니다(파일 감시 구현 필요).
여러분이 이 설정을 사용하면 개발자가 프로덕션 API 키를 모르고도 개발할 수 있고, 키가 노출되어도 Vault에서 즉시 무효화하고 새 키로 교체할 수 있습니다. 또한 RBAC으로 특정 서비스 어카운트만 특정 시크릿에 접근하도록 제한하여 최소 권한 원칙을 구현할 수 있습니다.
감사 로그를 통해 보안 사고 발생 시 영향 범위를 정확히 파악할 수 있습니다.
실전 팁
💡 Sealed Secrets를 사용하면 암호화된 Secret을 Git에 안전하게 커밋할 수 있습니다. kubeseal로 암호화하면 클러스터 내에서만 복호화할 수 있어 GitOps 워크플로우에 적합합니다.
💡 시크릿을 절대 로그에 출력하지 마세요. print(f"Using API key: {api_key}") 같은 코드는 위험합니다. 대신 api_key[:8] + "..." 같이 일부만 마스킹하여 디버깅하세요.
💡 개발, 스테이징, 프로덕션 환경별로 다른 Vault 네임스페이스나 AWS Secrets Manager 시크릿을 사용하세요. 개발 환경에서 실수로 프로덕션 키를 사용하는 것을 방지합니다.
💡 시크릿 순환 시 애플리케이션이 새 값을 동적으로 로드하도록 구현하세요. 파일 감시나 SIGHUP 시그널로 설정을 다시 읽으면 Pod 재시작 없이 키를 업데이트할 수 있습니다.
💡 AWS에서는 IAM Roles for Service Accounts(IRSA)를 사용해 Pod에 AWS 권한을 부여하세요. Secret에 AWS credentials를 저장하는 대신 Pod가 자동으로 임시 자격 증명을 받아 Secrets Manager나 S3에 접근할 수 있습니다.
6. CI/CD 파이프라인 - 자동화된 배포
시작하며
여러분이 RAG 시스템에 새 기능을 추가하거나 버그를 수정할 때 이런 상황을 겪어본 적 있나요? 코드를 푸시한 후 수동으로 Docker 이미지를 빌드하고, 레지스트리에 푸시하고, kubectl apply를 실행하고, 배포가 성공했는지 확인하는 과정이 너무 번거롭고 실수하기 쉬운 경우 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 수동 배포는 시간이 오래 걸리고, 사람마다 다르게 배포하여 일관성이 없으며, 테스트를 건너뛰기 쉽습니다.
특히 RAG 시스템은 ML 모델 테스트, 벡터 인덱스 검증, 응답 품질 평가 등 복잡한 검증 단계가 필요하므로 자동화가 필수입니다. 바로 이럴 때 필요한 것이 CI/CD 파이프라인입니다.
GitHub Actions나 GitLab CI를 사용하면 코드를 푸시할 때 자동으로 테스트를 실행하고, Docker 이미지를 빌드하고, Kubernetes에 배포하며, 배포 후 검증까지 수행하여 개발자가 배포 과정을 신경 쓰지 않아도 됩니다.
개요
간단히 말해서, CI/CD 파이프라인은 코드 변경부터 프로덕션 배포까지의 모든 과정을 자동화하여 빠르고 안전하게 소프트웨어를 릴리스하는 시스템입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, RAG 시스템은 코드뿐만 아니라 프롬프트 템플릿, 임베딩 모델 버전, 벡터 인덱스 설정 등 많은 요소가 결과에 영향을 줍니다.
CI 파이프라인에서 단위 테스트, 통합 테스트, 그리고 RAG 품질 테스트(예: 정답률 측정)를 자동으로 실행하면 회귀 버그를 조기에 발견할 수 있습니다. 예를 들어, 새 프롬프트 템플릿이 기존보다 정답률이 5% 떨어지면 배포를 자동으로 중단하고 알림을 보낼 수 있습니다.
전통적인 방법과의 비교를 해보면, 기존에는 개발자가 로컬에서 "테스트를 실행했다고 믿고" 배포했다면, 이제는 모든 커밋이 클린 환경에서 자동으로 테스트되고, 테스트가 통과한 코드만 배포되므로 품질이 일관되게 유지됩니다. CI/CD 파이프라인의 핵심 특징은 빠른 피드백, 일관성 보장, 롤백 용이성, 그리고 배포 히스토리 추적입니다.
이러한 특징들이 RAG 시스템의 개발 속도를 높이고, 프로덕션 장애를 줄이며, 팀 전체의 생산성을 크게 향상시킵니다.
코드 예제
# .github/workflows/deploy.yml - GitHub Actions CI/CD 파이프라인
name: RAG Service CI/CD
on:
push:
branches: [main, develop] # main은 프로덕션, develop은 스테이징
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip' # 의존성 캐싱으로 빌드 속도 향상
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run unit tests
run: pytest tests/ --cov=app --cov-report=xml
- name: Run RAG quality tests # 응답 품질 검증
run: |
python tests/rag_quality_test.py --threshold 0.85
# 정답률이 85% 미만이면 실패
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
build:
needs: test # 테스트 통과 후에만 빌드
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix={{branch}}- # develop-abc123 형식
type=semver,pattern={{version}} # v1.2.3
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha # GitHub Actions 캐시 활용
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # main 브랜치만 배포
steps:
- name: Set up kubectl
uses: azure/setup-kubectl@v3
- name: Configure AWS credentials # EKS 사용 시
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-west-2
- name: Update kubeconfig
run: |
aws eks update-kubeconfig --name production-cluster --region us-west-2
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/rag-service \
rag-api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-${{ github.sha }}
kubectl rollout status deployment/rag-service --timeout=5m
- name: Smoke test # 배포 후 검증
run: |
curl -f https://rag-api.company.com/health || exit 1
# 헬스체크 실패 시 자동 롤백 (수동으로 설정 필요)
설명
이것이 하는 일: 이 GitHub Actions 워크플로우는 코드가 푸시되면 자동으로 테스트를 실행하고, Docker 이미지를 빌드하여 레지스트리에 푸시하고, Kubernetes 클러스터에 배포하는 전체 CI/CD 프로세스를 자동화합니다. 첫 번째로, test 잡에서는 Python 의존성을 설치하고 pytest로 단위 테스트를 실행합니다.
cache: 'pip'를 사용하면 requirements.txt가 변경되지 않은 경우 이전에 설치한 패키지를 재사용하여 빌드 시간이 수 분에서 수십 초로 단축됩니다. rag_quality_test.py는 미리 준비한 평가 데이터셋으로 RAG 시스템의 응답을 테스트하여 정답률이 임계값 이하면 배포를 차단합니다.
예를 들어, 100개의 질문-정답 쌍으로 테스트하여 85개 이상 맞춰야 통과하도록 설정할 수 있습니다. 그 다음으로, build 잡에서는 테스트가 통과한 경우에만 Docker 이미지를 빌드합니다.
docker/metadata-action으로 Git 브랜치와 커밋 해시를 기반으로 태그를 자동 생성하므로, develop-abc123, main-def456 같은 형식으로 이미지를 구분할 수 있습니다. 이렇게 하면 특정 커밋의 이미지를 쉽게 찾아 롤백할 수 있습니다.
cache-from/cache-to로 Docker 레이어 캐싱을 활성화하면 변경된 레이어만 다시 빌드하여 이미지 빌드 시간이 10분에서 2-3분으로 줄어듭니다. 세 번째로, deploy 잡에서는 main 브랜치에 푸시된 경우에만 프로덕션에 배포합니다.
kubectl set image로 Deployment의 이미지를 업데이트하면 Kubernetes가 RollingUpdate 전략에 따라 무중단 배포를 수행합니다. kubectl rollout status로 배포가 완료될 때까지 기다리고, timeout을 5분으로 설정하여 배포가 멈추면 실패 처리합니다.
배포 후 smoke test로 헬스체크 엔드포인트를 호출하여 애플리케이션이 정상 동작하는지 검증합니다. 여러분이 이 파이프라인을 사용하면 코드를 푸시한 후 5-10분 내에 자동으로 프로덕션에 배포되고, 모든 과정이 GitHub Actions UI에서 추적됩니다.
테스트가 실패하거나 빌드 에러가 발생하면 즉시 Slack으로 알림을 받을 수 있고, 배포 히스토리를 통해 "언제 누가 무엇을 배포했는지" 추적할 수 있습니다. 또한 pull request를 열면 자동으로 테스트가 실행되어 코드 리뷰 전에 버그를 발견할 수 있습니다.
실전 팁
💡 단계별 환경을 사용하세요. develop 브랜치는 스테이징에 자동 배포하고, main 브랜치는 프로덕션에 배포하여 실제 트래픽으로 검증한 후 릴리스하세요.
💡 카나리 배포를 구현하세요. Argo Rollouts나 Flagger를 사용하면 신버전을 10% 트래픽에만 먼저 배포하고, 에러율이 낮으면 점진적으로 100%까지 늘릴 수 있습니다.
💡 자동 롤백을 설정하세요. 배포 후 에러율이 1% 초과하거나 P99 레이턴시가 5초를 넘으면 자동으로 이전 버전으로 롤백하도록 파이프라인을 구성하세요.
💡 의존성 스캔을 추가하세요. Trivy나 Snyk으로 Docker 이미지의 취약점을 스캔하여 심각한 보안 이슈가 있으면 배포를 차단하세요.
💡 병렬 실행을 최대한 활용하세요. 단위 테스트, 통합 테스트, 린팅을 별도 잡으로 병렬 실행하면 전체 파이프라인 시간을 30-50% 단축할 수 있습니다.
7. 로드 밸런싱과 인그레스 - 트래픽 관리
시작하며
여러분이 RAG 서비스를 여러 Pod로 스케일링했는데 이런 상황을 겪어본 적 있나요? 사용자 트래픽이 일부 Pod에만 몰려서 과부하가 걸리고, 다른 Pod는 유휴 상태로 놀고 있어서 리소스를 낭비하는 경우 말이죠.
또는 외부에서 서비스에 접근할 때 각 Pod의 IP를 직접 알아야 해서 관리가 복잡한 경우도 있습니다. 이런 문제는 실제 개발 현장에서 자주 발생합니다.
Kubernetes Pod는 일시적이어서 재시작될 때마다 IP가 바뀌므로 직접 접근하기 어렵고, 여러 Pod 간에 트래픽을 고르게 분산시키려면 로드 밸런서가 필요합니다. 특히 RAG 시스템은 요청마다 처리 시간이 다르므로 라운드 로빈보다 지능적인 로드 밸런싱이 필요합니다.
바로 이럴 때 필요한 것이 Kubernetes Service와 Ingress입니다. Service는 Pod 그룹에 안정적인 엔드포인트를 제공하고 내부 로드 밸런싱을 수행하며, Ingress는 외부 트래픽을 받아 SSL 종료와 경로 기반 라우팅을 처리합니다.
개요
간단히 말해서, Service는 Kubernetes 클러스터 내부에서 Pod 그룹에 접근하기 위한 추상화 레이어이고, Ingress는 HTTP/HTTPS 트래픽을 클러스터 외부에서 내부 Service로 라우팅하는 규칙입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, RAG 서비스가 3개의 Pod로 실행되고 있을 때 클라이언트는 Service의 고정된 DNS 이름(예: rag-service.default.svc.cluster.local)으로 접근하면 자동으로 정상 Pod 중 하나로 연결됩니다.
만약 한 Pod가 죽으면 Service가 자동으로 해당 Pod를 제외하고 나머지 2개로만 트래픽을 보냅니다. 예를 들어, readinessProbe가 실패한 Pod는 Service의 엔드포인트에서 제거되어 사용자가 장애를 겪지 않습니다.
전통적인 방법과의 비교를 해보면, 기존에는 HAProxy나 Nginx를 수동으로 설정하고 백엔드 서버 목록을 하드코딩했다면, 이제는 Kubernetes가 Service Mesh나 Ingress Controller를 통해 자동으로 서비스 디스커버리와 로드 밸런싱을 처리합니다. Service와 Ingress의 핵심 특징은 서비스 디스커버리, 헬스 체크 기반 라우팅, SSL/TLS 종료, 그리고 경로 기반 라우팅입니다.
이러한 특징들이 RAG 시스템의 가용성을 높이고, 트래픽을 효율적으로 분산하며, 보안과 관리를 간소화합니다.
코드 예제
# kubernetes/service-ingress.yaml - Service와 Ingress 설정
apiVersion: v1
kind: Service
metadata:
name: rag-service
annotations:
# AWS Load Balancer Controller 설정 (선택)
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
spec:
type: ClusterIP # 클러스터 내부에서만 접근 (Ingress가 외부 노출)
selector:
app: rag-service # Deployment의 레이블과 일치
ports:
- name: http
port: 80
targetPort: 8000 # Pod의 컨테이너 포트
sessionAffinity: ClientIP # 같은 클라이언트는 같은 Pod로 라우팅 (선택)
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600 # 1시간 동안 세션 유지
---
# Ingress - 외부 트래픽 라우팅
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rag-ingress
annotations:
# NGINX Ingress Controller 설정
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m" # 최대 요청 크기
nginx.ingress.kubernetes.io/proxy-read-timeout: "300" # LLM 응답 대기
# Rate limiting으로 DDoS 방지
nginx.ingress.kubernetes.io/limit-rps: "100" # 초당 100 요청 제한
# Cert-manager로 자동 SSL 인증서 발급
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- rag-api.company.com
secretName: rag-tls-cert # cert-manager가 자동 생성
rules:
- host: rag-api.company.com
http:
paths:
- path: /v1/query # RAG 질의 엔드포인트
pathType: Prefix
backend:
service: