Repository for dip
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

14 KiB

MLflow 배포 (OIDC Auth + Multi-Tenant Workspace)

MLflow 3.11.1 + mlflow-oidc-auth v7.0.3을 Keycloak과 연동하고, 팀별 workspace로 멀티테넌시를 구성하는 가이드이다. 커스텀 이미지 paasup/mlflow:v3.11.1-oidc에 OIDC 플러그인이 포함되어 있다.


1. 사전 준비

1.1 Keycloak 설정

Client 생성

  • Realm: paasup, Client ID: mlflow, Protocol: openid-connect
  • Access Type: confidential
  • Valid Redirect URIs: https://mlflow.example.org/*
  • Web Origins: https://mlflow.example.org

Groups Mapper 추가

Client → Mappers → Create:

항목
Mapper type Group Membership
Token Claim Name groups
Full group path false
Add to ID token true
Add to access token true
Add to userinfo true ← 반드시 true

Groups 생성

그룹명 역할
mlflow 모든 사용자 필수 소속 — 미소속 시 로그인 거부
mlflow-admin 관리자 권한
mlflow-team-<팀명> workspace 매핑용 팀 그룹 (예: mlflow-team-ds, mlflow-team-mlops)

사용자는 반드시 mlflow 그룹과 소속 팀 그룹 모두에 추가해야 한다.

예시:
mlflow-test      → mlflow, mlflow-team-ds
mlflow-ops-test  → mlflow, mlflow-team-mlops
mlflow-admin     → mlflow, mlflow-admin

1.2 Keycloak CA 인증서 ConfigMap 생성

Keycloak이 사설 CA 인증서를 사용하는 경우 필수이다.

# Keycloak TLS secret에서 CA 인증서 추출
kubectl get secret keycloak.example.org-tls -n platform \
  -o jsonpath='{.data.ca\.crt}' | base64 -d > /tmp/keycloak-ca.crt

# ConfigMap 생성
kubectl create configmap keycloak-ca-cert -n mlflow \
  --from-file=ca.crt=/tmp/keycloak-ca.crt

1.3 OIDC Secret 생성

SECRET_KEY는 uvicorn 멀티워커 환경에서 세션 공유를 위해 반드시 포함해야 한다.

FERNET_KEY=$(python3 -c "import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())")
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")

kubectl create secret generic mlflow-oidc-secret -n mlflow \
  --from-literal=OIDC_CLIENT_SECRET="<keycloak-client-secret>" \
  --from-literal=MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY="$FERNET_KEY" \
  --from-literal=SECRET_KEY="$SECRET_KEY"

Keycloak Client Secret은 Keycloak Admin Console → Client → Credentials 탭에서 확인한다.


1.4 Workspace Detection Plugin ConfigMap 생성

팀 그룹(mlflow-team-*)을 workspace 이름으로 변환하는 Python 플러그인을 ConfigMap으로 배포한다.

# mlflow_workspace_detector.py
import base64
import json

def get_user_workspaces(access_token: str) -> list:
    """
    JWT access_token의 groups 클레임에서 mlflow-team-* 그룹을 읽어
    workspace 이름 목록을 반환한다.

    예: ["mlflow-team-ds", "mlflow"] → ["team-ds"]
    """
    try:
        payload = access_token.split(".")[1]
        payload += "=" * (4 - len(payload) % 4)
        claims = json.loads(base64.b64decode(payload))
    except Exception:
        return []
    groups = claims.get("groups", [])
    return [
        g[len("mlflow-"):]
        for g in groups
        if g.startswith("mlflow-team-")
    ]
kubectl create configmap mlflow-workspace-plugin -n mlflow \
  --from-file=mlflow_workspace_detector.py=mlflow_workspace_detector.py

2. 배포 방법

git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog

# 신규 설치
helm install mlflow manifests/helm/mlflow/1.9.0/ \
  -f manifests/helm/mlflow/1.9.0/custom-values.yaml \
  -n mlflow --create-namespace

# 업그레이드
helm upgrade mlflow manifests/helm/mlflow/1.9.0/ \
  -f manifests/helm/mlflow/1.9.0/custom-values.yaml \
  -n mlflow

3. custom-values.yaml 설명

3.1 이미지 설정

OIDC 플러그인이 포함된 커스텀 이미지를 사용한다.

image:
  repository: paasup/mlflow
  tag: "v3.11.1-oidc"

initImages:
  mlflowDbMigration:
    repository: paasup/mlflow
    tag: "v3.11.1-oidc"

3.2 OIDC 환경변수

extraEnvVars:
  # --- 기본 설정 ---
  MLFLOW_S3_ENDPOINT_URL: "http://minio.minio.svc.cluster.local:9000"
  MLFLOW_S3_IGNORE_TLS: "true"
  SSL_CERT_FILE: "/etc/ssl/certs/custom-ca.crt"

  # --- OIDC 설정 ---
  OIDC_CLIENT_ID: "mlflow"
  OIDC_DISCOVERY_URL: "https://keycloak.example.org/realms/paasup/.well-known/openid-configuration"
  OIDC_REDIRECT_URI: "https://mlflow.example.org/callback"
  OIDC_SCOPE: "openid email profile"
  OIDC_GROUPS_ATTRIBUTE: "groups"
  OIDC_GROUP_NAME: "mlflow"
  OIDC_ADMIN_GROUP_NAME: "mlflow-admin"
  OIDC_USERS_DB_URI: "postgresql://mlflow:mlflow1234@mlflow-postgresql:5432/mlflow"
  DEFAULT_MLFLOW_PERMISSION: "READ"
  AUTOMATIC_LOGIN_REDIRECT: "true"
  OIDC_ALEMBIC_VERSION_TABLE: "mlflow_oidc_alembic_version"

주의사항

항목 올바른 값 잘못된 값 이유
SSL_CERT_FILE SSL_CERT_FILE REQUESTS_CA_BUNDLE mlflow-oidc-auth는 httpx를 사용하며 httpx는 REQUESTS_CA_BUNDLE을 인식하지 않음
OIDC_SCOPE "openid email profile" "openid,email,profile" OAuth2 RFC 6749 표준: 스코프는 공백으로 구분
OIDC_REDIRECT_URI .../callback .../oidc/callback auth_router에 prefix가 없어 실제 경로는 /callback
OIDC_ALEMBIC_VERSION_TABLE "mlflow_oidc_alembic_version" 기본값(alembic_version) MLflow와 mlflow-oidc-auth가 동일한 테이블 사용 시 마이그레이션 충돌

3.3 Workspace 환경변수

extraEnvVars:
  # --- Workspace 설정 ---
  MLFLOW_ENABLE_WORKSPACES: "true"
  OIDC_WORKSPACE_DEFAULT_PERMISSION: "EDIT"
  OIDC_WORKSPACE_DETECTION_PLUGIN: "mlflow_workspace_detector"
  PYTHONPATH: "/opt/mlflow-plugins"
  WORKSPACE_CACHE_MAX_SIZE: "1024"
  WORKSPACE_CACHE_TTL_SECONDS: "300"
  PERMISSION_SOURCE_ORDER: "user,group,regex,group-regex"
항목 설명
MLFLOW_ENABLE_WORKSPACES workspace 기능 활성화
OIDC_WORKSPACE_DEFAULT_PERMISSION workspace 첫 접근 시 자동 부여 권한. EDIT 설정 시 팀원 전원 편집 가능
OIDC_WORKSPACE_DETECTION_PLUGIN workspace 감지 플러그인 모듈명 (함수명 미포함, 모듈명만 지정)
PYTHONPATH ConfigMap으로 마운트된 플러그인 경로를 Python import 경로에 추가
WORKSPACE_CACHE_MAX_SIZE workspace 목록 캐시 최대 항목 수
WORKSPACE_CACHE_TTL_SECONDS workspace 목록 캐시 TTL (초)
PERMISSION_SOURCE_ORDER 권한 판단 순서: 개인 → 그룹 → 정규식 → 그룹-정규식

플러그인 주의사항

  • OIDC_WORKSPACE_DETECTION_PLUGIN에는 모듈명만 지정한다. (mlflow_workspace_detector.get_user_workspaces 형태 불가)
  • 호출되는 함수 이름은 get_user_workspaces로 고정되어 있다.
  • 함수 인자로 전달되는 access_tokenJWT 문자열이며, 플러그인 내부에서 base64 디코딩이 필요하다.
  • 플러그인 오류 발생 시 exception이 조용히 처리되어 workspace 목록이 빈 배열로 반환된다. 로그에서 WARNING 레벨로 확인 가능하다.

3.4 OIDC App 활성화

extraArgs:
  appName: "oidc-auth"
  uvicornOpts: "--timeout-keep-alive 600"
  allowedHosts: "mlflow.example.org"
  corsAllowedOrigins: "https://mlflow.example.org"    # CORS 허용 Origin

log:
  enabled: false    # uvicornOpts 사용 시 반드시 false (gunicorn/uvicorn 충돌 방지)

auth:
  enabled: false    # mlflow-oidc-auth가 자체 인증 처리

3.5 Secret 참조

extraSecretNamesForEnvFrom:
  - mlflow-oidc-secret    # OIDC_CLIENT_SECRET, SECRET_KEY, MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY 포함

3.6 볼륨 마운트

extraVolumes:
  - name: keycloak-ca-cert
    configMap:
      name: keycloak-ca-cert
  - name: workspace-plugin
    configMap:
      name: mlflow-workspace-plugin

extraVolumeMounts:
  - name: keycloak-ca-cert
    mountPath: /etc/ssl/certs/custom-ca.crt
    subPath: ca.crt
    readOnly: true
  - name: workspace-plugin
    mountPath: /opt/mlflow-plugins

3.7 before_request.py 패치

mlflow-oidc-auth v7.0.3의 hooks/before_request.py line 520에 권한 체크 오류가 있다. workspace에서 실험·모델 생성 시 MANAGE(can_manage)를 요구하지만, OIDC_WORKSPACE_DEFAULT_PERMISSION: "EDIT"으로 자동 부여된 EDIT 권한은 can_update=True, can_manage=False이므로 EDIT 사용자가 항상 403을 받는다.

수정 내용 (files/before_request.py line 520)

# 원본 (버그)
if ws_perm is None or not ws_perm.can_manage:
    return responses.make_forbidden_response()

# 패치 (수정)
if ws_perm is None or not ws_perm.can_update:
    return responses.make_forbidden_response()

ConfigMap 생성

kubectl create configmap mlflow-hooks-patch -n mlflow \
  --from-file=before_request.py=manifests/helm/mlflow/1.9.0/files/before_request.py

패치 파일 소스: files/before_request.py

Python 버전 확인: 컨테이너 이미지의 Python 버전이 다를 경우 extraVolumeMountsmountPath를 수정한다. paasup/mlflow:v3.11.1-oidc 이미지의 실제 Python 버전은 3.10이므로 경로는 python3.10을 사용한다.

kubectl exec -n mlflow <pod> -- python -c \
  "import mlflow_oidc_auth.hooks.before_request as m; print(m.__file__)"

업스트림 이슈: 이 동작이 의도된 설계인지 여부를 mlflow-oidc-auth 저장소에 문의했다. → Issue #240 업스트림에서 수정이 반영되면 이 패치는 제거한다.


3.8 Ingress 설정

ingress:
  enabled: true
  className: "kong"
  annotations:
    cert-manager.io/cluster-issuer: "selfsigned-issuer"
    cert-manager.io/duration: 8760h
    cert-manager.io/renew-before: 720h
  hosts:
    - host: mlflow.example.org
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls:
    - secretName: mlflow-tls-secret
      hosts:
        - mlflow.example.org

mlflow.example.org를 실제 도메인으로 변경한다.


3.9 CORS 설정

브라우저가 MLflow API를 cross-origin으로 호출할 때 발생하는 Cross-origin request blocked를 해결하기 위해 두 가지 설정이 필요하다.

MLflow 서버 네이티브 설정

extraArgs.corsAllowedOrigins으로 --cors-allowed-origins 플래그를 전달한다.

extraArgs:
  corsAllowedOrigins: "https://mlflow.example.org"

여러 origin을 허용할 경우 쉼표로 구분한다.

  corsAllowedOrigins: "https://mlflow.example.org,https://other.example.org"

Kong Ingress CORS 플러그인

Kong이 CORS preflight(OPTIONS) 요청을 처리하고 응답 헤더를 보완하도록 KongPlugin을 추가한다.

kubectl apply -f - <<EOF
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: mlflow-cors
  namespace: mlflow
plugin: cors
config:
  origins:
    - "https://mlflow.example.org"
  methods:
    - GET
    - POST
    - PUT
    - DELETE
    - OPTIONS
    - PATCH
  headers:
    - Accept
    - Authorization
    - Content-Type
  credentials: true
  max_age: 3600
EOF

Ingress에 플러그인을 연결한다.

ingress:
  annotations:
    konghq.com/plugins: mlflow-cors

동작 검증

curl -sk -X OPTIONS https://mlflow.example.org/api/2.0/mlflow/experiments/list \
  -H "Origin: https://mlflow.example.org" \
  -H "Access-Control-Request-Method: GET" \
  -D - -o /dev/null | grep -i access-control

정상 응답 예시:

access-control-allow-origin: https://mlflow.example.org
access-control-allow-credentials: true
access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS,PATCH

주의: 브라우저 콘솔에서 403 응답이 Cross-origin request blocked로 표시되는 경우가 있다. 응답 헤더에 Access-Control-Allow-Origin이 존재하면 CORS 자체는 정상이며, 실제 원인은 MLflow 권한(403) 문제이다.


3.10 PostgreSQL 설정

내장 PostgreSQL을 사용한다.

postgresql:
  enabled: true
  auth:
    username: mlflow
    password: mlflow1234    # 변경 권장
    database: mlflow
  image:
    repository: bitnamilegacy/postgresql
  primary:
    persistence:
      enabled: true

OIDC_USERS_DB_URI도 동일한 접속 정보를 사용한다.

extraEnvVars:
  OIDC_USERS_DB_URI: "postgresql://mlflow:mlflow1234@mlflow-postgresql:5432/mlflow"

3.11 S3 (MinIO) 설정

artifactRoot:
  proxiedArtifactStorage: true
  defaultArtifactsDestination: "s3://mlflow/artifacts"
  s3:
    enabled: true
    bucket: mlflow
    path: artifacts
    awsAccessKeyId: "adminuser"       # MinIO access key
    awsSecretAccessKey: "adminuser"   # MinIO secret key

extraEnvVars:
  MLFLOW_S3_ENDPOINT_URL: "http://minio.minio.svc.cluster.local:9000"
  MLFLOW_S3_IGNORE_TLS: "true"

외부 MinIO 사용 시 MLFLOW_S3_ENDPOINT_URL을 해당 엔드포인트로 변경한다.


4. 배포 검증

4.1 OIDC 인증 검증

  1. https://mlflow.example.org 접속 → Keycloak 로그인 페이지로 자동 리다이렉트 확인
  2. mlflow 그룹 사용자로 로그인 → MLflow UI 정상 진입 확인
  3. mlflow-admin 그룹 사용자로 로그인 → 관리자 메뉴 접근 확인
  4. mlflow 그룹 미소속 사용자 로그인 → 접근 거부 확인

4.2 Workspace 격리 검증

사용자 소속 그룹 기대 workspace
mlflow-test mlflow, mlflow-team-ds team-ds
mlflow-ops-test mlflow, mlflow-team-mlops team-mlops
  1. mlflow-test로 로그인 → team-ds workspace 자동 생성 확인
  2. mlflow-ops-test로 로그인 → team-mlops workspace 자동 생성 확인
  3. 각 사용자가 상대방 workspace의 실험·모델에 접근 불가 확인