CI/CD와 MLOps를 함께 설명할 때는 “코드를 push하면 모델이 학습되고 운영에 배포된다”는 흐름으로 요약하곤 합니다. 전체 순서는 파악할 수 있지만 실제 운영을 설계하려면 다음 질문에도 답해야 합니다.
- CI가 Kubernetes에 직접 배포합니까, 아니면 Git만 변경합니까?
- 학습 이미지와 학습 결과 모델은 같은 버전 체계를 씁니까?
- 정확도가 낮은 모델도 자동으로 운영에 배포됩니까?
- 누가 어떤 근거로 모델을 승인했고, 어떻게 이전 버전으로 돌아갑니까?
이 글에서는 코드와 플랫폼을 전달하는 파이프라인과 학습·평가·모델 승격 파이프라인을 구분합니다. 각 파이프라인이 만든 결과를 Git의 불변 기록으로 연결해, 학습 실행과 모델 배포를 각각 관리합니다.
이 글에서 다루는 것
- GitLab, Harbor, GitLab Chart Repo, Argo CD의 정확한 책임
- push 기반 CI와 pull 기반 GitOps가 연결되는 방식
- rootless BuildKit으로 학습 이미지를 만드는 GitLab CI 예제
- Chart Repo write-back과 Argo CD 자동 동기화
- RayJob 결과를 MLflow에 기록하고 KServe 배포로 승격하는 별도 게이트
latest와 직접kubectl apply없이 감사·롤백 가능한 흐름 만들기
실습 환경과 치환값
| 항목 | 예시 |
|---|---|
| Code Repo | https://gitlab.lab.example.com/mlops/digits-training.git |
| Chart Repo | https://gitlab.lab.example.com/mlops/ml-platform-gitops.git |
| Harbor registry | harbor.lab.example.com |
| 학습 이미지 | harbor.lab.example.com/mlops/digits-trainer |
| Argo CD | https://argocd.lab.example.com |
| 실습 namespace | mlops-demo |
lab.example.com, mlops-demo, 예시 계정은 공개 문서용 치환값입니다. 실제 환경에서는 자신의 값으로 바꾸되 블로그 원고에는 실주소·사설 IP·계정·토큰을 남기지 않습니다.
다음 값을 GitLab CI 변수로 등록하고, 이 중 계정·암호·토큰은 masked/protected로 설정합니다.
| CI 변수 | 용도 |
|---|---|
HARBOR_ROBOT_USER |
push 전용 Harbor robot 계정 |
HARBOR_ROBOT_PASSWORD |
robot 계정 암호 |
CHART_REPO_TOKEN |
Chart Repo write-back 전용 project access token |
UV_TEST_IMAGE |
uv와 Python이 포함된 승인 CI image digest |
BUILDKIT_IMAGE |
승인된 rootless BuildKit image digest |
토큰에는 필요한 저장소의 최소 권한만 부여합니다. 예제 계정명이나 토큰을 YAML에 직접 적지 않습니다.
두 파이프라인과 두 번의 Git 변경
src/train.py 변경으로 pipeline이 시작됩니다. CI는 테스트와 rootless 이미지 빌드를 수행합니다.
개발자 → GitLab Code Reposrc/train.py 변경
GitLab Code Repo → GitLab CIpipeline 시작
GitLab CI → GitLab CItest + rootless build
단계를 선택하면 자동 재생이 멈춥니다. 선의 번호와 아래 설명을 함께 읽어 주세요.
전체 단계 한눈에 읽기
- 코드 변경과 이미지 검증
src/train.py 변경으로 pipeline이 시작됩니다. CI는 테스트와 rootless 이미지 빌드를 수행합니다.
- 개발자 → GitLab Code Repo: src/train.py 변경
- GitLab Code Repo → GitLab CI: pipeline 시작
- GitLab CI → GitLab CI: test + rootless build
- 첫 번째 Git 변경: 학습 실행
CI가 Harbor에 commit SHA 이미지를 push하고 Chart Repo에 RayJob의 image/runId를 write-back합니다.
- GitLab CI → Harbor: commit SHA 이미지 push
- GitLab CI → GitLab Chart Repo: image/runId write-back
- 학습 실행과 결과 기록
Argo CD가 pull/webhook으로 변경을 감지해 RayJob 선언을 동기화합니다. 학습 결과의 메트릭·모델·계보는 MLflow에 기록됩니다.
- GitLab Chart Repo → Argo CD: 변경 감지
- Argo CD → KubeRay / RayJob: RayJob 선언 동기화
- KubeRay / RayJob → MLflow: 메트릭·모델·계보 기록
- 평가와 승인 게이트
여기서 학습 실행과 모델 승격을 분리합니다. 평가·승인한 run/model version을 CI에 전달하며, 학습 성공 자체가 배포 승인은 아닙니다.
- MLflow → GitLab CI: 승인 대상 run/model version
- 두 번째 Git 변경: 모델 승격
CI가 승인된 모델의 불변 storageUri를 Chart Repo에 write-back합니다.
- GitLab CI → GitLab Chart Repo: 불변 storageUri write-back
- 승인 모델을 서빙에 반영
Argo CD가 두 번째 Git 변경을 감지하면 InferenceService를 갱신합니다.
- GitLab Chart Repo → Argo CD: 변경 감지
- Argo CD → KServe: InferenceService 갱신
도식 원문
sequenceDiagram
actor Dev as 개발자
participant Code as GitLab Code Repo
participant CI as GitLab CI
participant Harbor
participant GitOps as GitLab Chart Repo
participant Argo as Argo CD
participant Ray as KubeRay / RayJob
participant MLflow
participant Serve as KServe
Dev->>Code: src/train.py 변경
Code->>CI: pipeline 시작
CI->>CI: test + rootless image build
CI->>Harbor: commit SHA 이미지 push
CI->>GitOps: RayJob image/runId write-back
GitOps-->>Argo: 변경 감지(pull/webhook)
Argo->>Ray: RayJob 선언 동기화
Ray->>MLflow: 메트릭·모델·계보 기록
Note over MLflow,GitOps: 평가와 승인 게이트
MLflow->>CI: 승인 대상 run/model version
CI->>GitOps: 불변 model storageUri write-back
GitOps-->>Argo: 변경 감지
Argo->>Serve: InferenceService 갱신첫 번째 Git 변경으로는 학습 실행을 배포합니다. 두 번째 Git 변경으로는 검증된 모델을 서빙에 승격합니다. 두 변경을 분리하면 학습이 성공한 뒤에도 평가와 승인을 거쳐야 모델이 운영 트래픽을 받도록 구성할 수 있습니다.
각 시스템이 보관하는 진실
| 시스템 | 보관하는 것 | 보관하지 말아야 할 것 |
|---|---|---|
| GitLab Code Repo | Python 코드, 테스트, Dockerfile, lockfile, CI 정의 | 모델 바이너리, 운영 Secret |
| Harbor | 학습·서빙 OCI 이미지, digest, 스캔 결과 | 모델 승인 상태 |
| GitLab Chart Repo | Helm chart, 이미지 식별자, 모델 URI, Argo Application | 가변 로컬 빌드 산출물 |
| Argo CD | Git과 클러스터의 차이, 동기화 상태 | 원본 소스 코드 빌드 |
| MLflow | run, 메트릭, 모델 버전, 계보, 승인 태그/별칭 | Kubernetes 배포 상태의 원본 |
| S3/MinIO | 데이터셋과 모델 아티팩트 | 사람이 수정하는 배포 선언 |
GitOps에서는 클러스터에 적용할 원하는 상태를 Chart Repo에 기록합니다. CI가 kubectl apply와 argocd app set을 계속 호출하면 실제 상태를 변경한 이력과 Git 기록이 어긋납니다. 따라서 CI는 이미지를 push하고 Git을 갱신하는 역할을 맡습니다. Argo CD는 그 Git을 pull해 클러스터에 적용합니다.
저장소 구조
Code Repo는 실행 가능한 소프트웨어를 가집니다.
digits-training/
├── .gitlab-ci.yml
├── Dockerfile
├── pyproject.toml
├── uv.lock
├── src/
│ └── train.py
└── tests/
└── test_train.pyChart Repo는 환경별 원하는 상태를 가집니다.
ml-platform-gitops/
├── applications/
│ └── digits-ml.yaml
└── charts/
└── digits-ml/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
└── templates/
├── rayjob.yaml
└── inferenceservice.yaml이 글에서는 전체 흐름을 살펴보기 쉽도록 학습과 서빙을 하나의 chart와 mlops-demo namespace에 둡니다. 조직 규모가 커지면 별도 chart·Application·namespace로 나누고 각 AppProject 권한도 분리합니다. Code Repo에는 Chart Repo의 어느 경로에 어떤 값을 기록할지 정해 두어야 합니다. Chart Repo에는 배포에 필요한 선언을 두며, Code Repo의 내부 구현까지 복제하지 않습니다.
1단계: 변경 범위를 제한한다
학습 이미지 빌드는 실행에 영향을 주는 파일이 바뀌었을 때만 수행하도록 범위를 정합니다. 문서만 수정한 경우에는 불필요한 빌드 비용이 들지 않도록 GitLab CI의 rules:changes에 실행 조건을 명시합니다.
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- when: never
.training_changes: &training_changes
changes:
- src/**/*
- tests/**/*
- pyproject.toml
- uv.lock
- Dockerfile
- .gitlab-ci.yml최종 과제의 요구처럼 src/train.py만 지정할 수도 있지만, 의존성과 Dockerfile이 바뀌어도 새 이미지가 필요합니다. 실제 파이프라인은 실행 결과에 영향을 주는 파일을 모두 포함해야 합니다.
2단계: rootless BuildKit으로 이미지를 빌드한다
Docker daemon을 공유하거나 privileged Docker-in-Docker를 기본으로 쓰지 않습니다. GitLab Runner가 허용하는 환경에서 rootless BuildKit의 daemonless 스크립트를 사용합니다.
stages:
- test
- build
- writeback
test-training-code:
stage: test
image: "${UV_TEST_IMAGE}"
rules:
- <<: *training_changes
script:
- uv sync --frozen --dev
- uv run pytest -q
- uv lock --check
build-training-image:
stage: build
image:
name: "${BUILDKIT_IMAGE}"
entrypoint: [""]
variables:
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
HARBOR_REGISTRY: harbor.lab.example.com
IMAGE_REPOSITORY: harbor.lab.example.com/mlops/digits-trainer
needs:
- test-training-code
rules:
- <<: *training_changes
before_script:
- mkdir -p "${HOME}/.docker"
- |
AUTH="$(printf '%s:%s' "${HARBOR_ROBOT_USER}" "${HARBOR_ROBOT_PASSWORD}" | base64 | tr -d '\n')"
printf '{"auths":{"%s":{"auth":"%s"}}}\n' \
"${HARBOR_REGISTRY}" "${AUTH}" > "${HOME}/.docker/config.json"
script:
- |
buildctl-daemonless.sh build \
--frontend dockerfile.v0 \
--local context=. \
--local dockerfile=. \
--opt filename=Dockerfile \
--output "type=image,name=${IMAGE_REPOSITORY}:${CI_COMMIT_SHA},push=true"
- printf 'IMAGE_REPOSITORY=%s\nIMAGE_TAG=%s\n' "${IMAGE_REPOSITORY}" "${CI_COMMIT_SHA}" > image.env
artifacts:
reports:
dotenv: image.env이 예제는 이미지의 원본 커밋을 식별하기 위해 전체 CI_COMMIT_SHA를 사용합니다. 짧은 SHA도 실습에는 편하지만, 충돌 가능성을 피하고 원본 커밋을 명확히 하려면 전체 SHA 또는 OCI digest를 사용해야 합니다. 더 높은 보증 수준이 필요하면 BuildKit metadata에서 digest를 가져와 Chart Repo에 repository@sha256:... 형태로 기록합니다.
rootless BuildKit은 privileged daemon을 요구하지 않지만 모든 Runner에서 자동으로 동작한다는 뜻은 아닙니다. 사용자 namespace, seccomp/AppArmor, Runner executor 정책이 rootless worker를 허용해야 합니다. 실패하면 권한을 넓히기 전에 Runner 운영 정책과 GitLab 공식 요구사항을 확인합니다.
UV_TEST_IMAGE와 BUILDKIT_IMAGE에는 조직이 검증한 tag가 아니라 가능하면 registry/repository@sha256:... 형식의 digest를 넣습니다. 예제 안에서 도구 버전을 암묵적으로 최신값으로 갱신하지 않기 위한 장치입니다.
3단계: Chart Repo에 이미지와 새 RayJob 식별자를 기록한다
values.yaml의 핵심 값은 다음과 같습니다.
training:
image:
repository: harbor.lab.example.com/mlops/digits-trainer
tag: a1b2c3d4e5f6
runId: a1b2c3d4e5f6
source:
commit: a1b2c3d4e5f6
serving:
enabled: false
model:
storageUri: ""runId를 RayJob 이름에 포함하면 동일한 immutable job spec을 새 이름으로 생성할 수 있습니다.
metadata:
name: {{ printf "%s-%s" .Release.Name .Values.training.runId | trunc 63 | trimSuffix "-" }}
labels:
app.kubernetes.io/name: {{ .Release.Name | quote }}
mlops.example.com/source-commit: {{ .Values.training.source.commit | quote }}CI는 토큰을 URL에 넣지 않고 GIT_ASKPASS로 전달합니다. 동시에 여러 pipeline이 같은 파일을 갱신하지 않도록 resource_group을 둡니다.
write-training-state:
stage: writeback
image: alpine:3.21
needs:
- job: build-training-image
artifacts: true
rules:
- <<: *training_changes
resource_group: ml-platform-gitops-writeback
variables:
CHART_REPO_URL: https://gitlab.lab.example.com/mlops/ml-platform-gitops.git
CHART_VALUES_FILE: charts/digits-ml/values.yaml
before_script:
- apk add --no-cache git yq
- git config --global user.name "mlops-ci"
- git config --global user.email "mlops-ci@lab.example.com"
script:
- |
set -eu
export GITLAB_USER=oauth2
export GITLAB_PASSWORD="${CHART_REPO_TOKEN}"
cat > /tmp/git-askpass <<'EOF'
#!/bin/sh
case "$1" in
*Username*) printf '%s\n' "$GITLAB_USER" ;;
*Password*) printf '%s\n' "$GITLAB_PASSWORD" ;;
esac
EOF
chmod 700 /tmp/git-askpass
export GIT_ASKPASS=/tmp/git-askpass
export GIT_TERMINAL_PROMPT=0
git clone "${CHART_REPO_URL}" gitops
cd gitops
export IMAGE_REPOSITORY IMAGE_TAG CI_COMMIT_SHA
yq -i '.training.image.repository = strenv(IMAGE_REPOSITORY)' "${CHART_VALUES_FILE}"
yq -i '.training.image.tag = strenv(IMAGE_TAG)' "${CHART_VALUES_FILE}"
yq -i '.training.runId = strenv(CI_COMMIT_SHA)' "${CHART_VALUES_FILE}"
yq -i '.training.source.commit = strenv(CI_COMMIT_SHA)' "${CHART_VALUES_FILE}"
git add "${CHART_VALUES_FILE}"
git diff --cached --quiet && exit 0
git commit -m "train: deploy ${CI_COMMIT_SHA} [skip ci]"
git push origin HEAD:main[skip ci]는 Chart Repo 자체의 불필요한 파이프라인 재귀를 막습니다. 대신 Chart Repo에서 Helm lint와 정책 검사를 반드시 실행해야 한다면, skip을 쓰지 말고 해당 저장소의 pipeline이 코드 저장소를 다시 호출하지 않도록 규칙을 설계합니다.
실제 운영에서는 CI가 main에 바로 push하기보다 merge request를 만들고 정책 검사·승인을 거치게 할 수 있습니다. 개발 환경은 자동 write-back, 운영 환경은 MR 승인으로 나누는 방식이 현실적입니다.
4단계: Argo CD가 Git을 pull해 RayJob을 만든다
Argo CD Application도 Git에서 관리합니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: digits-ml
namespace: argocd
spec:
project: ml-platform
source:
repoURL: https://gitlab.lab.example.com/mlops/ml-platform-gitops.git
targetRevision: main
path: charts/digits-ml
helm:
valueFiles:
- values.yaml
destination:
server: https://kubernetes.default.svc
namespace: mlops-demo
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=trueArgo CD는 기본 폴링 또는 GitLab webhook으로 변경을 감지합니다. webhook은 감지 시간을 줄일 뿐, Git이 원하는 상태라는 원칙은 변하지 않습니다. CI에 Argo CD 관리자 암호를 넣고 직접 sync할 필요가 없습니다.
배포 후에는 GitOps 동기화 상태, 배포된 RayJob과 이미지, 실제 학습 로그를 차례로 확인합니다.
# GitOps 상태
argocd app get digits-ml
# 배포된 RayJob과 이미지
kubectl get rayjob -n mlops-demo
kubectl get rayjob -n mlops-demo -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.rayClusterSpec.headGroupSpec.template.spec.containers[0].image}{"\n"}{end}'
# 학습 로그
kubectl get pods -n mlops-demo
kubectl logs -n mlops-demo -l ray.io/node-type=head --tail=2005단계: RayJob은 candidate를 만들고 MLflow에 기록한다
학습이 성공하면 모델 후보를 등록하고, 배포 승인을 위한 검증 상태를 태그로 남깁니다. 학습 완료와 배포 승인 여부를 별도로 확인하기 위한 기록입니다.
import mlflow
import mlflow.sklearn
from mlflow.models import infer_signature
with mlflow.start_run() as run:
# 학습과 평가 코드 생략
mlflow.log_metric("accuracy", accuracy)
mlflow.set_tags(
{
"validation_status": "pending",
"source.git_commit": git_commit,
"runtime.training_image": training_image,
}
)
model_info = mlflow.sklearn.log_model(
sk_model=model,
artifact_path="model",
signature=infer_signature(x_train, model.predict(x_train)),
registered_model_name="digits-classifier",
)
print({"run_id": run.info.run_id, "model_uri": model_info.model_uri})평가 자동화는 run의 메트릭과 모델 버전을 읽고, 기준을 통과한 경우에만 validation_status=passed와 champion 별칭을 설정할 수 있습니다.
import os
from mlflow import MlflowClient
client = MlflowClient()
model_name = "digits-classifier"
candidate_version = os.environ["CANDIDATE_MODEL_VERSION"]
candidate = client.get_model_version(model_name, candidate_version)
run = client.get_run(candidate.run_id)
accuracy = run.data.metrics["accuracy"]
if accuracy < float(os.getenv("MIN_ACCURACY", "0.95")):
client.set_model_version_tag(model_name, candidate_version, "validation_status", "failed")
raise SystemExit(f"accuracy gate failed: {accuracy}")
client.set_model_version_tag(model_name, candidate_version, "validation_status", "passed")
client.set_registered_model_alias(model_name, "champion", candidate_version)메트릭 하나만으로 운영 승격을 결정하는 것은 단순화된 예입니다. 실제로는 데이터 품질, 공정성, 지연, 메모리, 보안 검사와 사람 승인이 포함될 수 있습니다.
6단계: 승인된 모델 URI를 Git에 기록해 KServe로 보낸다
MLflow의 champion 별칭은 승인 대상을 찾기 편하지만 가변 포인터입니다. 배포 Git에는 평가 당시 확인한 불변 S3 URI를 기록합니다.
serving:
enabled: true
model:
format: mlflow
runtime: kserve-mlserver
storageUri: s3://ml-models/digits/sha256-8dd4...b91/model
mlflowModelVersion: "12"
sourceRunId: 4c87...e19
minReplicas: 1Chart template은 이를 InferenceService로 만듭니다.
{{- if .Values.serving.enabled }}
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: {{ .Release.Name }}-predictor
annotations:
mlops.example.com/mlflow-model-version: {{ .Values.serving.model.mlflowModelVersion | quote }}
mlops.example.com/source-run-id: {{ .Values.serving.model.sourceRunId | quote }}
spec:
predictor:
minReplicas: {{ .Values.serving.minReplicas }}
serviceAccountName: kserve-model-reader
model:
modelFormat:
name: {{ .Values.serving.model.format }}
runtime: {{ .Values.serving.model.runtime }}
protocolVersion: v2
storageUri: {{ .Values.serving.model.storageUri | quote }}
{{- end }}승격 자동화가 Chart Repo에 MR을 만들 때 본문에 다음 근거를 포함하면 리뷰가 쉬워집니다.
- MLflow run URL과 model version
- 기존 champion과 candidate의 평가 비교
- 학습 image digest와 Git commit
- dataset version
- 통합 테스트 결과
- 롤백 대상 모델 URI
전체 흐름의 완료 증거
| 단계 | 완료 증거 |
|---|---|
| 코드 검증 | 테스트 결과와 lockfile check 성공 |
| 이미지 빌드 | Harbor의 commit SHA tag와 digest |
| 학습 배포 | Chart Repo의 write-back commit |
| 클러스터 반영 | Argo CD Synced/Healthy와 RayJob UID |
| 학습 완료 | RayJob Complete/SUCCEEDED |
| 모델 기록 | MLflow run ID, 메트릭, 모델 버전 |
| 모델 승인 | 검증 태그, alias 변경, 승인 기록 |
| 서빙 배포 | 모델 URI를 바꾼 GitOps commit |
| 추론 준비 | InferenceService Ready=True와 smoke test |
“파이프라인이 초록색이었다”는 결과와 함께 각 단계의 증거를 연결해 남겨야 합니다. 그래야 어떤 코드와 이미지로 학습했고, 어떤 모델을 승인해 서빙에 적용했는지 확인할 수 있습니다.
실패 지점별 문제 해결
BuildKit이 권한 오류로 시작하지 않는다
Runner가 rootless container에 필요한 사용자 namespace나 보안 프로필을 허용하는지 확인합니다. --oci-worker-no-process-sandbox를 썼는데도 실패한다면 임의로 privileged를 켜기 전에 Runner executor 설정과 노드 커널 정책을 점검합니다.
Harbor push가 401 또는 403이다
robot 계정의 project 범위와 push 권한, registry hostname, 인증서 신뢰 체인을 확인합니다. CI 로그에 암호를 출력하지 말고 GitLab 변수의 masked/protected 설정도 확인합니다.
Chart Repo push가 거절된다
보호 브랜치 정책, project access token의 역할과 만료일, write_repository scope를 확인합니다. 운영 브랜치 직접 push가 금지된 경우 오류가 아니라 정책대로 MR 방식으로 바꿔야 합니다.
Argo CD는 Synced인데 새 RayJob이 없다
helm template digits-ml ./charts/digits-ml -f ./charts/digits-ml/values.yaml
argocd app manifests digits-ml
kubectl get rayjob -n mlops-demorunId가 바뀌었는지, template 조건이 true인지, Application이 올바른 path와 value file을 보는지 확인합니다.
모델 승격은 됐지만 KServe가 이전 모델을 쓴다
MLflow alias만 바꾸고 Git의 storageUri를 바꾸지 않았는지 확인합니다. 이 설계에서 KServe의 실제 배포 원본은 GitOps 값입니다. Argo CD revision, InferenceService annotation, storage initializer 로그를 차례로 비교합니다.
보안과 운영 원칙
- Code Repo CI 계정은 Kubernetes 관리자 권한을 갖지 않습니다.
- Harbor robot 계정은 특정 project에 push만 허용하고 사람 계정을 재사용하지 않습니다.
- Chart Repo token은 해당 저장소 write 권한만 갖고 짧은 만료·주기적 회전을 적용합니다.
- CI 로그에
set -x를 켜지 않고 token이 포함된 URL을 만들지 않습니다. - Argo CD는 AppProject로 허용 repository, cluster, namespace, resource kind를 제한합니다.
- 운영 승격은 merge request, CODEOWNERS, 서명된 commit 같은 승인 경계를 둡니다.
- 이미지와 모델은 각각 digest/checksum으로 고정하며 두 식별자를 MLflow와 Git 양쪽에 남깁니다.
- 자동 prune이 완료된 학습 기록까지 없애지 않도록 RayJob 수명과 로그·아티팩트 보존 정책을 분리합니다.
정리
GitLab CI는 코드를 검증하고 rootless BuildKit으로 이미지를 만듭니다. Harbor는 그 불변 이미지를 보관합니다. CI는 Chart Repo에 새 이미지와 RayJob 식별자를 기록하고, Argo CD는 Git을 pull해 학습을 실행합니다. RayJob은 모델 후보를 MLflow에 남기며, 별도의 평가·승인 절차를 통과한 모델 URI만 다시 Git에 기록됩니다. 마지막으로 Argo CD가 KServe를 갱신합니다.
이 구조에서는 적용할 원하는 상태를 Git에 기록하고, 각 단계의 불변 식별자를 다음 단계에 전달하며, 학습 성공과 모델 승격을 구분합니다. 이 세 가지를 유지해야 자동화된 흐름에서도 배포 대상과 승인 근거를 확인할 수 있습니다.
다음 글에서는 이 설계를 작은 Digits 분류 모델로 처음부터 끝까지 직접 구현합니다.
이전 글: 재현 가능한 ML을 위한 스토리지와 버전 관리
다음 글: RayJob·MLflow·KServe 최종 실습