MLflow Artifact 프록시 — 전환 적용·롤백
외부 학습 서버의 표준 mlflow SDK 가 artifact(체크포인트·모델)를 gend-api 프록시
경유로 업/다운로드하게 하는 전환(ADR-0038
결정 4, 사용자 가이드는 실험 추적)의
운영자 절차입니다.
:::danger 수동 적용 대상 — CI 자동 배포 아님
gend-api 는 main 머지 시 CI 가 자동 배포하지만, mlflow 는 아닙니다. 이
매니페스트는 운영자가 직접 kubectl apply 해야 반영됩니다.
:::
적용
infra/mlflow/deployment.yaml 의 command 가 --default-artifact-root s3://…
대신 --artifacts-destination $(MLFLOW_ARTIFACTS_DESTINATION) 를 사용하도록
바뀌어야 신규 실험의 artifact 트래픽이 서버 REST(프록시 대상)를 탑니다.
:::danger infra/mlflow/deployment.yaml 을 그대로 apply 하지 마세요 — 이미지가 덮어써집니다
이 매니페스트는 image: gend/mlflow:placeholder 를 담고 있습니다. 전체
프로비저닝 스크립트(infra/azure-deploy/scripts/02-deploy-k8s.sh)는 적용
직전에 sed 로 실제 ACR 태그를 채워 넣는 것을 전제하는데, 이 파일 하나만 떼어
kubectl apply -f 하면 그 치환이 빠진 채로 라이브 이미지가 플레이스홀더로
덮어써져 ImagePullBackOff 로 죽습니다. 2026-08-05 prod 에서 정확히 이
경로로 kyuubi·mlflow 가 동시에 다운됐습니다 (가드 상세:
.claude/commands/deploy.md 상단 :placeholder 매니페스트 절).
적용 전 반드시 현재 라이브 이미지를 확인하고, 그 태그로 치환한 사본에 apply 하세요:
LIVE_IMAGE=$(kubectl --context aks-genos-prod -n gend get deploy mlflow \
-o jsonpath='{.spec.template.spec.containers[0].image}')
echo "$LIVE_IMAGE" # gend/mlflow:placeholder 가 아니라 실제 태그(예:
# genosprodacr.azurecr.io/mlflow:0.1.0)여야 함 — placeholder
# 가 나오면 먼저 원인을 해결하고 진행하세요.
sed "s#image: gend/mlflow:placeholder#image: ${LIVE_IMAGE}#" \
infra/mlflow/deployment.yaml > /tmp/mlflow-deployment-apply.yaml
:::
:::danger apply 전 apply 대상 파일의 kind: 를 전수 확인하세요
2026-08-06 prod 장애가 바로 이 지점에서 났습니다. 당시 infra/mlflow/ configmap.yaml 에 kind: ConfigMap 문서와 kind: Secret 문서가 한 파일에
섞여 있었고, 이 런북대로 그 파일을 그대로 apply 하자 라이브 DB 비밀번호가
레포의 dev placeholder 값으로 조용히 덮어써져 mlflow 가 CrashLoop 에
빠졌습니다. 파일명이 configmap.yaml 이라는 사실은 그 안에 Secret 이
없다는 보증이 아닙니다. 이때 유일한 단서는 apply 출력의
secret/mlflow-secret configured 한 줄뿐이었고(configmap/mlflow-config unchanged 같은 무해한 줄들 사이에 섞여 넘어가기 쉽습니다), 사고 이후에야
알아챘습니다.
infra/mlflow/configmap.yaml 은 이제(#2991 Task 1 / PR #2999, v1.2+) kind: ConfigMap 만 담고, Secret 은 별도 infra/mlflow/secret.yaml 로 분리돼
있습니다. 정책 전체는 ADR-0040
을 보세요. 이 분리가 재발하지 않았는지 apply 직전 눈으로 확인하세요:
grep -n '^kind:' infra/mlflow/configmap.yaml
# ConfigMap 한 줄만 나와야 합니다. Secret 이 함께 나오면 즉시 중단하고
# #2991 이 재발했는지(파일이 다시 합쳐졌는지) 먼저 확인하세요 — 그대로
# apply 하면 이번 사고가 그대로 반복됩니다.
이 원칙(파일명이 아니라 kind: 를 실측)은 이 런북에 국한되지 않습니다 —
infra/ 하위 아무 매니페스트나 apply 하기 전에 습관화하세요. CI 가드
scripts/test_manifest_secret_isolation.py 가 kind: Secret 이 다른 kind
와 같은 파일에 커밋되는 것 자체는 막지만, 로컬에서 아직 커밋되지 않은 임시
수정본이나 이 문서처럼 과거 커밋을 체크아웃해 쓰는 파일까지는 지켜주지
않습니다.
:::
kubectl --context aks-genos-prod apply \
-f infra/mlflow/configmap.yaml \
-f /tmp/mlflow-deployment-apply.yaml
kubectl --context aks-genos-prod -n gend rollout status deploy/mlflow --timeout=180s
적용 후에도 파드 인자가 실제로 바뀌었는지 다시 한번 실측하세요 — Deployment 스펙이 곧 파드 스펙이 아닙니다(위 이미지 치환을 빠뜨렸다면 여기서 재차 드러납니다):
POD=$(kubectl --context aks-genos-prod -n gend get pod -l app=mlflow -o jsonpath='{.items[0].metadata.name}')
kubectl --context aks-genos-prod -n gend exec "$POD" -- cat /proc/1/cmdline | tr '\0' ' '
# --artifacts-destination 이 값과 함께 보여야 하고 --default-artifact-root 는 없어야 함
scripts/test_mlflow_artifact_flags.py 가 이 두 플래그 짝(존재/부재)과
--artifacts-destination 값이 참조하는 $(VAR) 가 mlflow-config ConfigMap 에
실제로 정의돼 있는지를 매니페스트 정적 검사로 강제합니다. 같은 파일이
ephemeral-storage requests/limits + /tmp emptyDir sizeLimit 존재도
검사합니다 — mlflow 가 artifact 업/다운로드 본문을 전량 /tmp tempfile 로
받으므로, 상한 없이는 in-flight artifact 가 노드 디스크를 압박할 수 있습니다.
롤백
:::danger rollout undo 단독 금지 — ConfigMap 키 rename 때문에 파드가 깨진다
이 전환은 ConfigMap 키를 MLFLOW_DEFAULT_ARTIFACT_ROOT → MLFLOW_ARTIFACTS_DESTINATION
로 rename 했습니다(적용 절 참고). kubectl rollout undo 는 Deployment 의 이전
리비전(옛 --default-artifact-root $(MLFLOW_DEFAULT_ARTIFACT_ROOT) command)만
되돌릴 뿐 ConfigMap 은 손대지 않습니다 — 현재 mlflow-config ConfigMap 에는 옛
MLFLOW_DEFAULT_ARTIFACT_ROOT 키가 존재하지 않으므로, 되돌아간 커맨드의
$(MLFLOW_DEFAULT_ARTIFACT_ROOT) 는 미정의 변수 참조가 되어 Kubernetes 가 치환하지
않고 리터럴 문자열 그대로 컨테이너 인자에 남깁니다. mlflow 는 이 리터럴 값을
상대경로로 해석해 artifact 를 컨테이너 로컬 파일시스템에 쓰려 시도하고,
readOnlyRootFilesystem: true 때문에 쓰기가 실패해 파드가 깨집니다.
rollout undo 를 실행하지 말고, ConfigMap + Deployment 를 짝으로 되돌리세요.
:::
전환 이전 커밋(a6c92d5d — perf(infra): CPU request 전면 재산정 … (#2961),
이미 main 에 존재)에서 두 매니페스트를 체크아웃해, 적용 절과 동일한 순서
(ConfigMap → Deployment)로 apply 합니다:
:::danger a6c92d5d 의 configmap.yaml 은 Secret 분리(#2991) 이전 상태 — 그대로 apply 하면 안 됩니다
a6c92d5d 는 #2991 Task 1(PR #2999, Secret 을 infra/mlflow/secret.yaml
로 분리)보다 이전 커밋입니다. 즉 git show a6c92d5d:infra/mlflow/ configmap.yaml 로 뽑아낸 파일은 kind: ConfigMap 과 kind: Secret(옛
mlflow-secret, DB 비밀번호 포함)이 한 파일에 섞인 원래 사고 당시 상태
그대로입니다. grep -n '^kind:' 로 확인하지 않고 이 파일을 통째로
apply 하면, 이 런북이 막으려는 바로 그 사고(라이브 DB 비밀번호가 옛
placeholder 값으로 덮어써짐)를 롤백 절차 스스로 재현하게 됩니다.
git show a6c92d5d:infra/mlflow/configmap.yaml > /tmp/mlflow-configmap-rollback.yaml
grep -n '^kind:' /tmp/mlflow-configmap-rollback.yaml
# ConfigMap 과 Secret 두 줄이 나오는 게 정상입니다(옛 커밋 상태) — 하지만
# 아래에서 ConfigMap 문서만 추출해 적용합니다. Secret 문서를 그대로
# apply 하면 라이브 DB 비밀번호가 이 옛 placeholder 값으로 덮어써집니다.
# (이 롤백은 artifact 프록시 플래그만 되돌리는 것이 목적이지 DB 자격증명을
# 되돌리는 것이 아닙니다 — 라이브 mlflow-secret 은 그대로 둡니다.)
# 아래 python3 스니펫은 PyYAML(`import yaml`) 이 필요합니다. apps/api/pyproject.toml
# 의 의존성은 운영 호스트(bastion 등)의 python3 에 자동으로 설치되지 않으므로,
# 여기서 조용히 죽지 않도록 먼저 확인하세요(PR #2999 리뷰):
python3 -c 'import yaml' 2>/dev/null || {
echo "PyYAML 이 없습니다 — 아래 python3 스니펫이 ModuleNotFoundError 로 즉시 실패합니다."
echo "설치: pip install --user PyYAML"
echo "또는 PyYAML 이 이미 있는 인터프리터를 사용하세요: apps/api/.venv/bin/python3"
exit 1
}
python3 - <<'PYEOF'
import yaml
path = "/tmp/mlflow-configmap-rollback.yaml"
docs = [d for d in yaml.safe_load_all(open(path)) if d]
config_only = [d for d in docs if d.get("kind") == "ConfigMap"]
assert config_only, "ConfigMap 문서를 찾지 못함 — a6c92d5d 경로/내용을 다시 확인하세요"
with open(path, "w") as f:
yaml.safe_dump_all(config_only, f)
PYEOF
grep -n '^kind:' /tmp/mlflow-configmap-rollback.yaml # 이제 ConfigMap 한 줄만 나와야 합니다
:::
git show a6c92d5d:infra/mlflow/deployment.yaml > /tmp/mlflow-deployment-rollback.yaml
# 이 옛 매니페스트도 image: gend/mlflow:placeholder 다 — 적용 절과 동일한 이유로
# 그대로 apply 하면 라이브 이미지를 덮어쓴다. 같은 방식으로 현재 라이브 태그를
# 확인해 치환한다.
LIVE_IMAGE=$(kubectl --context aks-genos-prod -n gend get deploy mlflow \
-o jsonpath='{.spec.template.spec.containers[0].image}')
echo "$LIVE_IMAGE" # gend/mlflow:placeholder 가 아닌 실제 태그인지 확인
sed -i '' "s#image: gend/mlflow:placeholder#image: ${LIVE_IMAGE}#" \
/tmp/mlflow-deployment-rollback.yaml # GNU sed 환경(Linux)에선 -i '' 대신 -i
kubectl --context aks-genos-prod apply \
-f /tmp/mlflow-configmap-rollback.yaml \
-f /tmp/mlflow-deployment-rollback.yaml
kubectl --context aks-genos-prod -n gend rollout status deploy/mlflow --timeout=180s
적용 후 파드 인자가 실제로 옛 플래그로 되돌아갔는지 실측하세요(적용 절과 동일한 절차):
POD=$(kubectl --context aks-genos-prod -n gend get pod -l app=mlflow -o jsonpath='{.items[0].metadata.name}')
kubectl --context aks-genos-prod -n gend exec "$POD" -- cat /proc/1/cmdline | tr '\0' ' '
# --default-artifact-root 이 값과 함께 보여야 하고 --artifacts-destination 은 없어야 함
:::danger 손실성 경고 — 전환 이후 생성된 런의 artifact 참조가 파손됩니다
롤백해서 매니페스트를 --default-artifact-root s3://… 로 되돌려도, 전환
기간 동안 생성된 런은 이미 artifact_location 이 mlflow-artifacts:/(프록시
상대경로)로 기록돼 있습니다. 롤백 이후에는 이 URI 스킴을 해석하는 프록시
자체가 비활성화되므로, 그 런들의 artifact 조회(목록·다운로드 모두)가
깨집니다 — 롤백은 신규 artifact 트래픽만 원복시킬 뿐, 전환 기간에 생성된
런의 artifact 참조를 되돌리지 못합니다.
롤백을 판단하기 전에 반드시 이 손실을 감안하세요. 메트릭·파라미터 로깅과 실험/런 메타데이터는 이 손실과 무관하게 보존됩니다(영향은 artifact 바이트 접근에 한정).
:::
관련
- ADR-0038 MLflow 외부 SDK 접근
- 실험 추적 (사용자 가이드)
- 환경 변수 —
GEND_MLFLOW_TRACKING_URL/GEND_MLFLOW_ARTIFACT_MAX_MB