ํ”„๋กœ์ ํŠธ/MLOps ํ†ตํ•ฉ

MLOps 2์ฐจ ์•„ํ‚คํ…์ฒ˜ — Airflow๋ฅผ ๊ฑท์–ด๋‚ด๊ณ  Kafka ์ด๋ฒคํŠธ + DevOps ArgoCD๋กœ ๋ฐฐํฌํ•˜๊ธฐ

THE NICOLE 2026. 6. 14. 17:21

์•ž์„  ๊ธ€์—์„œ 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๋Š” ์ž˜ ๋งž๋Š” ์ง์ด์—ˆ๋‹ค.