본문으로 건너뛰기

ML Plugin Train Trigger (#1285 G-3 M3)

POST /api/v1/ml-plugins/{plugin_id}/train → Dagster GraphQL launchRunml_plugin_train_job (#1354) 실행 → MLflow 모델 등록.

두 가이드 — 역할 분리 (#1537)

본 가이드 (ml-plugin-train-trigger.md) = 자동화 / CI / ETL 통합 — REST API 직접 호출, 3-layer allowlist 보안, GraphQL launchRun 내부 구조, 디자인 결정.

운영자 UI 시나리오 (클릭) = ml-plugin-train.md — GenD UI 안 detail sheet 의 Dagster iframe 임베드로 Playground 직접 사용. 사람 운영자는 이 페이지부터 보세요.

디자인 결정

1. 3-layer allowlist 보안

  • Layer 1 (M2 install): MLPluginService.create_plugin 가 INSERT 시 검증
  • Layer 2 (M3 trigger #1285): MLPluginTrainTrigger.trigger_train 가 GraphQL 호출 전 재검증
  • Layer 3 (M3 asset #1354): ml_plugin_train 가 importlib 직전 재검증

왜 3-layer?:

  • DB-direct INSERT 가 install-time 우회 가능 (DBA, migration)
  • Allowlist 정책이 후속 릴리스에서 강화될 수 있음 (기존 row 차단)
  • 신뢰 경계: layer 1 → DB → layer 2 → Dagster → layer 3 → executor. 각 경계에서 재검증

2. 왜 admin 전용?

  • ML 학습은 리소스 집약 (GPU, 큰 데이터셋)
  • provider_class 가 임의 코드 실행 위험 (importlib)
  • 관리자 권한 strict 강제 — viewer/editor 모두 차단

3. 왜 Dagster GraphQL launchRun?

  • Dagster Job 의 표준 trigger 메커니즘
  • runConfigData 로 plugin 컨텍스트 전달 (plugin_id, dataset_uri, hyperparams)
  • Dagster UI 에서 run 추적 가능 (operator 가 진행 상황 모니터링)

코드 핵심

# apps/api/src/gend_api/services/ml_plugins/dagster_trigger.py
async def trigger_train(self, plugin_id, *, dataset_uri=None, hyperparams=None):
# Step 1-3: DB lookup + enabled check + allowlist re-validate
plugin = await fetch(plugin_id)
if not plugin.enabled: raise ValueError("disabled")
_validate_provider_class_path(plugin.provider_class)

# Step 4: GraphQL launchRun
payload = {
"query": "mutation { launchRun(executionParams: {...}) {...} }",
"variables": {
"executionParams": {
"selector": {
"repositoryLocationName": "ml_plugin_train_repo",
"repositoryName": "ml_plugin_train_repo",
"jobName": "ml_plugin_train_job",
},
"runConfigData": {
"ops": {
"ml_plugin_train": {
"config": {
"plugin_id": str(plugin_id),
"plugin_name": plugin.name,
"provider_class": plugin.provider_class,
"dataset_uri": dataset_uri,
"hyperparams": hyperparams or {},
}
}
}
},
"executionMetadata": {
"tags": [
{"key": "plugin_id", "value": str(plugin_id)},
{"key": "trigger", "value": "manual_api"},
]
},
}
}
}
resp = await http.post(dagster_url, json=payload)
return {"run_id": resp_data["runId"], "plugin_id": str(plugin_id), "status": "queued"}

회귀 가드 — 7-9 tests (#1285) + 8 tests (#1354)

apps/api/tests/test_ml_plugin_train_trigger.py (#1285):

  • 정상 → 202 + run_id
  • 404: plugin 없음
  • 400: disabled / allowlist 위반
  • 403: 비-admin
  • 502: Dagster 5xx / errors 필드
  • Dagster mock 으로 selector + tags + ops.config 검증

pipelines/tests/test_ml_plugin_train_asset.py (#1354):

  • allowlist 통과/거부 / path traversal 거부
  • 성공 round-trip (provider → MLflow register → alias)
  • missing artifact.run_id → fail-loud
  • default alias = Staging

트러블슈팅

증상원인해결
400 "outside allowlist"provider_class prefix 위반gend_api.services.ml_plugins.<provider>.<Class> 형식 강제
502 DagsterDagster webserver 도달 못함GEND_DAGSTER_GRAPHQL_URL env 확인
502 selector not foundml_plugin_train_repo location 미등록PR #1354 머지 + Dagster image 재빌드
Run started but FAILUREprovider class import failDagster log 의 ImportError 확인

관련

  • PR #1285 (trigger), #1354 (asset)
  • 보안 메모리: feedback_no_workarounds