MLOps를 구성 요소로 쪼개 보기 — 컨테이너·MLflow·관측성·드리프트·CI/CD

MLOps를 컨테이너화, MLflow 실험관리, SLO 기반 관측성, 드리프트 탐지, CI/CD for ML 다섯 요소로 나눠 FastAPI·Kubernetes·MLflow 코드와 함께 정리한다.

2026-08-13 · 최초 발행 2025-12-11

모델 하나를 배포하는 데 필요한 건 학습 코드가 아니라 파이프라인이다. 코드 커밋이 이미지가 되고, 이미지가 실험 기록과 연결되고, 실험이 검증을 통과해야 배포로 이어지는 게이트 체계가 없으면 ML Ops는 이름만 남는다. ML Ops는 머신러닝 모델의 개발→검증→배포→모니터링→재학습 전 과정을 자동화·표준화하는 엔드투엔드 운영 체계이며, Docker 기반 컨테이너화와 Kubernetes 오케스트레이션, 데이터·성능 지표 기반 드리프트 탐지와 경보, MLflow의 실험 추적·모델 레지스트리가 이 체계를 이루는 축이다.

구성 요소로 나눠보는 MLOps

컨테이너화와 오케스트레이션은 Docker 이미지 표준화로 런타임 환경 불일치를 없애고 이식성을 확보하는 데서 출발한다. Kubernetes의 Deployment·Service·HPA로 무중단 롤링 업데이트, 오토스케일링, 셀프힐링을 구현한다.

실험 관리와 모델 레지스트리는 MLflow가 맡는다. Tracking으로 파라미터·메트릭·아티팩트를 버저닝해 재현성을 보장하고, Model Registry로 Staging·Production 사이의 단계 전환과 승인 게이트, 롤백을 다룬다.

관측성과 SLO 기반 모니터링은 예측 지연·에러율·리소스 사용량·모델 성능 지표(AUC, RMSE)를 SLO로 정의하는 데서 시작한다. Prometheus/Grafana와 로깅·분산 트레이싱을 연계하고 Slack·PagerDuty 같은 알림 채널로 이어붙인다.

드리프트 탐지는 데이터 드리프트(입력 분포 변화)와 개념 드리프트(입력-라벨 관계 변화)를 구분해서 관리하는 일이다. KS·PSI 같은 통계 검정, 모니터링 윈도우, 임계값 기반 경보로 재학습 파이프라인을 자동화한다.

CI/CD for ML은 모델·데이터·피처 파이프라인 단위로 테스트를 돌리고 Great Expectations 같은 도구로 데이터 품질을 체크하는 것까지 포함한다. Terraform·Helm 같은 IaC와 Sealed Secrets·Vault 같은 시크릿 관리, 카나리·블루그린 전략이 여기 들어간다.

파이프라인 전체 흐름

코드 커밋Docker 빌드푸시실험 로깅승격 요청승인 배포 트리거이미지 참조서비스 노출실시간 예측 트래픽메트릭·로그샘플 데이터 스트림임계 초과 경보재학습 파이프라인 트리거 모델 등록개발자(코드/모델 푸시)CI파이프라인(테스트·정적분석·데이터 검증)컨테이너 이미지이미지 레지스트리MLflowTracking(파라미터·메트릭·아티팩트)MLflow ModelRegistry(Staging/Production)CD파이프라인(Helm/Kustomize)KubernetesDeployment(롤링 업데이트)Kubernetes Service/Ingress모델 서빙(Pod)모니터링스택(Prometheus/Grafana)드리프트탐지(PSI/KS·윈도우링)알림·이슈 트래킹재학습/재평가(Job/Workflow)

Docker 단독으로 충분한가

항목 Docker 단독 Kubernetes
성능 단일 호스트 성능 최적화 용이 멀티노드 확장·자원 분산, 오토스케일링
확장성 수동 스케일링, 한계 명확 HPA/클러스터 오토스케일러로 수평 확장
일관성 호스트 의존성 영향 가능 선언형 스펙으로 재현성·환경 일관성 우수
안정성 재시작·장애 복구 수동 대응 셀프힐링, 롤백·롤아웃 전략 내장
운영 편의 단순, 초기 진입 용이 초기 복잡도 높으나 운영 자동화 우수

단일 서빙 서버로 충분한 트래픽이면 Docker 단독도 나쁘지 않지만, 무중단 롤아웃과 셀프힐링이 필요해지는 시점부터는 Kubernetes로 옮기는 편이 운영 부담을 줄인다.

서비스로 붙이기: FastAPI + Kubernetes

전제조건은 Python 3.10, Docker 24+, Kubernetes 1.27+, kubectl/Helm, MLflow 2.9+, scikit-learn 1.3+, SciPy 1.11+이며 환경 변수로 MLFLOW_TRACKING_URI, 필요하면 MLFLOW_S3_ENDPOINT_URL과 AWS/GCS 인증을 준비한다.

Dockerfile은 최소 실행 가능한 형태로 구성한다.

# python:3.10-slim 기준
FROM python:3.10-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi==0.110.0 uvicorn[standard]==0.27.0 scikit-learn==1.3.2 joblib==1.3.2
COPY model.joblib /app/model.joblib
COPY app.py /app/app.py
EXPOSE 8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]

app.py는 헬스체크와 예측 엔드포인트를 함께 노출한다.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib

app = FastAPI()
try:
    model = joblib.load("model.joblib")
except Exception as e:
    model = None

class Input(BaseModel):
    features: list[float]

@app.get("/health")
def health():
    return {"status": "ok", "model_loaded": model is not None}

@app.post("/predict")
def predict(payload: Input):
    if model is None:
        raise HTTPException(status_code=503, detail="model not loaded")
    try:
        pred = model.predict([payload.features])[0]
        return {"prediction": float(pred)}
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"invalid input: {e}")

빌드와 실행은 다음 순서다.

docker build -t myorg/model:1.0 .
docker run -p 8080:8080 myorg/model:1.0

Kubernetes 매니페스트는 3개 레플리카에 롤링 업데이트 정책을 얹는다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: model-deploy
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: model-svc
  template:
    metadata:
      labels:
        app: model-svc
    spec:
      containers:
        - name: model
          image: myorg/model:1.0
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits: { cpu: "1", memory: "1Gi" }
          readinessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /health, port: 8080 }
            initialDelaySeconds: 10
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: model-svc
spec:
  selector:
    app: model-svc
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: ClusterIP
kubectl apply -f k8s.yaml

카나리 배포는 버전 라벨을 분리(app: model-svc, version: v1/v2)하고 Service가 두 버전 중 하나를 고르게 만드는 방식이다. Ingress/Nginx의 canary annotation이나 Istio 같은 서비스 메시로 트래픽 가중치를 나눈다.

MLflow로 실험을 추적하고 승격하기

import mlflow
import mlflow.sklearn
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

mlflow.set_experiment("breast_cancer_rf")

X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)

with mlflow.start_run() as run:
    params = {"n_estimators": 200, "max_depth": 6, "random_state": 42}
    model = RandomForestClassifier(**params).fit(X_tr, y_tr)
    proba = model.predict_proba(X_te)[:, 1]
    auc = roc_auc_score(y_te, proba)

    mlflow.log_params(params)
    mlflow.log_metric("auc", auc)
    mlflow.sklearn.log_model(model, artifact_path="model", registered_model_name="bc_rf")

# 모델 등록 후 UI에서 Staging→Production 승격 권장

MLflow Tracking·Registry 서버는 별도로 구동해야 한다.

mlflow server --host 0.0.0.0 --port 5000 --backend-store-uri sqlite:///mlflow.db --default-artifact-root s3://bucket/path

드리프트를 수치로 잡아낸다

탐지 임계값은 PSI 0.2 이상 또는 KS p-value 0.01 이하를 기준으로 삼는다.

import numpy as np
from scipy import stats

def psi(expected: np.ndarray, actual: np.ndarray, bins: int = 10) -> float:
    q = np.linspace(0, 100, bins + 1)
    e_bins = np.clip(np.percentile(expected, q), -np.inf, np.inf)
    e_counts, _ = np.histogram(expected, bins=e_bins)
    a_counts, _ = np.histogram(actual,   bins=e_bins)
    e_dist = np.where(e_counts == 0, 1e-6, e_counts / e_counts.sum())
    a_dist = np.where(a_counts == 0, 1e-6, a_counts / a_counts.sum())
    return np.sum((a_dist - e_dist) * np.log(a_dist / e_dist))

def ks_pvalue(expected: np.ndarray, actual: np.ndarray) -> float:
    return stats.ks_2samp(expected, actual, alternative="two-sided", mode="auto").pvalue

def detect_drift(expected: np.ndarray, actual: np.ndarray) -> dict:
    psi_val = psi(expected, actual)
    pval = ks_pvalue(expected, actual)
    drift = (psi_val >= 0.2) or (pval <= 0.01)
    return {"psi": float(psi_val), "ks_pvalue": float(pval), "drift": drift}

# 사용 예: 과거 윈도우 vs 최근 윈도우
# expected, actual = np.array([...]), np.array([...])
# print(detect_drift(expected, actual))

운영 주기는 온라인이면 515분, 배치면 13시간 윈도우로 잡는다. 임계 초과 시 Slack이나 온콜로 경보를 보내고, 2회 연속 초과하면 재학습 워크플로를 트리거하는 게 실무에서 쓰는 완충 규칙이다.

품질 게이트 없는 CI/CD는 위험하다

CI/CD for ML을 일반 DevOps 파이프라인과 구분 짓는 건 모델·데이터·피처 파이프라인 단위의 테스트와 Great Expectations 같은 데이터 품질 체크다. 여기에 Terraform·Helm으로 인프라를 코드화하고 Sealed Secrets나 Vault로 시크릿을 관리하며, 카나리·블루그린 전략을 배포 단계에 적용한다. 이 게이트가 빠지면 데이터 스키마가 조용히 바뀌거나 시크릿이 평문으로 새는 사고를 CI가 잡아내지 못한다.

시나리오별 입출력과 예외 처리

온라인 예측 API 배포는 JSON 입력을 받아 전처리·모델 추론을 거쳐 확률이나 클래스를 반환한다. 모델 로드 실패는 503, 타임아웃은 504, 입력 스키마 불일치는 400으로 응답한다.

배치 추론 파이프라인은 데이터 레이크 파티션을 스파크나 벡터화 추론으로 처리해 파케 형식으로 결과를 저장한다. 입력 스냅샷 버전을 고정하고 멱등 실행과 체크포인트 관리로 무결성을 지킨다.

드리프트 기반 재학습 자동화는 최근 윈도우의 피처 분포를 통계 검정으로 확인해 임계 초과 시 재학습 파이프라인을 트리거한다. Model Registry의 단계 전환에는 승인 게이트가, 배포에는 카나리를 통한 점진 전환이 붙는다.

놓치기 쉬운 보안·운영 지점

시크릿·자격증명은 Kubernetes Secret을 KMS로 암호화하고 Sealed Secrets나 Vault 통합을 권장한다 — 초기 복잡도가 늘어나는 대신 컴플라이언스를 충족한다. 이미지 공급망 보안은 SBOM·서명(cosign)·취약점 스캔(trivy)으로 지키는데, 빌드 시간이 늘어나는 대신 공급망 위험이 줄어든다. 데이터 접근은 최소권한 원칙과 데이터 마스킹·PII 토큰화로 관리한다 — 개발 편의성은 떨어지지만 데이터 유출 리스크는 줄어든다. 배포 전략은 카나리·블루그린으로 위험을 분산하는데, 운영 비용이 늘어나는 대신 MTTR이 줄어드는 트레이드오프다.

자동화 범위를 넓혀갈 때 남는 것들

모델 실험에서 프로덕션 배포까지의 리드타임은 5070% 줄어든다. 롤링 업데이트와 헬스체크로 장애율이 3050% 줄고 MTTR은 4060% 개선된다. HPA와 오토스케일링으로 피크 대비 평균 자원 사용률이 2040% 향상되고, 드리프트 조기 경보로 성능 하락 구간 체류 시간이 60% 이상 단축된다. 다섯 조각을 전부 갖추기보다는 필수 경로 — 컨테이너화, CI 테스트, 기본 모니터링, 수동 승격 — 로 먼저 시작하고, 카나리와 재학습 트리거 같은 자동화는 팀의 리스크 허용도에 맞춰 단계적으로 넓혀가는 편이 안전하다.

MLOpsMLflow드리프트탐지CICDKubernetes