Post

프로덕션 환경에서의 인공지능 모델 배포 완벽 가이드

프로덕션 환경에서의 인공지능 모델 배포 완벽 가이드

Claude와 함께 모델 배포 공부하기

프로덕션 환경에서의 인공지능 모델 배포 완벽 가이드

인공지능 모델을 개발하는 것은 전체 AI 시스템 구축 과정의 일부일 뿐입니다. 모델이 실제 비즈니스 가치를 창출하려면 안정적이고 확장 가능한 방식으로 프로덕션 환경에 배포되어야 합니다. 이 글에서는 인공지능 모델을 훈련시키는 단계부터 실제 서비스에 배포하는 전체 과정을 상세히 설명합니다.

목차

  1. 모델 훈련 및 평가
  2. 모델 최적화
  3. 모델 컨테이너화
  4. 쿠버네티스로 모델 서빙
  5. CI/CD 파이프라인 구축
  6. 모니터링 및 로깅 시스템
  7. 모델 관리 시스템
  8. 보안 강화
  9. 데이터 드리프트 감지
  10. 전체 시스템 통합

1. 모델 훈련 및 평가

모델 훈련이란?

모델 훈련이란 AI 모델이 데이터로부터 패턴을 학습하여 예측이나 분류 같은 작업을 수행할 수 있게 만드는 과정입니다. 이는 데이터 과학 워크플로우의 핵심 단계입니다.

PyTorch로 모델 훈련하기

PyTorch는 딥러닝 모델을 쉽게 개발할 수 있는 인기 있는 프레임워크입니다. 아래는 이미지 분류 모델을 훈련하는 간단한 예시 코드입니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms, models

# 데이터 변환 및 로딩
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

train_dataset = datasets.ImageFolder('data/train', transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)

# 사전 훈련된 ResNet 모델 로드
model = models.resnet50(pretrained=True)

# 마지막 완전 연결 계층 수정 (예: 10개 클래스로 분류)
num_classes = 10
model.fc = nn.Linear(model.fc.in_features, num_classes)

# 손실 함수 및 옵티마이저 정의
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

# 모델 훈련
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)

for epoch in range(10):  # 10 에폭 동안 훈련
    running_loss = 0.0
    for inputs, labels in train_loader:
        inputs, labels = inputs.to(device), labels.to(device)
        
        # 그래디언트 초기화
        optimizer.zero_grad()
        
        # 순전파 + 역전파 + 최적화
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
    
    print(f'에폭 {epoch+1}, 손실: {running_loss/len(train_loader):.4f}')

# 모델 저장
torch.save(model.state_dict(), 'model.pth')

모델 평가 방법

모델이 얼마나 잘 작동하는지 확인하려면 적절한 평가 지표를 사용해야 합니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import torch
from sklearn.metrics import accuracy_score, precision_recall_fscore_support
import json

# 테스트 데이터 로드
test_dataset = datasets.ImageFolder('data/test', transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=32)

# 평가 모드로 전환
model.eval()

# 예측 수행
all_preds = []
all_labels = []

with torch.no_grad():
    for inputs, labels in test_loader:
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = model(inputs)
        _, predictions = torch.max(outputs, 1)
        
        all_preds.extend(predictions.cpu().numpy())
        all_labels.extend(labels.cpu().numpy())

# 정확도 계산
accuracy = accuracy_score(all_labels, all_preds)
precision, recall, f1, _ = precision_recall_fscore_support(all_labels, all_preds, average='weighted')

# 평가 결과 저장
evaluation_results = {
    "overall_accuracy": float(accuracy),
    "precision": float(precision),
    "recall": float(recall),
    "f1_score": float(f1)
}

with open("evaluation_results.json", "w") as f:
    json.dump(evaluation_results, f, indent=2)

print(f"정확도: {accuracy:.4f}")
print(f"정밀도: {precision:.4f}")
print(f"재현율: {recall:.4f}")
print(f"F1 점수: {f1:.4f}")

2. 모델 최적화

모델 최적화가 필요한 이유

훈련된 모델은 종종 크기가 크고 계산 비용이 많이 들어 프로덕션 환경에서 직접 사용하기 어렵습니다. 최적화를 통해 성능을 크게 저하시키지 않으면서 모델의 크기와 추론 시간을 줄일 수 있습니다.

ONNX로 모델 변환하기

ONNX(Open Neural Network Exchange)는 다양한 딥러닝 프레임워크 간의 모델 교환을 위한 개방형 표준입니다. PyTorch 모델을 ONNX로 변환하면 다양한 하드웨어 및 소프트웨어 플랫폼에서 실행할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import torch
import onnx
import onnxruntime
import numpy as np

# PyTorch 모델 로드
model = models.resnet50()
model.fc = nn.Linear(model.fc.in_features, num_classes)
model.load_state_dict(torch.load('model.pth'))
model.eval()

# ONNX로 변환하기 위한 샘플 입력
dummy_input = torch.randn(1, 3, 224, 224)

# ONNX로 내보내기
torch.onnx.export(
    model,               # 실행할 모델
    dummy_input,         # 모델 입력(튜플 또는 텐서)
    "model.onnx",        # 저장할 모델 파일 이름
    export_params=True,  # 모델 파일에 학습된 매개변수 가중치 저장
    opset_version=12,    # ONNX 버전
    do_constant_folding=True,  # 상수 폴딩 최적화 수행
    input_names=['input'],     # 입력의 이름
    output_names=['output'],   # 출력의 이름
    dynamic_axes={
        'input': {0: 'batch_size'},  # 가변 길이 축
        'output': {0: 'batch_size'}
    }
)

# ONNX 모델 검증
onnx_model = onnx.load("model.onnx")
onnx.checker.check_model(onnx_model)
print("ONNX 모델이 성공적으로 내보내졌습니다.")

# ONNX Runtime으로 추론 테스트
session = onnxruntime.InferenceSession("model.onnx")
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

# 샘플 입력으로 추론 테스트
input_data = dummy_input.numpy()
result = session.run([output_name], {input_name: input_data})
print("ONNX 모델 테스트 완료!")

모델 양자화로 성능 최적화

양자화(Quantization)는 32비트 부동 소수점 가중치를 8비트 정수로 변환하여 모델 크기를 줄이고 추론 속도를 높이는 기술입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType

# 원본 ONNX 모델 로드
model_path = "model.onnx"
quantized_model_path = "model_quantized.onnx"

# 동적 양자화 수행
quantize_dynamic(
    model_path,
    quantized_model_path,
    weight_type=QuantType.QInt8
)

print(f"양자화된 모델이 저장되었습니다: {quantized_model_path}")

# 원본 모델과 양자화된 모델의 크기 비교
import os
original_size = os.path.getsize(model_path) / (1024 * 1024)
quantized_size = os.path.getsize(quantized_model_path) / (1024 * 1024)

print(f"원본 모델 크기: {original_size:.2f} MB")
print(f"양자화된 모델 크기: {quantized_size:.2f} MB")
print(f"크기 감소: {(1 - quantized_size/original_size) * 100:.2f}%")

3. 모델 컨테이너화

컨테이너화의 중요성

컨테이너를 사용하면 모델과 그 종속성을 패키징하여 어떤 환경에서도 일관되게 실행할 수 있습니다. Docker는 가장 널리 사용되는 컨테이너화 도구입니다.

FastAPI로 모델 서빙 API 만들기

FastAPI는 Python으로 빠르고 성능이 좋은 API를 쉽게 만들 수 있는 최신 웹 프레임워크입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# app.py
import os
import io
import numpy as np
from PIL import Image
import onnxruntime
from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
import time
import logging
from prometheus_client import Counter, Histogram, start_http_server

# FastAPI 앱 초기화
app = FastAPI(title="이미지 분류 API")

# CORS 설정 (Cross-Origin Resource Sharing)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 로깅 설정
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Prometheus 메트릭 설정
REQUESTS = Counter('model_requests_total', '처리된 요청 수')
PREDICTIONS = Counter('model_predictions', '예측된 클래스별 수', ['class'])
PREDICTION_TIME = Histogram('model_prediction_seconds', '예측에 소요된 시간')

# 클래스 이름
class_names = ["클래스1", "클래스2", "클래스3", "클래스4", "클래스5", 
               "클래스6", "클래스7", "클래스8", "클래스9", "클래스10"]

# 모델 로드
model_path = "model_quantized.onnx"
session = onnxruntime.InferenceSession(model_path)
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

logger.info(f"모델이 로드되었습니다: {model_path}")

# 이미지 전처리 함수
def preprocess_image(image):
    # PIL 이미지를 NumPy 배열로 변환
    image = image.resize((224, 224))
    image = np.array(image).transpose(2, 0, 1)  # (H, W, C) -> (C, H, W)
    
    # 정규화
    mean = np.array([0.485, 0.456, 0.406]).reshape(-1, 1, 1)
    std = np.array([0.229, 0.224, 0.225]).reshape(-1, 1, 1)
    image = (image / 255.0 - mean) / std
    
    # 배치 차원 추가
    image = np.expand_dims(image, axis=0).astype(np.float32)
    return image

# 시작 이벤트
@app.on_event("startup")
def startup_event():
    # Prometheus 서버 시작 (포트 8000)
    start_http_server(8000)
    logger.info("Prometheus 메트릭 서버가 포트 8000에서 시작되었습니다")

# 헬스 체크 엔드포인트
@app.get("/health")
def health_check():
    return {"status": "healthy"}

# 예측 엔드포인트
@app.post("/predict")
async def predict(file: UploadFile = File(...)):
    # 요청 카운터 증가
    REQUESTS.inc()
    
    # 이미지 로드 및 전처리
    start_time = time.time()
    image_data = await file.read()
    image = Image.open(io.BytesIO(image_data)).convert("RGB")
    processed_image = preprocess_image(image)
    
    # 모델 추론
    with PREDICTION_TIME.time():
        results = session.run([output_name], {input_name: processed_image})
    
    # 결과 처리
    predictions = results[0][0]
    predicted_class_idx = np.argmax(predictions)
    predicted_class = class_names[predicted_class_idx]
    confidence = float(predictions[predicted_class_idx])
    
    # 예측 클래스 카운터 증가
    PREDICTIONS.labels(class=predicted_class).inc()
    
    # 처리 시간 계산
    processing_time = time.time() - start_time
    
    # 결과 반환
    result = {
        "class": predicted_class,
        "confidence": confidence,
        "processing_time_ms": processing_time * 1000
    }
    
    logger.info(f"예측 결과: {result}")
    return result

# 메인 함수
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8080)

Dockerfile 작성하기

Dockerfile은 Docker 이미지를 빌드하기 위한 지침을 포함하는 텍스트 파일입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 기본 이미지로 Python 3.9 사용
FROM python:3.9-slim

# 작업 디렉토리 설정
WORKDIR /app

# 필요한 패키지 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 애플리케이션 코드 복사
COPY app.py .
COPY model_quantized.onnx .

# 컨테이너 내부 포트 노출
EXPOSE 8080
EXPOSE 8000

# 앱 실행
CMD ["python", "app.py"]

requirements.txt 파일에는 다음과 같은 패키지가 포함됩니다:

1
2
3
4
5
6
7
fastapi==0.95.0
uvicorn==0.21.1
onnxruntime==1.14.1
numpy==1.24.2
Pillow==9.5.0
python-multipart==0.0.6
prometheus-client==0.16.0

Docker 이미지 빌드와 푸시

1
2
3
4
5
6
7
8
9
# Docker 이미지 빌드
docker build -t yourusername/image-classifier:latest .

# 빌드된 이미지 테스트
docker run -p 8080:8080 -p 8000:8000 yourusername/image-classifier:latest

# Docker Hub에 이미지 푸시
docker login
docker push yourusername/image-classifier:latest

4. 쿠버네티스로 모델 서빙

쿠버네티스란?

쿠버네티스(Kubernetes)는 컨테이너화된 애플리케이션의 배포, 확장 및 관리를 자동화하는 오픈소스 플랫폼입니다. 이는 모델을 안정적으로 서빙하고 확장하는 데 이상적입니다.

쿠버네티스 디플로이먼트 YAML 작성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: image-classifier
  namespace: model-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: image-classifier
  template:
    metadata:
      labels:
        app: image-classifier
    spec:
      containers:
      - name: image-classifier
        image: yourusername/image-classifier:latest
        ports:
        - containerPort: 8080
          name: api
        - containerPort: 8000
          name: metrics
        resources:
          limits:
            cpu: "1"
            memory: "2Gi"
          requests:
            cpu: "500m"
            memory: "1Gi"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10

쿠버네티스 서비스 YAML 작성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# kubernetes/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: image-classifier
  namespace: model-serving
spec:
  selector:
    app: image-classifier
  ports:
  - port: 80
    targetPort: 8080
    name: api
  - port: 8000
    name: metrics
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: image-classifier
  namespace: model-serving
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
  rules:
  - host: model-api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: image-classifier
            port:
              number: 80

수평 포드 자동 확장 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# kubernetes/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: image-classifier
  namespace: model-serving
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: image-classifier
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

쿠버네티스 리소스 배포

1
2
3
4
5
6
7
8
9
10
11
12
# 네임스페이스 생성
kubectl create namespace model-serving

# 쿠버네티스 리소스 배포
kubectl apply -f kubernetes/deployment.yaml
kubectl apply -f kubernetes/service.yaml
kubectl apply -f kubernetes/hpa.yaml

# 배포 상태 확인
kubectl get pods -n model-serving
kubectl get services -n model-serving
kubectl get ingress -n model-serving

5. CI/CD 파이프라인 구축

CI/CD의 중요성

CI/CD(지속적 통합/지속적 배포)는 코드 변경이 자동으로 테스트되고 프로덕션 환경에 배포되도록 하여 개발 워크플로우를 자동화합니다. 이는 모델 업데이트 프로세스를 더 안정적이고 효율적으로 만듭니다.

GitHub Actions 워크플로우 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.9'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pytest
    
    - name: Run tests
      run: |
        pytest
    
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: $
        password: $
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: $/image-classifier:latest,$/image-classifier:$
    
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
    - uses: actions/checkout@v3
    
    - name: Install kubectl
      uses: azure/setup-kubectl@v3
      with:
        version: 'v1.25.0'
    
    - name: Configure kubeconfig
      uses: azure/k8s-set-context@v3
      with:
        kubeconfig: $
    
    - name: Update deployment image
      run: |
        kubectl set image deployment/image-classifier -n model-serving image-classifier=$/image-classifier:$
    
    - name: Check deployment status
      run: |
        kubectl rollout status deployment/image-classifier -n model-serving

6. 모니터링 및 로깅 시스템

모니터링의 중요성

모델이 프로덕션 환경에 배포되면 성능과 건강 상태를 지속적으로 모니터링해야 합니다. 이를 통해 잠재적인 문제를 신속하게 감지하고 해결할 수 있습니다.

Prometheus와 Grafana로 모니터링 시스템 구축

Prometheus는 메트릭을 수집하고 저장하는 모니터링 시스템이며, Grafana는 이러한 메트릭을 시각화하는 도구입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# kubernetes/prometheus.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
    
    scrape_configs:
      - job_name: 'kubernetes-pods'
        kubernetes_sd_configs:
        - role: pod
        relabel_configs:
        - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
          action: keep
          regex: true
        - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
          action: replace
          target_label: __metrics_path__
          regex: (.+)
        - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
          action: replace
          regex: ([^:]+)(?::\d+)?;(\d+)
          replacement: $1:$2
          target_label: __address__
        - action: labelmap
          regex: __meta_kubernetes_pod_label_(.+)
        - source_labels: [__meta_kubernetes_namespace]
          action: replace
          target_label: kubernetes_namespace
        - source_labels: [__meta_kubernetes_pod_name]
          action: replace
          target_label: kubernetes_pod_name
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      containers:
      - name: prometheus
        image: prom/prometheus:v2.43.0
        args:
        - "--config.file=/etc/prometheus/prometheus.yml"
        - "--storage.tsdb.path=/prometheus"
        ports:
        - containerPort: 9090
        volumeMounts:
        - name: config-volume
          mountPath: /etc/prometheus
        - name: data
          mountPath: /prometheus
      volumes:
      - name: config-volume
        configMap:
          name: prometheus-config
      - name: data
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: monitoring
spec:
  selector:
    app: prometheus
  ports:
  - port: 9090
  type: ClusterIP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# kubernetes/grafana.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      labels:
        app: grafana
    spec:
      containers:
      - name: grafana
        image: grafana/grafana:9.5.1
        ports:
        - containerPort: 3000
        env:
        - name: GF_SECURITY_ADMIN_USER
          value: admin
        - name: GF_SECURITY_ADMIN_PASSWORD
          value: admin123  # 프로덕션 환경에서는 시크릿 사용
        volumeMounts:
        - name: data
          mountPath: /var/lib/grafana
      volumes:
      - name: data
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: grafana
  namespace: monitoring
spec:
  selector:
    app: grafana
  ports:
  - port: 3000
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grafana
  namespace: monitoring
spec:
  rules:
  - host: grafana.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: grafana
            port:
              number: 3000

ELK 스택으로 로깅 시스템 구축

ELK 스택(Elasticsearch, Logstash, Kibana)은 로그 데이터를 수집, 저장, 분석하고 시각화하는 강력한 솔루션입니다. 이 예제에서는 모델의 로그를 수집하고 분석하기 위해 ELK 스택을 설정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# kubernetes/elasticsearch.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  namespace: logging
spec:
  serviceName: elasticsearch
  replicas: 1
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      containers:
      - name: elasticsearch
        image: docker.elastic.co/elasticsearch/elasticsearch:8.7.0
        env:
        - name: discovery.type
          value: single-node
        - name: ES_JAVA_OPTS
          value: "-Xms512m -Xmx512m"
        - name: xpack.security.enabled
          value: "false"
        ports:
        - containerPort: 9200
          name: http
        - containerPort: 9300
          name: transport
        volumeMounts:
        - name: data
          mountPath: /usr/share/elasticsearch/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
  name: elasticsearch
  namespace: logging
spec:
  selector:
    app: elasticsearch
  ports:
  - port: 9200
    name: http
  - port: 9300
    name: transport
  type: ClusterIP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# kubernetes/kibana.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kibana
  namespace: logging
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kibana
  template:
    metadata:
      labels:
        app: kibana
    spec:
      containers:
      - name: kibana
        image: docker.elastic.co/kibana/kibana:8.7.0
        env:
        - name: ELASTICSEARCH_HOSTS
          value: "http://elasticsearch:9200"
        ports:
        - containerPort: 5601
---
apiVersion: v1
kind: Service
metadata:
  name: kibana
  namespace: logging
spec:
  selector:
    app: kibana
  ports:
  - port: 5601
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kibana
  namespace: logging
spec:
  rules:
  - host: kibana.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kibana
            port:
              number: 5601
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# kubernetes/fluentd.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: logging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluentd
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - namespaces
  verbs:
  - get
  - list
  - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: fluentd
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluentd
subjects:
- kind: ServiceAccount
  name: fluentd
  namespace: logging
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: logging
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      serviceAccountName: fluentd
      containers:
      - name: fluentd
        image: fluent/fluentd-kubernetes-daemonset:v1.16-debian-elasticsearch7-1
        env:
        - name: FLUENT_ELASTICSEARCH_HOST
          value: "elasticsearch"
        - name: FLUENT_ELASTICSEARCH_PORT
          value: "9200"
        - name: FLUENT_ELASTICSEARCH_SCHEME
          value: "http"
        - name: FLUENTD_SYSTEMD_CONF
          value: "disable"
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

로깅 시스템을 설정하는 스크립트:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# setup_logging.sh

# 로깅 네임스페이스 생성
kubectl create namespace logging

# ELK 스택 배포
kubectl apply -f kubernetes/elasticsearch.yaml
kubectl apply -f kubernetes/kibana.yaml
kubectl apply -f kubernetes/fluentd.yaml

echo "로깅 시스템이 설정되었습니다."
echo "Kibana: http://kibana.example.com"

7. 모델 관리 시스템

모델 관리의 중요성

모델 관리 시스템은, 특히 여러 모델 버전을 관리하고 이들의 성능을 추적해야 할 때 필수적입니다. 이러한 시스템은 모델 버전 간 비교를 가능하게 하고 모델 개발 및 배포 워크플로우를 문서화합니다.

MLflow를 사용한 모델 관리 시스템 구축

MLflow는 머신러닝 실험 및 모델 관리를 위한 오픈소스 플랫폼입니다. 다음은 쿠버네티스 환경에서 MLflow를 설정하는 방법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# kubernetes/mlflow.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mlflow-config
  namespace: mlops
data:
  MLFLOW_S3_ENDPOINT_URL: "http://minio:9000"
  AWS_ACCESS_KEY_ID: "minioadmin"
  AWS_SECRET_ACCESS_KEY: "minioadmin"
  MLFLOW_TRACKING_URI: "postgresql://mlflow:mlflow@postgresql:5432/mlflow"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mlflow
  namespace: mlops
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mlflow
  template:
    metadata:
      labels:
        app: mlflow
    spec:
      containers:
      - name: mlflow
        image: ghcr.io/mlflow/mlflow:v2.3.0
        args:
        - mlflow
        - server
        - --backend-store-uri
        - postgresql://mlflow:mlflow@postgresql:5432/mlflow
        - --default-artifact-root
        - s3://mlflow/artifacts
        - --host
        - 0.0.0.0
        - --port
        - "5000"
        ports:
        - containerPort: 5000
        envFrom:
        - configMapRef:
            name: mlflow-config
---
apiVersion: v1
kind: Service
metadata:
  name: mlflow
  namespace: mlops
spec:
  selector:
    app: mlflow
  ports:
  - port: 5000
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mlflow
  namespace: mlops
spec:
  rules:
  - host: mlflow.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: mlflow
            port:
              number: 5000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# kubernetes/postgres.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgresql
  namespace: mlops
spec:
  serviceName: postgresql
  replicas: 1
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
      - name: postgresql
        image: postgres:14.7
        env:
        - name: POSTGRES_USER
          value: mlflow
        - name: POSTGRES_PASSWORD
          value: mlflow
        - name: POSTGRES_DB
          value: mlflow
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
  name: postgresql
  namespace: mlops
spec:
  selector:
    app: postgresql
  ports:
  - port: 5432
  type: ClusterIP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# kubernetes/minio.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
  namespace: mlops
spec:
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
      - name: minio
        image: minio/minio:RELEASE.2023-04-13T03-08-07Z
        args:
        - server
        - /data
        - --console-address
        - ":9001"
        env:
        - name: MINIO_ROOT_USER
          value: minioadmin
        - name: MINIO_ROOT_PASSWORD
          value: minioadmin
        ports:
        - containerPort: 9000
        - containerPort: 9001
        volumeMounts:
        - name: data
          mountPath: /data
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: minio-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: minio-pvc
  namespace: mlops
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
  name: minio
  namespace: mlops
spec:
  selector:
    app: minio
  ports:
  - port: 9000
    name: api
  - port: 9001
    name: console
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minio-console
  namespace: mlops
spec:
  rules:
  - host: minio.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: minio
            port:
              number: 9001

모델 등록 및 배포 스크립트:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# register_model.py
import mlflow
import mlflow.onnx
import os
import json
import onnx

# MLflow 서버 설정
mlflow.set_tracking_uri("http://mlflow.example.com")
mlflow.set_experiment("image_classification")

# 평가 결과 로드
with open("evaluation_results.json", "r") as f:
    evaluation = json.load(f)

# 모델 로드
model_path = "model_quantized.onnx"
onnx_model = onnx.load(model_path)

# 모델 등록
with mlflow.start_run() as run:
    # 성능 지표 기록
    mlflow.log_metric("accuracy", evaluation["overall_accuracy"])
    mlflow.log_metric("precision", evaluation["precision"])
    mlflow.log_metric("recall", evaluation["recall"])
    mlflow.log_metric("f1_score", evaluation["f1_score"])
    
    # 모델 아티팩트 등록
    mlflow.onnx.log_model(onnx_model, "model", registered_model_name="image_classifier")
    
    print(f"모델이 MLflow에 등록되었습니다. Run ID: {run.info.run_id}")

8. 보안 강화

보안의 중요성

AI 모델 배포 파이프라인의 보안은 매우 중요합니다. 특히 모델이 민감한 데이터를 처리하거나 중요한 결정을 내리는 경우 더욱 그렇습니다.

시크릿 관리

쿠버네티스 시크릿 대신 외부 시크릿 관리 도구를 사용하면 보안을 강화할 수 있습니다. HashiCorp Vault는 인기 있는 시크릿 관리 솔루션입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# kubernetes/vault.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vault
  namespace: security
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vault
  template:
    metadata:
      labels:
        app: vault
    spec:
      containers:
      - name: vault
        image: vault:1.13.1
        ports:
        - containerPort: 8200
        env:
        - name: VAULT_DEV_ROOT_TOKEN_ID
          value: "root"
---
apiVersion: v1
kind: Service
metadata:
  name: vault
  namespace: security
spec:
  selector:
    app: vault
  ports:
  - port: 8200
  type: ClusterIP

Vault 통합 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# vault_secrets.py
import hvac
import os

# Vault 클라이언트 설정
client = hvac.Client(url='http://vault:8200', token='root')

# 시크릿 읽기
db_secrets = client.secrets.kv.v2.read_secret_version(path='database/credentials')
db_username = db_secrets['data']['data']['username']
db_password = db_secrets['data']['data']['password']

# 환경 변수로 설정
os.environ['DB_USERNAME'] = db_username
os.environ['DB_PASSWORD'] = db_password

네트워크 정책

네트워크 정책을 사용하여 Pod 간 트래픽을 제한할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# kubernetes/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: model-api-policy
  namespace: model-serving
spec:
  podSelector:
    matchLabels:
      app: image-classifier
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: default
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: monitoring
    ports:
    - protocol: TCP
      port: 9090

9. 데이터 드리프트 감지

데이터 드리프트란?

데이터 드리프트는 모델이 훈련된 데이터 분포와 프로덕션 환경에서 모델이 받는 데이터 분포 간의 변화를 말합니다. 이러한 변화는 시간이 지남에 따라 모델 성능 저하로 이어질 수 있습니다.

데이터 드리프트 감지 시스템 구축

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# drift_detection.py
import numpy as np
from sklearn.base import BaseEstimator
from alibi_detect.cd import KSDrift
import mlflow
import json
import time
import requests

# 참조 데이터 로드
reference_data = np.load('reference_data.npy')

# 드리프트 감지기 초기화
drift_detector = KSDrift(
    reference_data,
    p_val=0.05,
    alternative='two-sided'
)

# MLflow 설정
mlflow.set_tracking_uri("http://mlflow.example.com")
mlflow.set_experiment("drift_detection")

# Slack 알림 함수
def send_slack_alert(message):
    webhook_url = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
    payload = {"text": message}
    requests.post(webhook_url, json=payload)

# 주기적으로 드리프트 감지 실행
while True:
    # 최근 데이터 수집 (예: 최근 1시간 동안의 모델 입력 데이터)
    recent_data = fetch_recent_data()  # 구현 필요
    
    # 드리프트 감지
    drift_result = drift_detector.predict(recent_data)
    
    # 결과 기록 및 처리
    with mlflow.start_run():
        mlflow.log_param("timestamp", time.time())
        mlflow.log_metric("p_value", drift_result['data']['p_val'][0])
        mlflow.log_param("drift_detected", drift_result['data']['is_drift'])
        
        # 드리프트가 감지되면 알림 발송
        if drift_result['data']['is_drift']:
            alert_message = f"⚠️ 데이터 드리프트가 감지되었습니다! p-value: {drift_result['data']['p_val'][0]}"
            send_slack_alert(alert_message)
            mlflow.log_param("alert_sent", True)
            
            # 추가 분석을 위한 데이터 저장
            with open(f"drift_{int(time.time())}.json", "w") as f:
                json.dump(drift_result['data'], f)
    
    # 1시간마다 실행
    time.sleep(3600)

이를 쿠버네티스 크론잡으로 배포:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# kubernetes/drift-detection.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: drift-detection
  namespace: model-serving
spec:
  schedule: "0 * * * *"  # 매시간 실행
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: drift-detection
            image: yourusername/drift-detection:latest
          restartPolicy: OnFailure

10. 전체 시스템 통합

GitOps 워크플로우로 전체 시스템 통합

ArgoCD를 사용하여 GitOps 워크플로우를 설정할 수 있습니다. 이는 Git 저장소를 단일 진실 소스로 사용하여 인프라 상태를 자동으로 동기화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# kubernetes/argocd.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: model-serving
  namespace: argocd
spec:
  project: default
  source:
    repoURL: 'https://github.com/yourusername/image-classifier.git'
    path: kubernetes
    targetRevision: HEAD
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: model-serving
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

전체 시스템을 통합하는 스크립트:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash
# deploy_complete_system.sh

# 필요한 네임스페이스 생성
kubectl create namespace argocd
kubectl create namespace model-serving
kubectl create namespace mlops
kubectl create namespace monitoring
kubectl create namespace logging
kubectl create namespace security

# ArgoCD 설치
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 모니터링 시스템 배포
kubectl apply -f kubernetes/prometheus.yaml
kubectl apply -f kubernetes/grafana.yaml

# 로깅 시스템 배포
kubectl apply -f kubernetes/elasticsearch.yaml
kubectl apply -f kubernetes/kibana.yaml
kubectl apply -f kubernetes/fluentd.yaml

# 모델 관리 시스템 배포
kubectl apply -f kubernetes/postgres.yaml
kubectl apply -f kubernetes/minio.yaml
kubectl apply -f kubernetes/mlflow.yaml

# 보안 시스템 배포
kubectl apply -f kubernetes/vault.yaml
kubectl apply -f kubernetes/network-policy.yaml

# ArgoCD 애플리케이션 배포
kubectl apply -f kubernetes/argocd.yaml

echo "전체 ML 시스템이 배포되었습니다."
echo "ArgoCD: https://argocd.example.com"
echo "모델 서빙 API: https://model-api.example.com"
echo "Grafana: https://grafana.example.com"
echo "Kibana: https://kibana.example.com"
echo "MLflow: https://mlflow.example.com"

결론

인공지능 모델을 프로덕션 환경에 배포하는 것은 복잡한 과정이지만, 올바른 도구와 접근 방식을 사용하면 효율적으로 관리할 수 있습니다. 이 가이드에서는 모델 훈련부터 프로덕션 배포까지 전체 과정을 다루었습니다:

  1. PyTorch를 사용한 모델 훈련 및 평가
  2. ONNX로 모델 변환 및 최적화
  3. Docker를 사용한 모델 컨테이너화
  4. 쿠버네티스를 사용한 모델 서빙 시스템 구축
  5. GitHub Actions를 사용한 CI/CD 파이프라인 구축
  6. Prometheus/Grafana와 ELK 스택을 사용한 모니터링 및 로깅
  7. MLflow를 사용한 모델 관리 시스템
  8. Vault를 사용한 보안 강화
  9. 데이터 드리프트 감지 시스템 구축
  10. ArgoCD를 사용한 GitOps 워크플로우 설정

이 모든 단계는 오픈소스 툴을 사용하여 구현되었으며, 확장 가능하고 안정적인 AI 모델 배포 파이프라인을 구축하는 데 필요한 핵심 요소를 제공합니다.

각 개별 요소는 필요에 따라 더 확장하거나 대체할 수 있으며, 프로덕션 환경에서는 보안 및 확장성 측면에서 추가적인 고려가 필요할 수 있습니다.

머신러닝 모델을 효율적으로 배포하고 관리하면 AI 솔루션의 가치를 최대화하고 비즈니스 목표를 달성하는 데 도움이 됩니다. 이 가이드가 여러분의 AI 배포 여정에 도움이 되길 바랍니다!

This post is licensed under CC BY 4.0 by the author.