이 글에서 다루는 것
MLflow를 PostgreSQL + S3 백엔드로 구성하여 Kubernetes에 Helm 배포하고, 커스텀 이미지와 Ingress를 연동합니다.
선수지식
- MLOps 플랫폼 구축 2단계: S3 & PostgreSQL Secret 관리 — Secret/ConfigMap 주입 전략
이 단계에서 해결하려는 문제
한 프로젝트에 여러 버전의 모델이 등장하면, 각 실험의 파라미터/메트릭/아티팩트를 체계적으로 추적할 서버가 필요합니다. 로컬 MLflow UI(SQLite + file 저장)로는 팀 공유와 아티팩트 영속성이 부족합니다. 이 단계에서는 MLflow를 PostgreSQL(메타데이터) + S3(아티팩트) 백엔드로 구성하고 Helm으로 Kubernetes에 배포합니다.
📐 아키텍처 구성도

🐳 커스텀 MLflow Docker 이미지 제작
공식 MLflow 이미지는 PostgreSQL 드라이버(psycopg2)가 포함되어 있지 않아 오류가 발생합니다.
Dockerfile
FROM ghcr.io/mlflow/mlflow:v2.13.0
RUN pip install psycopg2-binary boto3
빌드 & 푸시
docker build -t ghcr.io/your-id/mlflow:with-psycopg2-and-s3 .
docker push ghcr.io/your-id/mlflow:with-psycopg2-and-s3
🛠 Helm Chart 구성 요소
Chart.yaml
apiVersion: v2
name: mlflow
description: A Helm chart for deploying MLflow on Kubernetes
version: 0.1.0
appVersion: "2.13.0"
values.yaml
image:
repository: hoizz/mlflow
tag: with-psycopg2-and-s3
pullPolicy: IfNotPresent
env:
defaultArtifactRoot: s3://mlflow-artifacts-keonho
service:
type: ClusterIP
port: 5000
ingress:
enabled: true
className: nginx # 반드시 명시해야 NGINX가 라우팅함
host: mlflow.local
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-body-size: 10m
nginx.ingress.kubernetes.io/ssl-redirect: "false"
📄 Deployment 구성 (핵심 부분)
command: ["mlflow", "server"]
args:
- "--backend-store-uri=postgresql://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)"
- "--default-artifact-root={{ .Values.env.defaultArtifactRoot }}"
- "--host=0.0.0.0"
- "--port=5000"
- "--serve-artifacts" # 이게 있어야 artifacts UI/REST 지원 가능
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: mlflow-db-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: mlflow-db-secret
key: password
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: mlflow-db-config
key: host
envFrom:
- secretRef:
name: aws-credentials-secret
🌐 Ingress 구성
로컬 Kubernetes에서는 LoadBalancer가 작동하지 않으므로(퍼블릭 IP 없음), DaemonSet + hostNetwork 방식으로 Ingress를 구성합니다.
# Pod가 노드와 동일한 네트워크 사용하도록 ingress-nginx 커스텀
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.hostNetwork=true \
--set controller.dnsPolicy=ClusterFirstWithHostNet \
--set controller.kind=DaemonSet
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mlflow-ingress
namespace: mlflow
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mlflow.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mlflow-service
port:
number: 5000
🚀 배포
kubectl create namespace mlflow
helm install mlflow mlflow-helm/ -n mlflow
🧪 검증
| 항목 | 확인 |
|---|---|
| MLflow UI | http://mlflow.local 접근 확인 |
| 실험 등록 | mlflow.log_param, mlflow.log_metric, log_model() |
| S3 연동 | .mlruns 폴더가 S3에 생성되는지 확인 |
| PostgreSQL 연동 | 실험, run metadata가 DB에 들어가는지 확인 |
🚨 트러블슈팅
TS_01: psycopg2 모듈 없음 → 500 오류
ModuleNotFoundError: No module named 'psycopg2'
해결: 커스텀 Docker 이미지에서 pip install psycopg2-binary 포함
TS_02: Calico IPIP 문제로 VM(PostgreSQL) 통신 실패
# Calico가 IPIP 터널링(Protocol 4)을 사용해 Pod → 외부로 패킷 전송
tcpdump -i ens33 proto 4
# PostgreSQL이 설치된 VM이 IPIP 패킷을 Drop함
해결1: SNAT 강제 적용 (로컬 테스트 환경)
iptables -t nat -A POSTROUTING -s {pod-network-CIDR} -d {DB_IP} -j MASQUERADE
해결2: Calico IPIP 터널 비활성화 (VM-Pod 직접 통신 필요시)
kubectl edit ippool default-ipv4-ippool
spec:
ipipMode: Never
natOutgoing: true
# 이후 모든 Pod 재시작 + 노드 간 L3 라우팅 필요
설계 판단 (Why This Way?)
공식 MLflow 이미지에 psycopg2/boto3가 없어 커스텀 이미지가 필수이며, –serve-artifacts로 프록시 모드를 활성화해야 클라이언트가 직접 S3 인증 없이 아티팩트에 접근할 수 있습니다. 로컬 환경에서는 Ingress DaemonSet + hostNetwork로 클라우드 LB 없이 접근하고, 모델 URI는 Stage 기반으로 설정하여 핫스왑 구조와 연결했습니다.
다음에 읽을 글
→ MLOps 플랫폼 구축 4단계: Airflow GitSync + 외부 PostgreSQL + Secret 연동 — Airflow Helm 배포와 GitSync DAG 동기화