TT Lab
요금제

한국어

시작하기
블로그

블로그

Argo CD·Argo Workflows 읽기: 비교하는 고리와 파드 안의 일꾼, 그리고 CronJob 하나의 상태 판정

상태: 초안 (2026-10-17), 일부 실측. 기준 판 argoproj/argo-cd v3.5.1(커밋 109ca7ca7113), argoproj/argo-workflows v4.1.5(커밋 6c80c7e56ae6). Argo CD 는 작은 클러스터가 같은 판을 돌리고 있어 2026-10-17 에 읽기만 해서 확인했습니다(파드·Application 상태·설정 키·CronJob 상태 필드). Argo Workflows 는 그 클러스터에 없어 코드만 읽었습니다(미실측). 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.

목차

1. 한 줄 요약: 「비교하는 고리」와 「파드로 일하는 일꾼」

같은 Argo 이름을 쓰지만 두 프로젝트의 성격은 다릅니다.

구성요소 하는 일 어디서 도나
argocd-repo-server git·helm 저장소를 받아 helm/kustomize/plain 으로 매니페스트 렌더 Deployment
argocd-application-controller 앱마다 비교·상태 판정·sync. 클러스터 캐시 보유 StatefulSet(샤딩 가능)
argocd-server API·UI·CLI 의 입구, 인증 Deployment
redis 렌더 결과·앱 자원 트리 캐시 Deployment
applicationset-controller 생성기로 Application 을 찍어 냄 Deployment
workflow-controller Workflow 의 노드 상태 머신, 파드 생성 Deployment
argoexec 단계 파드 안의 init·wait(또는 supervisor) 컨테이너, main 을 감싸는 emissary 단계 파드마다

2. 구조도 ①: Argo CD 의 비교 고리

Argo CD: git 의 원하는 상태와 클러스터를 계속 비교한다 Git 저장소 원하는 상태 repo-server helm·kustomize·plain 렌더 → 매니페스트 결과는 redis 에 캐시 application-controller 큐 두 개: 새로 고침 · 작업 ① 새로 고침 큐 다시 볼 때인가 ② 비교 CompareAppState ③ 상태 판정 자원별 Lua·Go Synced/OutOfSync · Healthy/Degraded 를 Application 에 기록 ④ operation 큐 자동·수동 sync 요청 ⑤ SyncAppState 웨이브·훅 순서로 적용 비교·적용의 핵심은 저장소 안 gitops-engine 이 맡는다 클러스터 캐시 모든 자원을 watch 로 유지 쿠버네티스 API 대상 클러스터(여럿 가능) ③ 의 순서: 먼저 찾은 것 하나만 쓴다 1 argocd-cm 의 resource.customizations.health.{그룹_종류} 2 argocd-cm 의 와일드카드 항목 3 내장 Lua: resource_customizations/{그룹}/{종류}/health.lua 4 내장 Lua 와일드카드 → 5 Go 로 짠 내장 검사(Deployment 등)
그림: Argo CD 의 구성요소와 비교·상태 판정·sync 고리, 상태 판정 스크립트를 고르는 순서

그림 읽는 법:

3. 코드 지도

v3.5 의 argo-cd 저장소에는 예전에 따로 있던 gitops-engine 이 gitops-engine/ 폴더로 들어와 있습니다(go.mod 의 replace … => ./gitops-engine). 비교·sync·클러스터 캐시의 핵심은 여기 있습니다.

경로 무엇 처음 읽을 파일
argo-cd controller/ 앱 컨트롤러: 큐, 비교, 상태, sync appcontroller.go, state.go, health.go, sync.go
argo-cd controller/cache/ 클러스터 캐시를 앱 관점으로 감싼 층 cache.go
argo-cd gitops-engine/pkg/ cache(watch 캐시), diff, sync(웨이브·훅), health(Go 내장 검사) sync/sync_context.go, health/health.go
argo-cd reposerver/repository/ 매니페스트 생성, 렌더 캐시 repository.go
argo-cd resource_customizations/ 자원 종류별 Lua(상태·액션), 시험 데이터 batch/CronJob/health.lua
argo-cd util/lua/ Lua 실행기, 스크립트 고르기 lua.go
argo-workflows workflow/controller/ Workflow 컨트롤러, operator(상태 머신), 파드 만들기 controller.go, operator.go, workflowpod.go
argo-workflows workflow/executor/ 파드 안에서 도는 실행기(아티팩트 저장·불러오기) executor.go
argo-workflows workflow/artifacts/ 저장소 드라이버(s3, gcs, azure, oss, hdfs, http, git …) artifacts.go
argo-workflows cmd/argoexec/commands/ argoexec init·wait·emissary 명령 emissary.go

4. 핵심 경로 따라가기 — Argo CD

4.1 새로 고침: 언제 다시 볼까

컨트롤러에는 큐가 두 개 있습니다(controller/appcontroller.go:118 appRefreshQueue, 121줄 appOperationQueue). appcontroller.go:955 Run 이 두 큐에 일꾼 고루틴을 따로 붙입니다.

  1. 새로 고침 일꾼은 appcontroller.go:1775 processAppRefreshQueueItem 입니다. 먼저 appcontroller.go:2105 needRefreshAppStatus 로 「정말 다시 비교할 때인가」를 묻습니다. 사용자가 새로 고침을 눌렀거나, 관리하는 자원이 바뀌었거나, 정해 둔 주기(기본 120초에 지터 최대 60초, argocd_application_controller.go:45)가 지났으면 비교합니다.
  2. 비교는 controller/state.go:633 CompareAppState 입니다. 이 함수가 원하는 상태를 state.go:206 GetRepoObjs 로 repo-server 에 gRPC 로 받아 오고, 실제 상태를 controller/cache/cache.go:729 GetManagedLiveObjs 로 캐시에서 꺼낸 뒤, gitops-engine 의 pkg/diff/diff.go:76 Diff 로 자원마다 비교합니다.
  3. 같은 함수 끝에서 상태를 판정합니다(state.go:1051 에서 setApplicationHealth 호출). 4.3절에서 자세히 봅니다.
  4. 자동 sync 가 켜져 있으면 appcontroller.go:2322 autoSync 가 Application 에 operation 을 적고, 그러면 두 번째 큐가 그 앱을 집어 듭니다.

4.2 렌더와 적용: repo-server 와 sync

4.3 상태 판정: 어떤 스크립트가 고르는가

자원 하나의 상태는 gitops-engine 의 pkg/health/health.go:70 GetResourceHealth 가 정합니다. 지워지는 중이면 Progressing, 아니면 덮어쓰기(override) 를 먼저 물어보고, 답이 없을 때만 Go 로 짠 내장 검사 (health.go:104 GetHealthCheckFunc, Deployment·Pod·Job 등)로 갑니다.

Argo CD 쪽의 「덮어쓰기」가 Lua 입니다. util/lua/lua.go:169 GetHealthScript 의 순서를 줄이면 이렇습니다.

key := GetConfigMapKey(obj.GroupVersionKind())
if script, ok := vm.ResourceOverrides[key]; ok && script.HealthLua != "" {
    return script.HealthLua, script.UseOpenLibs, nil   // argocd-cm 에 사용자가 적은 것
}
// 와일드카드 항목 → 내장 Lua 파일 → 내장 와일드카드 순으로 찾는다
builtInScript, err := vm.getPredefinedLuaScripts(key, healthScriptFile)

출처: argoproj/argo-cd v3.5.1 util/lua/lua.go(GetHealthScript 요약), Apache-2.0.

내장 Lua 는 resource_customizations/<그룹>/<종류>/health.lua 파일이고, resource_customizations/embed.go 의 go:embed 로 바이너리에 통째로 들어갑니다. 같은 폴더의 health_test.yaml 과 testdata/ 가 그 스크립트의 시험입니다. 새 CRD 의 상태 판정을 기여하는 가장 쉬운 길이 이 폴더에 Lua 하나와 시험 데이터를 더하는 것입니다. Go 코드를 몰라도 되고, 시험은 util/lua/health_test.go 가 폴더의 health_test.yaml 을 모두 찾아 돌립니다.

이렇게 판정한 자원 상태는 controller/health.go:20 setApplicationHealth 가 모아 앱 전체 상태로 만듭니다. 가장 나쁜 것이 앱의 상태가 되고, 자원에 argocd.argoproj.io/ignore-healthcheck: "true" 주석이 있으면 모으는 데서 빠집니다(41줄). v3 에서는 자원별 상태를 Application 객체가 아니라 redis 의 자원 트리에 둡니다. --persist-resource-health 의 기본이 false 이기 때문입니다 (argocd_application_controller.go:293). Application 이 커져서 etcd 쓰기가 무거워지는 것을 피하려는 선택입니다.

4.4 CronJob 사례: 문서와 Lua 가 다른 곳

CronJob 의 내장 Lua 는 resource_customizations/batch/CronJob/health.lua 56줄짜리입니다. 결정은 다섯 갈래입니다.

조건(위에서부터 먼저 맞는 것) 판정 줄
spec.suspend == true Healthy(「CronJob is Suspended」) — 이슈 #24428 이 풀릴 때까지 임시 3
한 번도 성공한 적 없음(lastSuccessfulTime 없음) Healthy(「never completed successfully」) 15
지금 돌고 있는 Job 이 있음(status.active) Healthy(「job is running」) — 이슈 #24429 27
마지막 성공 < 마지막 예약 Degraded 35
그 밖(예약된 적 없음 포함) Healthy 41·50

v3.5.1 판 문서(docs/operator-manual/health.md)는 CronJob 절에서 「마지막 Job 이 실패하면 Degraded」, 「마지막 Job 이 돌고 있으면 Progressing」 이라고 적고, Suspended 상태의 예로 「멈춘 CronJob」을 듭니다. 코드와 비교하면 셋 다 어긋납니다. 실패한 뒤 다음 Job 이 돌고 있으면 Healthy 로 돌아가고, 돌고 있는 동안은 Progressing 이 아니라 Healthy 이며, 멈춘 CronJob 은 Suspended 가 아니라 Healthy 입니다.

5. 핵심 경로 따라가기 — Argo Workflows

Argo Workflows: 컨트롤러는 파드를 만들고, 파드가 일한다 Workflow CR(템플릿 묶음) workflow-controller · operate() 노드 상태 머신: steps·DAG·컨테이너 createWorkflowPod 단계 하나 = 파드 하나 init argoexec 입력 아티팩트 불러오기 main 사용자 이미지 emissary 가 감싸 실행 wait argoexec 출력 아티팩트 저장·결과 보고 아티팩트 저장소 — NewDriver 가 종류로 고른다 S3(MinIO 호환) · GCS · Azure · OSS · HDFS HTTP · Artifactory · Git · Raw · Plugin 어느 저장소를 쓰나(artifactRepositoryRef 해석) 지정함: 워크플로 네임스페이스의 ConfigMap → 컨트롤러 쪽 지정 안 함: 네임스페이스 기본 → 컨트롤러 설정의 기본
그림: Argo Workflows 의 컨트롤러·단계 파드·아티팩트 저장소
  1. workflow/controller/controller.go:337 Run 이 informer 와 일꾼을 띄우고, controller.go:922 processNextItem 이 Workflow 하나를 꺼내 operator 를 만듭니다.
  2. workflow/controller/operator.go:212 operate 가 한 번의 reconcile 입니다. 먼저 operator.go:1216 podReconciliation 으로 이미 띄운 파드들의 상태를 노드 상태에 반영하고, operator.go:2235 executeTemplate 으로 진입 템플릿부터 다시 내려가며 다음에 할 일을 정합니다. 템플릿 종류에 따라 steps.go:42 executeSteps, dag.go:278 executeDAG, operator.go:3435 executeContainer 로 갈립니다. 매번 처음부터 다시 걷되 이미 끝난 노드는 건너뛰는 방식이라, 컨트롤러가 재시작돼도 Workflow 의 status.nodes 만 있으면 이어서 할 수 있습니다.
  3. 컨테이너 단계는 workflowpod.go:341 createWorkflowPod 이 파드로 만듭니다. 사용자 컨테이너 main 옆에 init(workflowpod.go:1092 newInitContainers)과 wait 를 붙이고, main 의 명령은 argoexec emissary 로 감쌉니다 (cmd/argoexec/commands/emissary.go:46). v4 에는 init 없이 supervisor 컨테이너 하나가 init·wait 일을 같이 하는 방식도 들어왔습니다 (workflowpod.go:1127 newSupervisorContainer, initlessPod 설정).
  4. 파드 안에서 wait 는 main 이 끝나기를 기다렸다가(workflow/executor/executor.go:1355 Wait), executor.go:449 SaveArtifacts 로 출력 파일을 저장소에 올립니다.
  5. 저장소 드라이버는 workflow/artifacts/artifacts.go:29 NewDriver 가 아티팩트에 적힌 종류로 고릅니다. S3 가 맨 앞(38줄)이고, 접근 키는 그 자리에서 Secret 을 읽어 채웁니다. MinIO 는 S3 드라이버로 붙습니다.
  6. 아티팩트에 저장소를 적지 않았으면 workflow/artifactrepositories/artifactrepositories.go:38 Resolve 가 워크플로 네임스페이스의 기본 ConfigMap, 그다음 컨트롤러 설정의 기본 저장소 순으로 찾습니다.

왜 이렇게 나눴나. 컨트롤러는 클러스터에 하나지만 단계는 수천 개일 수 있습니다. 무거운 파일 전송을 컨트롤러가 하지 않고 단계 파드 안의 argoexec 에 맡기면, 전송량이 늘어도 단계 파드만 늘어납니다. 대신 파드마다 보조 컨테이너가 붙는 비용이 생기고, v4 의 supervisor 방식은 그 비용을 줄이려는 시도로 읽힙니다(설계 의도는 문서로 확인하지 못함, 미확인).

6. 글쓴이의 기여와 이어지는 곳

7. 실측: 작은 클러스터의 Argo CD

2026-10-17, 읽기 명령만 썼습니다. 앱 이름·저장소 주소는 적지 않습니다.

항목 읽은 값
구성요소 application-controller(StatefulSet 1), repo-server, server, applicationset-controller, notifications-controller, redis, dex — 모두 v3.5.1(dex·redis 제외)
repo-server 재시작 4회(나머지는 0)
Application 5개, 모두 Synced·Healthy
status.resourceHealthSource 5개 모두 appTree — 4.3의 「자원 상태는 redis 의 자원 트리에」 그대로
argocd-cm 의 resource.customizations.* 키 ignoreResourceUpdates.* 8개만 있고 health.* 는 없음 → CronJob 은 내장 Lua 로 판정

Application 의 status.resources 에서 CronJob 은 44개였고, 그 health 칸은 모두 비어 있었습니다(위의 appTree 때문). 그래서 CronJob 44개의 실제 상태 필드 (spec.suspend, status.lastScheduleTime, lastSuccessfulTime, active)를 읽고, 4.4 표의 규칙을 파이썬으로 옮겨 분류했습니다. 입력은 실측이고 판정은 Lua 를 옮긴 계산입니다.

걸린 갈래 개수 판정
멈춤(suspend: true) 27 Healthy
지금 Job 이 돌고 있음 3 Healthy
마지막 실행 성공 14 Healthy
마지막 실행 실패(돌고 있는 Job 없음) 0 Degraded

44개 가운데 27개가 멈춘 CronJob 이었습니다. 문서대로라면 UI 에서 Suspended 로 보여야 할 것들이 모두 Healthy 로 섞여 있다는 뜻이고, 「Suspended 로 걸러 보기」로는 하나도 찾을 수 없습니다. 이 분류는 이슈 #30243 의 3번을 실제 숫자로 보여 줍니다.

8. 어디부터 읽을까

  1. resource_customizations/batch/CronJob/ — Lua 56줄과 시험 데이터. 가장 작은 입구입니다.
  2. util/lua/lua.go:169 GetHealthScript → gitops-engine/pkg/health/health.go:70.
  3. controller/appcontroller.go:1775 processAppRefreshQueueItem — 한 번의 새로 고침 전체.
  4. controller/state.go:633 CompareAppState — 길지만 비교의 모든 재료가 모입니다.
  5. Workflows 는 workflow/controller/operator.go:212 operate 와 workflowpod.go:341 createWorkflowPod 두 곳이면 큰 그림이 잡힙니다.
  6. 마지막으로 workflow/artifacts/artifacts.go:29 — 드라이버 하나(s3)를 골라 끝까지 따라가 보기.

9. 첫 기여하기 좋은 곳

10. 한계

티티 한마디. 쉬는 중인 선수한테 「건강해요!」 라고만 적어 두면, 감독님이 「누가 쉬고 있지?」 하고 찾을 때 한 명도 안 나와요. 27명이나 쉬고 있었는데요!

참고

열람일은 모두 2026-10-17 입니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다