ML Plugin Train Trigger (#1285 G-3 M3)
POST /api/v1/ml-plugins/{plugin_id}/train → Dagster GraphQL launchRun → ml_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 Dagster | Dagster webserver 도달 못함 | GEND_DAGSTER_GRAPHQL_URL env 확인 |
| 502 selector not found | ml_plugin_train_repo location 미등록 | PR #1354 머지 + Dagster image 재빌드 |
| Run started but FAILURE | provider class import fail | Dagster log 의 ImportError 확인 |