# 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 인증서를 사용하는 경우 필수이다. ```sh # 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 멀티워커 환경에서 세션 공유를 위해 반드시 포함해야 한다. ```sh 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="" \ --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으로 배포한다. ```python # 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-") ] ``` ```sh kubectl create configmap mlflow-workspace-plugin -n mlflow \ --from-file=mlflow_workspace_detector.py=mlflow_workspace_detector.py ``` --- ## 2. 배포 방법 ```sh 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 플러그인이 포함된 커스텀 이미지를 사용한다. ```yaml image: repository: paasup/mlflow tag: "v3.11.1-oidc" initImages: mlflowDbMigration: repository: paasup/mlflow tag: "v3.11.1-oidc" ``` --- ### 3.2 OIDC 환경변수 ```yaml 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 환경변수 ```yaml 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_token`은 **JWT 문자열**이며, 플러그인 내부에서 base64 디코딩이 필요하다. - 플러그인 오류 발생 시 exception이 조용히 처리되어 workspace 목록이 빈 배열로 반환된다. 로그에서 `WARNING` 레벨로 확인 가능하다. --- ### 3.4 OIDC App 활성화 ```yaml 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 참조 ```yaml extraSecretNamesForEnvFrom: - mlflow-oidc-secret # OIDC_CLIENT_SECRET, SECRET_KEY, MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY 포함 ``` --- ### 3.6 볼륨 마운트 ```yaml 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) ```python # 원본 (버그) 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 생성 ```sh 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 버전이 다를 경우 `extraVolumeMounts`의 `mountPath`를 수정한다. > `paasup/mlflow:v3.11.1-oidc` 이미지의 실제 Python 버전은 **3.10**이므로 경로는 `python3.10`을 사용한다. > > ```sh > kubectl exec -n mlflow -- python -c \ > "import mlflow_oidc_auth.hooks.before_request as m; print(m.__file__)" > ``` > **업스트림 이슈**: 이 동작이 의도된 설계인지 여부를 mlflow-oidc-auth 저장소에 문의했다. > → [Issue #240](https://github.com/mlflow-oidc/mlflow-oidc-auth/issues/240) > 업스트림에서 수정이 반영되면 이 패치는 제거한다. --- ### 3.8 Ingress 설정 ```yaml 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` 플래그를 전달한다. ```yaml extraArgs: corsAllowedOrigins: "https://mlflow.example.org" ``` 여러 origin을 허용할 경우 쉼표로 구분한다. ```yaml corsAllowedOrigins: "https://mlflow.example.org,https://other.example.org" ``` #### Kong Ingress CORS 플러그인 Kong이 CORS preflight(OPTIONS) 요청을 처리하고 응답 헤더를 보완하도록 KongPlugin을 추가한다. ```sh kubectl apply -f - < **주의**: 브라우저 콘솔에서 403 응답이 `Cross-origin request blocked`로 표시되는 경우가 있다. > 응답 헤더에 `Access-Control-Allow-Origin`이 존재하면 CORS 자체는 정상이며, 실제 원인은 MLflow 권한(403) 문제이다. --- ### 3.10 PostgreSQL 설정 내장 PostgreSQL을 사용한다. ```yaml postgresql: enabled: true auth: username: mlflow password: mlflow1234 # 변경 권장 database: mlflow image: repository: bitnamilegacy/postgresql primary: persistence: enabled: true ``` `OIDC_USERS_DB_URI`도 동일한 접속 정보를 사용한다. ```yaml extraEnvVars: OIDC_USERS_DB_URI: "postgresql://mlflow:mlflow1234@mlflow-postgresql:5432/mlflow" ``` --- ### 3.11 S3 (MinIO) 설정 ```yaml 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의 실험·모델에 접근 불가 확인