์์ ๊ธ์์ MLOps ํ๋ซํผ 1์ฐจ ์ํคํ ์ฒ๋ฅผ ์ ๋ฆฌํ๋ค. 1์ฐจ๋ Airflow๊ฐ ํ์ต๋ถํฐ ๋ฐฐํฌ๊น์ง ์ง์ ์ค์ผ์คํธ๋ ์ด์ ํ๋ ๊ตฌ์กฐ์๋ค. 2์ฐจ์์๋ ์ด๊ฑธ ๋ ๊ฐ๋๋ก ๊ฐ์์์๋ค. โ ๋ฌด๊ฑฐ์ด Airflow๋ฅผ ๊ฑท์ด๋ด๊ณ ์คํ์ Kafka ์ด๋ฒคํธ๋ก ์์ํ๊ณ , โก ๋น๋·๋ฐฐํฌ๋ ์ง์ ํ์ง ์๊ณ DevOps ํ์ Jenkins·ArgoCD ์ธํ๋ผ๋ฅผ ๊ทธ๋๋ก ์ฌ์ฌ์ฉํ๋ค. ์ด ๊ธ์ ๊ทธ ์ ํ์ ์ธํ๋ผ ๊ตฌ์ฑ๊ณผ ์ค์ ์ฝ๋๋ก ์ ๋ฆฌํ๋ค.
ํต์ฌ ์์ฝ
- 1์ฐจ: MLOps ๋ฐฑ์๋ ์ฝ์ด(FastAPI)๊ฐ Airflow DAG๋ฅผ ํธ๋ฆฌ๊ฑฐํ๊ณ , KubernetesPodOperator๋ก ํ์ต·ํ๊ฐ·๋ฐฐํฌ๋ฅผ ํ ๊ทธ๋ํ ์์์ ๋๋ ธ๋ค.
- 2์ฐจ: MLOps ๋ฐฑ์๋๋ "์ปค๋งจ๋๋ฅผ Kafka๋ก ๋ฐํํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ Kafka๋ก ๋ฐ๋" ์ญํ ๋ง ํ๋ค. ๋ฌด๊ฑฐ์ด ์ผ์ ๊ฐ ์์ด์ ํธ๊ฐ ๋๋ ๋งก๋๋ค.
- ํ์ต = Kafka
RUN_JOB์ปค๋งจ๋ ๋ฐํ → MLOps ์ก ์์ด์ ํธ๊ฐ K8s Job ์คํ. - ๋น๋ = DevOps์ ์์ → Jenkins๊ฐ ์ด๋ฏธ์ง ๋น๋.
- ๋ฐฐํฌ = DevOps ๋ฐฐํฌ ์์ด์ ํธ๊ฐ GitOps ๋ ํฌ์ image tag๋ฅผ ๋ฐ๊พธ๊ณ → ArgoCD๊ฐ ํด๋ฌ์คํฐ๋ฅผ ๊ทธ ์ํ๋ก ๋๊ธฐํ.
- ๋ชจ๋ ๊ฒฐ๊ณผ๋ ๋ค์ Kafka๋ก ๋์์,
commandId๋ก ๋งค์นญํด ์ํ๋ฅผ ๊ฐฑ์ ํ๋ค(๋ฉฑ๋ฑ).
1. ๋ฐฐ๊ฒฝ — 1์ฐจ Airflow์ ํ๊ณ
1์ฐจ์์ Airflow๋ ๊ฐ๋ ฅํ๋ค. ํ์ต→ํ๊ฐ→์กฐ๊ฑด→๋ฐฐํฌ๋ฅผ DAG ํ ์ฅ์ผ๋ก ํํํ ์ ์์์ผ๋๊น. ํ์ง๋ง ์ด์ํ๋ฉด์ ๋ถ๋ด์ด ์์๋ค.
- Airflow ์์ฒด๊ฐ ๋ฌด๊ฒ๋ค. ์ค์ผ์ค๋ฌ·์น์๋ฒ·๋ฉํDB๋ฅผ ์์ ๋์์ผ ํ๋ค. ๋จ์ผ ๋ ธ๋ ๊ฐ๋ฐ ํด๋ฌ์คํฐ์์๋ ์ด ์์ฃผ ๋น์ฉ์ด ์ปธ๋ค.
- ๋์ DAG๊ฐ ๋ณต์กํ๋ค. ์ฌ์ฉ์๊ฐ ์ง ํ์ดํ๋ผ์ธ์ ๋ฐํ์ DAG๋ก ๋ณํํ๊ณ , ๋ฉํธ๋ฆญ์ XCom์ผ๋ก ๋๊ธฐ๊ณ , Pod ์คํฌ๋ฆฝํธ ์์์ ๋ชจ๋ธ ๋ฑ๋ก๊น์ง ์ํค๋ ํ๋ฆ์ด ์์ด ๋ง์ด ๊ฐ๋ค.
- MSA์ธ๋ฐ ์ธํ๋ผ๋ฅผ MLOps๊ฐ ๋ค๊ณ ์์๋ค. ๋น๋(Jenkins ํธ๋ฆฌ๊ฑฐ)์ ๋ฐฐํฌ(์๋น)๋ฅผ Airflow DAG ์์ ์ฑ์ฌ๋ฃ์๋๋ฐ, ์ฌ์ค ์ ํ(DevOps)์ด ์ด๋ฏธ ๋น๋·๋ฐฐํฌ ์ธํ๋ผ๋ฅผ ์ ๊ฐ์ถ๊ณ ์์๋ค.
๊ทธ๋์ 2์ฐจ์ ๋ฐฉํฅ์ ๋ถ๋ช ํด์ก๋ค. "์ง์ ๋ค ํ์ง ๋ง๊ณ , ์ด๋ฒคํธ๋ก ์์ํ์."
2. ํต์ฌ ์์ด๋์ด — ์ด๋ฒคํธ๋ก ์์ํ๊ธฐ
๋น์ : 1์ฐจ๋ ์ฃผ๋ฌธ๋ฐ๊ณ , ์๋ฆฌํ๊ณ , ๋ฐฐ๋ฌ๊น์ง ํผ์ ๋ค ํ๋ 1์ธ ์๋น์ด์๋ค. 2์ฐจ๋ ์ฃผ๋ฌธ ์ ํ(์ด๋ฒคํธ)๋ง ๋์ฐ๋ฉด, ์กฐ๋ฆฌ(๋น๋)์ ๋ฐฐ๋ฌ(๋ฐฐํฌ)์ ๊ฐ ์ ๋ฌธ ํํธ๊ฐ ๊ฐ์ ธ๊ฐ ์ฒ๋ฆฌํ๋ ๊ตฌ์กฐ๋ค. ํ(MLOps ๋ฐฑ์๋)์ ์ ํ๋ฅผ ๋ฃ๊ณ "๋์์ต๋๋ค(๊ฒฐ๊ณผ ์ด๋ฒคํธ)"๋ฅผ ๋ฐ๊ธฐ๋ง ํ๋ค. ๊ฐ ํํธ๋ ์๋ก๋ฅผ ์ง์ ํธ์ถํ์ง ์๊ณ ์ ํ(Kafka ํ ํฝ)๋ง ๋ณธ๋ค — ํ์ชฝ์ด ๋ฐ๋น ๋ ์ ํ๋ ํ์ ์์ฌ ๊ธฐ๋ค๋ฆฌ๋ฏ๋ก, ๋์จํ๊ฒ ๊ฒฐํฉ๋๋ค.
๊ตฌ์ฒด์ ์ผ๋ก ๋ ๊ฐ์ง๋ฅผ ๋ฐ๊ฟจ๋ค.
- ์ค์ผ์คํธ๋ ์ด์ : Airflow DAG → Kafka ์ด๋ฒคํธ(์ปค๋งจ๋). MLOps ๋ฐฑ์๋๊ฐ ์ปค๋งจ๋๋ฅผ ๋ฐํํ๋ฉด, ์ก ์์ด์ ํธ๊ฐ ์๋นํด K8s Job์ ๋์ด๋ค.
- ๋น๋·๋ฐฐํฌ: MLOps ์์ฒด ์ฒ๋ฆฌ → DevOps ์ธํ๋ผ ์ฌ์ฌ์ฉ. ๋น๋๋ DevOps Jenkins, ๋ฐฐํฌ๋ DevOps ArgoCD(GitOps).
3. ์ธํ๋ผ ๊ตฌ์ฑ — ๋๊ฐ ๋ฌด์์ ํ๋
์ค์ฌ์ Kafka(์ด๋ฒคํธ ๋ฒ์ค)๊ฐ ์๊ณ , ์์์ผ๋ก MLOps ๋ฐฑ์๋์ ์ฌ๋ฌ ์์ด์ ํธ๊ฐ ํ ํฝ์ ์ฃผ๊ณ ๋ฐ๋๋ค.
| ๊ตฌ์ฑ์์ | ์ญํ |
|---|---|
| MLOps ๋ฐฑ์๋(Spring) | ์ปค๋งจ๋ ๋ฐํ + ๊ฒฐ๊ณผ ์์ . ๋ฉํ·์ํ๋ฅผ DB์ ๊ธฐ๋ก. |
| Kafka | ์ด๋ฒคํธ ๋ฒ์ค. ์ปค๋งจ๋/๊ฒฐ๊ณผ ํ ํฝ์ ์ค๊ณ. |
| MLOps ์ก ์์ด์ ํธ(Python) | RUN_JOB ์๋น → K8s ํ์ต Job ์คํ → ๊ฒฐ๊ณผ ํ์ . |
| DevOps ๋น๋ ์์ด์ ํธ | ๋น๋ ์์ฒญ ์๋น → Jenkins๋ก ์ด๋ฏธ์ง ๋น๋ → Docker Registry push → ๊ฒฐ๊ณผ ํ์ . |
| DevOps ๋ฐฐํฌ ์์ด์ ํธ(Python/FastAPI) | ๋ฐฐํฌ ์์ฒญ ์๋น → GitOps image tag ๊ฐฑ์ → ArgoCD sync → health check → ๊ฒฐ๊ณผ ํ์ . |
| ArgoCD | GitOps ๋ ํฌ๋ฅผ ๊ฐ์ํ๋ค ๋ณ๊ฒฝ๋๋ฉด ํด๋ฌ์คํฐ๋ฅผ ๊ทธ ์ํ๋ก ๋๊ธฐํ. |
์ฃผ๊ณ ๋ฐ๋ ์ฃผ์ Kafka ํ ํฝ์ ์ด๋ ๋ค.
service.mlops.command # MLOps ๋ฐฑ์๋ → ์ก ์์ด์ ํธ (RUN_JOB ๋ฑ)
service.mlops.result # ์ก ์์ด์ ํธ → MLOps ๋ฐฑ์๋ (ํ์ต ๊ฒฐ๊ณผ)
service.build.result # DevOps ๋น๋ ์์ด์ ํธ → MLOps ๋ฐฑ์๋ (๋น๋ ๊ฒฐ๊ณผ)
service.deploy.result # DevOps ๋ฐฐํฌ ์์ด์ ํธ → MLOps ๋ฐฑ์๋ (๋ฐฐํฌ ๊ฒฐ๊ณผ)
4. ํ๋ฆ โ ํ์ต ์คํ — Kafka RUN_JOB
์คํ
์ด์ง(ํ์ต ๋ฑ)๋ฅผ ์คํํ๋ฉด, ๋ฐฑ์๋๋ Airflow๋ฅผ ๋ถ๋ฅด์ง ์๊ณ RUN_JOB ์ปค๋งจ๋ ํ๋๋ฅผ Kafka๋ก ๋์ง๋ค.
// PipelineExecSvcImpl.runStage() — RUN_JOB ์ปค๋งจ๋ ๋ฐํ (์์ง)
Map<String,Object> payload = new LinkedHashMap<>();
payload.put("mlopsRunId", stageRunInternalId);
payload.put("jobType", stage.getType()); // TRAIN / EVALUATE ...
payload.put("templateName", "python-ml");
payload.put("artifactRef", image); // ์คํํ ์ก ์ด๋ฏธ์ง
payload.put("command", cmdArgs[0]);
payload.put("args", cmdArgs[1]);
payload.put("namespace", "mlops-job");
payload.put("resource", resource); // CPU/GPU/๋ฉ๋ชจ๋ฆฌ
payload.put("envVars", envVars); // MinIO/MLflow ์ ๊ทผ์ ๋ณด ๋ฑ
Map<String,Object> command = new LinkedHashMap<>();
command.put("commandId", commandId);
command.put("serviceId", serviceId);
command.put("type", "RUN_JOB"); // ์ปค๋งจ๋ ํ์
command.put("payload", payload);
commandPublisher.publish(String.valueOf(serviceId), command); // serviceId๋ฅผ key๋ก ๋ฐํ
๋ฐํ ์์ฒด๋ ์์ ํผ๋ธ๋ฆฌ์ ๊ฐ ๋ด๋นํ๋ค. JSON์ผ๋ก ์ง๋ ฌํํด ํ ํฝ์ ๋ณด๋ด๋ ๊ฒ ์ ๋ถ๋ค.
// MlopsCommandPublisher.publish()
public void publish(String key, Map<String,Object> message) {
try {
String json = objectMapper.writeValueAsString(message);
kafkaTemplate.send(topic, key, json); // topic = service.mlops.command
log.info("[MlopsCommand] ๋ฐํ — type={}", message.get("type"));
} catch (Exception e) {
throw new RuntimeException("Kafka ์ปค๋งจ๋ ๋ฐํ ์คํจ", e);
}
}
๊ทธ๋ฌ๋ฉด ์ก ์์ด์ ํธ๊ฐ ์ด ์ปค๋งจ๋๋ฅผ ์๋นํด python-ml ํ
ํ๋ฆฟ์ผ๋ก K8s Job์ ๋ง๋ค์ด ์คํํ๊ณ , ๋๋๋ฉด service.mlops.result๋ก ๊ฒฐ๊ณผ๋ฅผ ๋๋ ค๋ณด๋ธ๋ค. ๋ฐฑ์๋๋ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ์ํ๋ฅผ ๊ฐฑ์ ํ๋ค.
// MlopsResultListener — ํ์ต ๊ฒฐ๊ณผ ์์
@KafkaListener(topics = "${kafka.topic.mlops-result}", ...)
public void onMlopsResult(String rawJson) {
JsonNode root = objectMapper.readTree(rawJson);
String type = root.path("type").asText(null);
if ("JOB_RUN_RESULT".equals(type)) { // ์คํ
์ด์ง ์คํ ๊ฒฐ๊ณผ
handleJobRunResult(root); // commandId๋ก ๋งค์นญํด DB ์ํ ๊ฐฑ์
}
...
}
Airflow์ ์ค์ผ์ค๋ฌ·DAG·XCom์ด ํต์งธ๋ก ์ฌ๋ผ์ง๊ณ , "๋ฐํ → ์๋น → ๊ฒฐ๊ณผ ํ์ "์ด๋ผ๋ ๋จ์ํ ํ์ดํ๋ง ๋จ์๋ค.
5. ํ๋ฆ โก ๋น๋ — DevOps Jenkins์ ์์
์ด๋ฏธ์ง ๋น๋๋ MLOps๊ฐ ์ง์ ํ์ง ์๋๋ค. DevOps ๋น๋ API๋ฅผ ํธ์ถํด ์์ํ๊ณ , ๋์์ฌ ๊ฒฐ๊ณผ๋ฅผ ๋งค์นญํ commandId๋ง ๊ธฐ๋กํด ๋๋ค.
// PipelineBuildSvcImpl.triggerStageBuild() (์์ง)
String subPath = slug + "/" + stageDir; // ์: mlsvc06023/train
DevopsPipelineTriggerReqDto req = DevopsPipelineTriggerReqDto.builder()
.branch(branch).triggerType("MANUAL")
.subPath(subPath).jobType(jobType).build();
Long devopsRunId = devopsApiService.triggerBuild(serviceId, req); // DevOps์ ์์
String commandId = "build." + devopsRunId; // ๊ฒฐ๊ณผ ๋งค์นญ์ฉ ํค
// ๋น๋ ์คํ
์ด์ง๋ฅผ DB์ BUILDING ์ผ๋ก ๊ธฐ๋ก (devopsRunId๋ฅผ soft-ref)
params.put("commandId", commandId);
params.put("devopsPipelineRunId", devopsRunId);
pipelineBuildDao.regBuildStage(params);
DevOps Jenkins๊ฐ ๋น๋๋ฅผ ๋๋ด๋ฉด service.build.result๋ก ๊ฒฐ๊ณผ๊ฐ ์ค๊ณ , ๋ฐฑ์๋๋ commandId="build.{runId}"๋ก ์ด๋ ์คํ
์ด์ง์ธ์ง ์ฐพ์ ์ํ์ ์ด๋ฏธ์ง ๊ฒฝ๋ก๋ฅผ ์ฑ์ด๋ค. DevOps๊ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ ๋ฒ ๋ณด๋ด๋ ์์ ํ๊ฒ ๋ฉฑ๋ฑ ์ฒ๋ฆฌํ๋ค.
// DevopsBuildResultListener (์์ง)
@KafkaListener(topics = "${kafka.topic.devops-build-result}", ...)
public void handleBuildResult(String rawJson) {
String commandId = root.path("commandId").asText(null);
long devopsRunId = Long.parseLong(commandId.substring("build.".length()));
Map<String,Object> stageRow = pipelineBuildDao.getBuildStageByDevopsRunId(devopsRunId);
if ("SUCCEEDED".equals(stageRow.get("status"))) return; // ๋ฉฑ๋ฑ — ์ด๋ฏธ ๋๋ ๊ฑด skip
String image = root.path("result").path("imageFullPath").asText(null);
pipelineBuildDao.modBuildStageResult(devopsRunId, "SUCCEEDED", image, ...);
}
6. ํ๋ฆ โข ๋ฐฐํฌ — DevOps GitOps + ArgoCD (์ด ๊ธ์ ํต์ฌ)
2์ฐจ์์ ๊ฐ์ฅ ๋ง์ด ๋ฐ๋ ๊ฒ ๋ฐฐํฌ๋ค. 1์ฐจ๋ Airflow DAG๊ฐ ๋ฐฐํฌ๊น์ง ์ง์ ํ์ง๋ง, 2์ฐจ๋ DevOps์ GitOps + ArgoCD ์ธํ๋ผ ์์ ์น์๋ค. MLOps๋ "๋ฐฐํฌํด์ค"๋ผ๊ณ ์ด๋ฒคํธ๋ง ๋์ง๊ณ , ์ค์ ํด๋ฌ์คํฐ์ ํ๋๋ฅผ ์ฌ๋ฆฌ๋ ๊ฑด ArgoCD๊ฐ ํ๋ค. ์ด ์ฅ์ ๊ทธ ๋ฐฐํฌ๊ฐ ํ ๋จ๊ณ์ฉ ์ด๋ป๊ฒ ์งํ๋๋์ง๋ฅผ, ์ค์ ArgoCD ์ค์ ·์ค์ GitOps ๋ ํฌ·์ค์ ์์ด์ ํธ ์ฝ๋๋ก ํ์ด๋ณธ๋ค.
๋น์ : GitOps๋ ์ง ์์ ์๋ ์จ๋์กฐ์ ๊ธฐ(๋ณด์ผ๋ฌ thermostat)๋ค. ์ฐ๋ฆฌ๋ ๋ฒฝ์ ๋ถ์ ํจ๋์ "24๋"๋ผ๋ ๋ชฉํ๋ง ์ ์ด๋๋ค(= Git ๋ ํฌ์ "์ด ์๋น์ค๋ ์ด ์ด๋ฏธ์ง๋ก ๋ ์์ด์ผ ํ๋ค"๊ณ ์ ๊ธฐ). ๊ทธ๋ฌ๋ฉด ์กฐ์ ๊ธฐ๊ฐ ์ค์ ๋ฐฉ ์จ๋๋ฅผ ๊ณ์ ์ฌ๋ฉด์, ๋ชฉํ๋ณด๋ค ์ถ์ฐ๋ฉด ๋ณด์ผ๋ฌ๋ฅผ ์ผ๊ณ ๋์ฐ๋ฉด ๋๋ค(= ArgoCD๊ฐ ํด๋ฌ์คํฐ ์ค์ ์ํ๋ฅผ ๋ชฉํ์ ๋ง์ถฐ ์๋ ด). ์ฌ๋์ด ์ง์ ๋ณด์ผ๋ฌ ๋ฐธ๋ธ๋ฅผ ๋๋ฆฌ์ง(=
kubectl apply์น์ง) ์๋๋ค. ๊ทธ๋์ ๋ฐฐํฌ = "ํจ๋์ ์ซ์ ํ ์ค(์ด๋ฏธ์ง ํ๊ทธ)์ ๋ฐ๊พธ๋ ์ผ"์ด ๋๋ค.
6-1. ๋ฑ์ฅ์ธ๋ฌผ ์ — deploy ๋ ํฌ · ๋ฐฐํฌ ์์ด์ ํธ · ArgoCD
๋ฐฐํฌ์๋ ์ธ ์ฃผ์ฐ์ด ์๋ค. ์ด๋ฆ๋ง ๋จผ์ ์ตํ์.
- deploy ๋ ํฌ(GitOps ๋ ํฌ) = "๋ชฉํ ์ํ๊ฐ ๋ถ๋ ๊ฒ์ํ". ์๋น์ค๋ง๋ค ๋ฐ๋ก ํ๋์ฉ ์๋ Git ๋ ํฌ๋ค. ์ฌ๊ธฐ
deployment.yaml(์ด๋ค ์ด๋ฏธ์ง๋ฅผ ๋ช ๊ฐ ๋์ธ์ง)์ด ๋ค์ด์๋ค. ์ด ๊ฒ์ํ์ด ๊ณง "์ ๋ต"์ด๋ค. - ๋ฐฐํฌ ์์ด์ ํธ(Python) = "๊ฒ์ํ์ ๊ณ ์ณ ๋ถ์ด๊ณ ์กฐ์ ๊ธฐ๋ฅผ ๊นจ์ฐ๋ ์ฌ๋". ๋ฐฐํฌ ์ปค๋งจ๋๋ฅผ ๋ฐ์ผ๋ฉด ๊ฒ์ํ์ ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ ์ ๋ฒ์ ์ผ๋ก ๋ฐ๊ฟ ์ปค๋ฐํ๊ณ , ArgoCD์๊ฒ "์ด๊ฑฐ ๋ด" ํ๊ณ ์๋ฆฐ๋ค.
- ArgoCD = "๊ฒ์ํ๊ณผ ์ค์ ์ฃผ๋ฐฉ์ ๋์กฐํ๋ ์๋ ์ฃผ๋ฐฉ์ฅ". ๊ฒ์ํ(Git)๊ณผ ์ค์ ํด๋ฌ์คํฐ๋ฅผ ๊ณ์ ๋น๊ตํ๋ค, ๋ค๋ฅด๋ฉด ํด๋ฌ์คํฐ๋ฅผ ๊ฒ์ํ๋๋ก ๋ง์ถ๋ค(sync).
ํต์ฌ ์ฌ์ค ํ๋: ์๋น์ค ํ๋ = ArgoCD ์ฑ 1๊ฐ + deploy ๋ ํฌ 1๊ฐ๋ก 1:1 ๋์๋๋ค. ์ค์ ์ฐ๋ฆฌ ํด๋ฌ์คํฐ์์ ArgoCD ์ฑ์ ์กฐํํ๋ฉด ์ด๋ ๊ฒ ๋ฌถ์ฌ ์๋ค.
# ArgoCD ์ฑ ↔ deploy ๋ ํฌ ๋งคํ (์ค์ ์กฐํ ๊ฒฐ๊ณผ)
์ฑ ์ด๋ฆ : ml06083-mlsvc06083-dev
โ ์์ค ๋ ํฌ(deploy): .../platoops-gitops-deploy/ml06083-mlsvc06083-deploy.git
โ ๊ฒฝ๋ก : dev # ๋ ํฌ ์ dev/ ํด๋๋ฅผ ๋ณธ๋ค
โ ๋์ ๋ค์์คํ์ด์ค : mlsvc06083
โ ๋ ์๋ ์ด๋ฏธ์ง : 192.168.2.56:30500/ml06083-mlsvc06083/mlsvc06083:1
6-2. deploy ๋ ํฌ๋ ์์ค ์ฝ๋์ "๋ฐ๋ก" ์๋ค
์ฒ์ ํท๊ฐ๋ ธ๋ ๊ฒ ์ด๊ฑฐ๋ค. ์๋น์ค ์ฝ๋๊ฐ ์๋ ์์ค ๋ ํฌ์, ๋ฐฐํฌ ์ํ๊ฐ ์ ํ deploy ๋ ํฌ๋ ๋ณ๊ฐ๋ค. ์์ค ๋ ํฌ์๋ main.py·Dockerfile์ด ์๊ณ , deploy ๋ ํฌ์๋ K8s ๋งค๋ํ์คํธ๋ง ์๋ค. ์ ๊ตณ์ด ๋๋๊น?
- "์ฝ๋"์ "์ง๊ธ ๋ ์์ด์ผ ํ ์ํ"๋ ๋ค๋ฅธ ๊ด์ฌ์ฌ๋ค. ์ฝ๋ ์ปค๋ฐ์ด ๋ฐฐํฌ๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๊ณ , ๋ฐ๋๋ก ๋ฐฐํฌ(์ด๋ฏธ์ง ํ๊ทธ ๋ณ๊ฒฝ) ์ด๋ ฅ์ด ์์ค ํ์คํ ๋ฆฌ๋ฅผ ๋๋ฝํ์ง ์๋๋ค.
- ๋ฐฐํฌ ์ด๋ ฅ์ด Git ์ปค๋ฐ์ผ๋ก ๋จ๋๋ค. "์ธ์ ์ด๋ค ์ด๋ฏธ์ง๋ก ๋ฐ๊ฟจ๋"๊ฐ deploy ๋ ํฌ ์ปค๋ฐ ๋ก๊ทธ์ ๊ทธ๋๋ก ์ฐํ๋ค — ๋๋๋ฆฌ๊ธฐ(rollback)๋ ๊ทธ ์ปค๋ฐ์ ๋๋๋ฆฌ๋ฉด ๋๋ค.
deploy ๋ ํฌ์ ์ค์ ๋ด์ฉ์ ์ด๋ ๋ค. provisioning(์๋น์ค ์์ฑ) ๋จ๊ณ์์ ์๋ ์์ฑ๋๋ฉฐ, ์์ผ๋ก ์ ๋ง๋ ๋ค.
# deploy ๋ ํฌ: plato-admin/platoops-gitops-deploy/{ํ๋ก์ ํธ}-{์๋น์ค}-deploy
# โ dev/deployment.yaml ← ์๋ (์๋น ํ๋ ์ ์)
# โ dev/service.yaml ← ๊ทธ ์๋ (ํด๋ฌ์คํฐ ๋ด๋ถ ์ ๊ทผ์ฉ Service)
# ===== dev/deployment.yaml (์ค์ ) =====
apiVersion: apps/v1
kind: Deployment
metadata:
name: mlsvc06092
namespace: ml06092
labels: { app: mlsvc06092, project: ml06092, env: dev, runtime: python }
spec:
replicas: 1 # ํ๋ ๋ช ๊ฐ ๋์ธ์ง
selector:
matchLabels: { app: mlsvc06092, env: dev }
template:
metadata:
labels: { app: mlsvc06092, project: ml06092, env: dev, runtime: python }
spec:
containers:
- name: mlsvc06092
image: {๋ ์ง์คํธ๋ฆฌ}/ml06092-mlsvc06092/mlsvc06092:latest # โ
์ด ํ ์ค์ด ๋ฐฐํฌ ๋ ๋ฐ๋๋ค
imagePullPolicy: Always
ports:
- { name: http, containerPort: 8080 }
livenessProbe: # ์ด์์๋ (์ฃฝ์ผ๋ฉด K8s๊ฐ ์ฌ์์)
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe: # ํธ๋ํฝ ๋ฐ์ ์ค๋น๋๋ (์ ๋๋ฉด Service์์ ์ ์ธ)
httpGet: { path: /health, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests: { cpu: 256m, memory: 1Gi } # ์ต์ ๋ณด์ฅ
limits: { cpu: 1, memory: 2Gi } # ์ํ
imagePullSecrets:
- name: registry-pull-secret # ์ฌ์ค ๋ ์ง์คํธ๋ฆฌ ์ธ์ฆ
# ===== dev/service.yaml (์ค์ ) =====
apiVersion: v1
kind: Service
metadata: { name: mlsvc06092, namespace: ml06092 }
spec:
type: ClusterIP # ํด๋ฌ์คํฐ ๋ด๋ถ์์๋ง ์ ๊ทผ(8080)
ports:
- { name: http, port: 8080, targetPort: 8080, protocol: TCP }
selector: { app: mlsvc06092, env: dev } # ์ด ๋ผ๋ฒจ ๊ฐ์ง ํ๋๋ก ํธ๋ํฝ ์ ๋ฌ
์ด๋ฏธ์ง ํ ์ค์ ์ ๋ณด์. {๋ ์ง์คํธ๋ฆฌ}/{ํ๋ก์ ํธ}-{์๋น์ค}/{์๋น์ค}:{ํ๊ทธ} ํ์์ด๋ค. provisioning์ด ์ฒ์ ๋ง๋ค ๋ ํ๊ทธ๊ฐ :latest์ธ๋ฐ, ๋ฐฐํฌ๊ฐ ์ผ์ด๋ ๋๋ง๋ค ์ด :latest(๋๋ ์ด์ ๋ฒํธ)๊ฐ ์ ๋น๋ ๋ฒํธ(:1, :2 …)๋ก ๊ต์ฒด๋๋ค. ๊ทธ "๊ต์ฒด+์ปค๋ฐ"์ด ๋ฐฐํฌ ์์ด์ ํธ์ ์ผ์ด๊ณ , ๊ทธ๊ฑธ ArgoCD๊ฐ ๊ฐ์งํ๋ค.
6-3. ArgoCD๋ ์ค์ ๋ก ์ด๋ป๊ฒ ์์ง์ด๋ (์ ์ธํ ๋๊ธฐํ)
ArgoCD์ ์ฌ์ฅ์ "๋น๊ต ํ ์๋ ด(reconcile)" ๋ฃจํ๋ค. ์ฌ์ง ์๊ณ ์ด ์ธ ๊ฐ์ง๋ฅผ ๋ฐ๋ณตํ๋ค.
- โ ๋ชฉํ(desired) ์ฝ๊ธฐ: deploy ๋ ํฌ
dev/์ ๋งค๋ํ์คํธ = "์ด๋ ๊ฒ ๋ ์์ด์ผ ํ๋ค". - โก ์ค์ (live) ์ฝ๊ธฐ: ํด๋ฌ์คํฐ์ ์ง๊ธ ์ค์ ๋ก ๋ญ๊ฐ ๋ ์๋.
- โข ๋น๊ต → ์๋ ด: ๋์ด ๊ฐ์ผ๋ฉด
Synced. ๋ค๋ฅด๋ฉดOutOfSync→ (์๋ ๋๊ธฐํ๋ฉด) ํด๋ฌ์คํฐ๋ฅผ ๋ชฉํ๋๋ก ๊ณ ์น๋ค. ์ฌ๋์ดkubectl apply๋ฅผ ์น๋ ๊ทธ ๋์์ ArgoCD๊ฐ ๋์ ํ๋ค.
๊ทธ๋์ ๋ฐฐํฌ ์์ด์ ํธ๊ฐ deploy ๋ ํฌ์ ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ :1๋ก ๋ฐ๊ฟ ์ปค๋ฐํ๋ฉด → ArgoCD๊ฐ "์ด, ๋ชฉํ๋ :1์ธ๋ฐ ์ค์ ๋ :latest๋ค(OutOfSync)" → ์ ์ด๋ฏธ์ง๋ก ํ๋๋ฅผ ๊ฐ์๋ผ์ด๋ค → Synced. ๋ฐฐํฌ๊ฐ 'Git ์ปค๋ฐ ํ ๋ฒ'์ผ๋ก ์์๋๋ ์ด์ ๊ฐ ์ด๊ฑฐ๋ค.
์ํ๋ ๋ ์ถ์ผ๋ก ๋ณธ๋ค. ํท๊ฐ๋ฆฌ๊ธฐ ์ฌ์์ ๊ผญ ๊ตฌ๋ถํ์.
- sync = ๋ชฉํ(Git)์ ์ค์ (ํด๋ฌ์คํฐ)๊ฐ ์ผ์นํ๋? (
Synced/OutOfSync) — "๊ฒ์ํ๋๋ก ๋๋" - health = ๊ทธ ๋ฆฌ์์ค๊ฐ ์ ์ ๋์ํ๋? (
Healthy/Progressing/Degraded) — ํ๋๊ฐ Ready์ธ๊ฐ, ์ฆ "์์์ด ์ค์ ๋ก ์ ๋์ค๋"
๋ฐฐํฌ ์ฑ๊ณต = Synced + Healthy ๋ ๋ค. ์ฐ๋ฆฌ ์ค์ ์ฑ ํ ๊ฐ๋ฅผ ArgoCD์์ ๊ทธ๋๋ก ๋ ๋ณด๋ฉด ์ด๋ ๊ฒ ์๊ฒผ๋ค.
# ArgoCD Application: ml06083-mlsvc06083-dev (์ค์ ๊ตฌ์ฑ)
spec:
source: # ← ์ด๋(Git)๋ฅผ ๋ณด๋
repoURL: .../ml06083-mlsvc06083-deploy.git
path: dev # ๋ ํฌ ์ dev/ ํด๋
targetRevision: main # main ๋ธ๋์น
destination: # ← ์ด๋(ํด๋ฌ์คํฐ)์ ๋ฐฐํฌํ๋
server: https://kubernetes.default.svc
namespace: mlsvc06083
syncPolicy:
automated: { prune: true, selfHeal: true } # ์๋ ๋๊ธฐํ
syncOptions: [ CreateNamespace=true ]
status:
sync: Synced # ๋ชฉํ(Git) == ์ค์ (ํด๋ฌ์คํฐ)
health: Healthy # ํ๋ Ready, ์ ์ ๋์
resources: # ArgoCD๊ฐ ๊ด๋ฆฌํ๋ ์ค์ ๋ฆฌ์์ค
- Deployment/mlsvc06083
- Service/mlsvc06083
history: # ๋ฐฐํฌ ์ด๋ ฅ (๊ฐ ๋ฆฌ๋น์ = ๋๋๋ฆด ์ ์๋ ์ง์ )
- id=2 revision=9a70763f deployedAt=2026-06-07T22:08:10Z
syncPolicy ์ธ ์ต์
์ด ์ด์ ํธ์์ ํต์ฌ์ด๋ค.
automated: Git์ด ๋ฐ๋๋ฉด ์ฌ๋์ด sync ๋ฒํผ์ ์ ๋๋ฌ๋ ArgoCD๊ฐ ์์์ ๋ฐ์.prune: true: Git์์ ์ง์ด ๋ฆฌ์์ค๋ ํด๋ฌ์คํฐ์์๋ ์ญ์ (์ฐ๋ ๊ธฐ ์ ๋จ๊น).selfHeal: true: ๋๊ฐ ํด๋ฌ์คํฐ๋ฅผ ์์ผ๋ก ๋ฐ๊ฟ๋๋(kubectl edit๋ฑ) Git ์ํ๋ก ๋๋๋ฆฐ๋ค. "๊ฒ์ํ์ด ํญ์ ์ด๊ธด๋ค."CreateNamespace=true: ๋์ ๋ค์์คํ์ด์ค๊ฐ ์์ผ๋ฉด ๋ง๋ค์ด ์ค๋ค.
๋น์ ์ ๋ฆฌ: deploy ๋ ํฌ = ๋ฒฝ์ ๋ถ์ ๋ชฉํ ํจ๋, ArgoCD = ๊ฐ์ ์นด๋ฉ๋ผ ๋ฌ๋ฆฐ ์๋ ์ฃผ๋ฐฉ์ฅ. ์ฃผ๋ฐฉ์ฅ์ ํจ๋(Git)๊ณผ ์ฃผ๋ฐฉ(ํด๋ฌ์คํฐ)์ ๋์์์ด ๋์กฐํด์, ์๋(๊ฐ๋ฐ์)์ด ์ฃผ๋ฐฉ์ ๋ชฐ๋ ๋ค์ด๊ฐ ๋๋น๋ฅผ ๋ฐ๊ฟ๋๋(
selfHeal) ํจ๋๋๋ก ๋๋๋ ค ๋๋๋ค. ๊ทธ๋์ "์ง๊ธ ๋ฌด์์ด ๋ ์์ด์ผ ํ๋๊ฐ"์ ์ ์ผํ ์ ๋ต์ ํญ์ Git์ด๋ค.
6-4. ๋ฐฐํฌ ์์ด์ ํธ์ 6๋จ๊ณ (์ค์ ์ฝ๋)
๊ทธ๋ผ MLOps๊ฐ "๋ฐฐํฌํด์ค" ์ด๋ฒคํธ๋ฅผ ๋์ง ๋ค, ๋ฐฐํฌ ์์ด์ ํธ(DeployOrchestrator.execute())๊ฐ ์ ํํ ๋ฌด์จ ์ผ์ ํ๋. ๋ฑ 6๋จ๊ณ๋ค.
# deploy_orchestrator.py — ๋ฐฐํฌ 6๋จ๊ณ (์ค์ , ์ฃผ์ ๋ณด๊ฐ)
async def execute(self, command_id, service_id, payload):
gitops_repo_url = payload["gitopsRepoUrl"] # ์ด ์๋น์ค์ deploy ๋ ํฌ ์ฃผ์
gitops_path = payload.get("gitopsPath", ".") # ๋ณดํต "dev"
image_tag = payload["imageTag"] # ์๋ก ๋์ธ ๋น๋ ๋ฒํธ (์: "1")
namespace = payload["namespace"] # ์: "mlsvc06083"
app_name = payload["appName"] # ์: "ml06083-mlsvc06083-dev"
# [1] ArgoCD์ deploy ๋ ํฌ ๋ฑ๋ก (์ด๋ฏธ ์์ผ๋ฉด ํต๊ณผ) — "์ด ๊ฒ์ํ ์ฃผ์ ๊ธฐ์ตํด๋ฌ"
await self._argocd.add_repository(gitops_repo_url, username="oauth2", password=self._gitlab_token)
# [2] โ
deploy ๋ ํฌ์ ์ด๋ฏธ์ง ํ๊ทธ๋ฅผ ์ ๋ฒ์ ์ผ๋ก ๋ฐ๊ฟ ์ปค๋ฐ (= ๋ชฉํ ์ํ ๋ณ๊ฒฝ)
project_id = await self._gitops.get_project_id_by_url(gitops_repo_url)
await self._gitops.update_image_tag(project_id, "main", f"{gitops_path}/deployment.yaml", image_tag)
# [3] ArgoCD ์ฑ์ด ์์ผ๋ฉด ์์ฑ (์์ผ๋ฉด ๊ทธ๋๋ก ๋ ) — "์ด ๊ฒ์ํ์ ๊ฐ์ํ๋ผ"
existing = await self._argocd.get_application_status(app_name)
if existing["syncStatus"] == "NotFound":
await self._argocd.create_application(app_name, gitops_repo_url, gitops_path, namespace)
# [4] Sync ํธ๋ฆฌ๊ฑฐ — "์ง๊ธ ๋น์ฅ ๊ฒ์ํ๊ณผ ์ฃผ๋ฐฉ์ ๋ง์ถฐ" (์ ์ฑ์ ๋ค์์คํ์ด์ค ์์ฑ ๋๊ธฐ ํ ์ฌ์๋)
await self._argocd.sync_application(app_name)
# [5] Healthy + Synced ๋ ๋๊น์ง ํด๋ง (์ต๋ 300์ด)
monitor = await self._monitor.monitor_deployment(app_name, service_id, command_id)
status = "FAILED" if monitor.get("error") else "SUCCESS"
# [6] ์ ์ ์๋ํฌ์ธํธ ์ถ์ถ + ๊ฒฐ๊ณผ๋ฅผ Kafka๋ก ํ์ + CP์ ์ฝ๋ฐฑ
endpoint = extract_endpoint(monitor["resources"]) # Ingress/NodePort์์ URL ๋ฝ๊ธฐ
await self._publisher.send_result(topic="service.deploy.result",
command_id=command_id, service_id=service_id, status=status,
result={ "appName": app_name, "endpoint": endpoint, "imageTag": image_tag })
์ด ์ค [2]·[3]·[4]๊ฐ ์ง์ง ํต์ฌ์ด๋ค. ํ๋์ฉ ์ฝ๋๋ก ๋ณด์.
[2] ์ด๋ฏธ์ง ํ๊ทธ ๊ต์ฒด. deploy ๋ ํฌ์ deployment.yaml์ GitLab API๋ก ์ฝ์ด, image: …:ํ๊ทธ์ ๋ง์ง๋ง ํ๊ทธ๋ง ์ ๊ท์์ผ๋ก ์ ๊ฐ์ผ๋ก ๋ฐ๊พธ๊ณ ์ปค๋ฐํ๋ค. ์ด ์ปค๋ฐ ํ ๋ฒ์ด ๊ณง "๋ฐฐํฌ ๋ช
๋ น"์ด๋ค.
# gitops_service.py — image tag ์นํ (์ค์ , ๋ฐ์ท)
async def update_image_tag(self, project_id, branch, file_path, new_tag):
enc = file_path.replace("/", "%2F")
# 1) ํ์ฌ manifest ์๋ฌธ ์ฝ๊ธฐ (GitLab API)
content = (await client.get(f"{base}/api/v4/projects/{project_id}/repository/files/{enc}/raw",
params={"ref": branch}, headers=self._headers)).text
# 2) 'image: <๋ ์ง์คํธ๋ฆฌ>/<์ด๋ฏธ์ง>:<ํ๊ทธ>' ์์ ':ํ๊ทธ' ๋ถ๋ถ๋ง new_tag ๋ก ์นํ
updated = re.sub(r'(image:\s*\S+:)\S+', rf'\g<1>{new_tag}', content)
# 3) ๋ฐ๋ ๋ด์ฉ์ ์ปค๋ฐ (← ์ด ์ปค๋ฐ์ด ArgoCD๋ฅผ ๊นจ์ด๋ค)
await client.put(f"{base}/api/v4/projects/{project_id}/repository/files/{enc}",
json={ "branch": branch, "content": updated,
"commit_message": f"chore: update image tag to {new_tag}" },
headers=self._headers)
[3] ArgoCD ์ฑ ์์ฑ. ์ฑ์ด ์์ง ์์ผ๋ฉด, "์ด deploy ๋ ํฌ์ dev/ ๊ฒฝ๋ก๋ฅผ ๋ณด๊ณ ์ด ๋ค์์คํ์ด์ค์ ๋ฐฐํฌํ๋ผ"๋ ์ ์๋ฅผ ๋ง๋ ๋ค. ์ฌ๊ธฐ์ ๋ณด๋ด๋ payload๊ฐ ๊ณง 6-3์์ ๋ณธ ์ฑ ๊ตฌ์ฑ์ด๋ค(์ฝ๋ → ์ค์ ์ฑ์ด 1:1๋ก ๋ง์๋จ์ด์ง๋ค).
# argocd_service.py — ์ฑ ์์ฑ payload (์ค์ , ๋ฐ์ท)
async def create_application(self, app_name, repo_url, path, namespace,
target_revision="main", auto_sync=True, self_heal=True):
sync_policy = { "syncOptions": ["CreateNamespace=true"] }
if auto_sync:
sync_policy["automated"] = { "prune": True, "selfHeal": self_heal } # ์๋ ๋๊ธฐํ + self-heal
payload = {
"metadata": { "name": app_name },
"spec": {
"project": "default",
"source": { "repoURL": repo_url, "path": path, "targetRevision": target_revision },
"destination": { "server": "https://kubernetes.default.svc", "namespace": namespace },
"syncPolicy": sync_policy,
},
}
await self._request("post", "/api/v1/applications", json=payload) # ArgoCD REST API
# [4] ์๋ Sync ํธ๋ฆฌ๊ฑฐ (์๋ ๋๊ธฐํ ์ธ์ "์ง๊ธ ๋ฐ๋ก" ํ ๋ฒ ๋ ๋ฏผ๋ค)
async def sync_application(self, app_name):
await self._request("post", f"/api/v1/applications/{app_name}/sync", json={})
[5] ๋ชจ๋ํฐ๋ง. ์ด์ ArgoCD์๊ฒ ์ํ๋ฅผ 5์ด๋ง๋ค ๋ฌผ์ด๋ณด๋ฉฐ, Healthy + Synced๊ฐ ๋๋ฉด ์ฑ๊ณต, ๋ง๊ฐ์ง๋ฉด(Degraded/Missing) ์คํจ๋ก ๋๋ธ๋ค(์ต๋ 300์ด).
# deploy_monitor.py — ๋ฐฐํฌ ์ํ ํด๋ง (์ค์ , ๋ฐ์ท)
while elapsed < timeout: # timeout=300์ด
st = await self._argocd.get_application_status(app_name)
health, sync = st["healthStatus"], st["syncStatus"]
if health == "Healthy" and sync == "Synced":
return { "healthStatus": health, "syncStatus": sync,
"pods": n, "readyPods": ready, "resources": resources } # โ
์ฑ๊ณต
if health in ("Degraded", "Missing", "NotFound"):
return { "error": f"Deploy unhealthy: {health}" } # โ ์คํจ
if st["operationState"].get("phase") == "Failed":
return { "error": "Sync failed: ..." } # โ Sync ์์ฒด ์คํจ
await asyncio.sleep(poll_interval) # poll_interval=5์ด
elapsed += poll_interval
6-5. ๊ฒฐ๊ณผ ํ์ — commandId๋ก ๋งค์นญ, ๋ฉฑ๋ฑ
์์ด์ ํธ๋ ๋๋๋ฉด service.deploy.result๋ก ๊ฒฐ๊ณผ๋ฅผ ํ์ ํ๋ค(์๋ํฌ์ธํธ·์ํ ํฌํจ). MLOps ๋ฐฑ์๋๋ ๊ทธ๊ฑธ ๋ฐ์, ๋น๋์ ๋๊ฐ์ด commandId="deploy.{runId}"๋ก ์ด๋ ๋ฐฐํฌ์ธ์ง ์ฐพ์ ์ํ๋ฅผ ๋ง๊ฐํ๋ค. ๊ฒฐ๊ณผ๊ฐ ๋ ๋ฒ ์๋ ์์ ํ๊ฒ ๋ฉฑ๋ฑ ์ฒ๋ฆฌํ๋ค.
// DevopsDeployResultListener (์์ง)
@KafkaListener(topics = "${kafka.topic.devops-deploy-result}", ...)
public void handleDeployResult(String rawJson) {
String commandId = root.path("commandId").asText(null);
if (commandId == null || !commandId.startsWith("deploy.")) return; // MLOps ๋ฐฐํฌ ์๋ → ๋ฌด์
long devopsRunId = Long.parseLong(commandId.substring("deploy.".length()));
Map<String,Object> deployRow = pipelineDeployDao.getDeployByDevopsRunId(devopsRunId);
if (deployRow == null) return; // ๋งค์นญ ์์ → skip
if ("SUCCEEDED".equals(deployRow.get("status"))) return; // ์ด๋ฏธ ๋๋จ → ๋ฉฑ๋ฑ skip
String status = root.path("status").asText("UNKNOWN");
String deployStatus = "SUCCESS".equals(status) ? "SUCCEEDED" : "FAILED";
String endpoint = root.path("result").path("endpoint").asText(null); // ์๋น์ค ์ ์ ์ฃผ์
pipelineDeployDao.modDeployResult(devopsRunId, deployStatus, endpoint, ...);
}
์ ๋ฆฌํ๋ฉด ๋ฐฐํฌ ํ ๋ฒ์ ์ด๋ ๊ฒ ํ๋ฅธ๋ค: MLOps "๋ฐฐํฌํด์ค"(Kafka) → ๋ฐฐํฌ ์์ด์ ํธ๊ฐ deploy ๋ ํฌ ์ด๋ฏธ์ง ํ๊ทธ ์ปค๋ฐ(๋ชฉํ ๋ณ๊ฒฝ) → ArgoCD๊ฐ ๊ฐ์งํด ํด๋ฌ์คํฐ๋ฅผ ๊ทธ ์ํ๋ก ์๋ ด(Synced+Healthy) → ๊ฒฐ๊ณผ๋ฅผ Kafka๋ก ํ์ → MLOps๊ฐ commandId๋ก ๋งค์นญํด ๋ง๊ฐ. MLOps๋ ํด๋ฌ์คํฐ๋ฅผ ์ง์ ๋ง์ง์ง ์๊ณ , "๋ชฉํ๋ฅผ Git์ ์ ๊ฒ" ์ํฌ ๋ฟ์ด๋ค.
7. ๋ฌด์์ ์ป์๋
- ๊ฐ๋ฒผ์์ก๋ค. Airflow ์ค์ผ์ค๋ฌ·์น์๋ฒ·๋ฉํDB๊ฐ ์ฌ๋ผ์ก๋ค. MLOps ๋ฐฑ์๋๋ ์ด๋ฒคํธ๋ฅผ ๋ฐํ·์์ ํ๋ ์์ ๊ณ์ธต์ด ๋๋ค.
- ๊ด์ฌ์ฌ๊ฐ ๋ถ๋ฆฌ๋๋ค. ๋น๋๋ DevOps Jenkins, ๋ฐฐํฌ๋ DevOps ArgoCD๊ฐ ๋งก๊ณ , MLOps๋ ML ํ์ดํ๋ผ์ธ ๋ก์ง์ ์ง์คํ๋ค. ์ ํ์ด ์ํ๋ ๊ฑธ ๋ค์ ๋ง๋ค์ง ์์๋ค.
- ๋์จํ๊ฒ ๊ฒฐํฉ๋๋ค. ์๋ก REST๋ก ์ง์ ๋ถ๋ฅด๋ ๋์ Kafka ํ ํฝ์ผ๋ก ๋น๋๊ธฐ ํต์ ํ๋ค. ํ์ชฝ์ด ๋๋ฆฌ๊ฑฐ๋ ์ ๊น ์ฃฝ์ด๋ ๋ฉ์์ง๋ ํ์ ๋จ์ ์ด์ด ์ฒ๋ฆฌ๋๋ค.
- ๋ฐฐํฌ๊ฐ ์ ์ธํ์ด ๋๋ค. "์ง๊ธ ๋ฌด์์ด ๋ ์์ด์ผ ํ๋์ง"๊ฐ Git์ ๋จ๋๋ค. ArgoCD๊ฐ ์๋ ๋๊ธฐํ·self-heal ํ๋ฏ๋ก, ์ฌ๋์ด ์ง์ ํด๋ฌ์คํฐ๋ฅผ ๋ง์ง ์ผ์ด ์ค์๋ค.
8. ์ค์ ์์ ๋ฐฐ์ด ๊ฒ
- ๋ฉฑ๋ฑ์ ์ ํ์ด ์๋๋ผ ํ์. ๊ฒฐ๊ณผ ์ด๋ฒคํธ๋ ์ฌ๋ฐํ๋ ์ ์์ด์, ๋ชจ๋ ๊ฒฐ๊ณผ ๋ฆฌ์ค๋๊ฐ
commandId๋งค์นญ + "์ด๋ฏธ ๋๋ ๊ฑด skip"์ผ๋ก ๋ฉฑ๋ฑํ๊ฒ ์ค๊ณํ๋ค. - ์ปจ์๋จธ ์คํ์
์
latest๋ก. ํ๋ ์์ด์ ํธ ์ฌ์์ ์ ๊ณผ๊ฑฐ ์ปค๋งจ๋๋ฅผearliest๋ก ์ผ๊ด ์ฌ์๋นํด, ํ์ต ์ด๋ฏธ์ง๊ฐ ํ๊บผ๋ฒ์ ๋น๊ฒจ์ง๋ฉฐ ๋์คํฌ๋ฅผ ์๋ฐํ ์ฌ๊ณ ๊ฐ ์์๋ค. ์ดํauto-offset-reset=latest๋ก ๋ฐ๊ฟ "์ฌ์์ํ๋ค๊ณ ๊ณผ๊ฑฐ๋ฅผ ๋ค์ ์คํ"ํ์ง ์๊ฒ ํ๋ค. - ๊ฒฐ๊ณผ ์ด๋ฒคํธ์ DLT๋ณด๋ค ์ฌ์๋. ๊ฒฐ๊ณผ๋ ๋ฉฑ๋ฑ·์ฌ๋ฐํ๋๋ฏ๋ก, ๊นจ์ง ๋ฉ์์ง๋ ์ฆ์ ๋ฒ๋ฆฌ๋ ์ผ์์ ์คํจ๋ ์ผ์ ๊ฐ๊ฒฉ์ผ๋ก ์ฌ๋ฌ ๋ฒ ์ฌ์๋ํ๋๋ก ์๋ฌ ํธ๋ค๋ฌ๋ฅผ ๋๋ค(๊ณ ์ ๊ฐ๊ฒฉ × ์ ํ).
9. ๋ง๋ฌด๋ฆฌ — 1์ฐจ vs 2์ฐจ
| ๊ตฌ๋ถ | 1์ฐจ | 2์ฐจ |
|---|---|---|
| ์ค์ผ์คํธ๋ ์ด์ | Airflow DAG | Kafka ์ด๋ฒคํธ(์ปค๋งจ๋/๊ฒฐ๊ณผ) |
| ์คํ ์์น | Airflow KubernetesPodOperator | MLOps ์ก ์์ด์ ํธ(K8s Job) |
| ๋น๋ | DAG ์์์ Jenkins ํธ๋ฆฌ๊ฑฐ | DevOps ๋น๋ API ์์(Jenkins) |
| ๋ฐฐํฌ | DAG ์์์ ์ฒ๋ฆฌ | DevOps ๋ฐฐํฌ ์์ด์ ํธ → GitOps + ArgoCD |
| MLOps ๋ฐฑ์๋ ์ญํ | ์ค์ผ์คํธ๋ ์ดํฐ(๋ฌด๊ฑฐ์) | ์ด๋ฒคํธ ๋ฐํ·์์ (๊ฐ๋ฒผ์) |
1์ฐจ๊ฐ "๋ด๊ฐ ๋ค ํ๋ค"์๋ค๋ฉด, 2์ฐจ๋ "์ด๋ฒคํธ๋ก ์๋ฆฌ๊ณ , ์ํ๋ ์ชฝ์ ๋งก๊ธด๋ค"์๋ค. ๋ฌด๊ฑฐ์ด ์ค์ผ์คํธ๋ ์ดํฐ๋ฅผ ๊ฑท์ด๋ด๊ณ ๋ฉ์์ง ๋ฒ์ค์ ์ ์ธํ ๋ฐฐํฌ(GitOps) ์์ ์ฌ๋ฆฌ๋, ์์คํ ์ด ํ๊ฒฐ ๊ฐ๋ณ๊ณ ์์ด ๋ ๊ฐ๋ ๊ตฌ์กฐ๊ฐ ๋๋ค. MSA์์ ํ ๊ฒฝ๊ณ๋ฅผ ์กด์คํ๋ฉด์๋ ๋๊น ์์ด ์๋ ๋ฐฉ๋ฒ์ผ๋ก, ์ด๋ฒคํธ์ GitOps๋ ์ ๋ง๋ ์ง์ด์๋ค.