상태: 초안 (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. 한 줄 요약: 「비교하는 고리」와 「파드로 일하는 일꾼」
- 2. 구조도 ①: Argo CD 의 비교 고리
- 3. 코드 지도
- 4. 핵심 경로 따라가기 — Argo CD
- 5. 핵심 경로 따라가기 — Argo Workflows
- 6. 글쓴이의 기여와 이어지는 곳
- 7. 실측: 작은 클러스터의 Argo CD
- 8. 어디부터 읽을까
- 9. 첫 기여하기 좋은 곳
- 10. 한계
- 참고
1. 한 줄 요약: 「비교하는 고리」와 「파드로 일하는 일꾼」
같은 Argo 이름을 쓰지만 두 프로젝트의 성격은 다릅니다.
- Argo CD 는 온도 조절기입니다. git 에 적힌 목표 온도(원하는 상태)와 방 안 온도(클러스터의 실제 상태)를 계속 비교하고, 다르면 알려 주거나(OutOfSync) 맞춥니다(sync). 이 비교가 끝없이 도는 고리가 코드의 중심입니다.
- Argo Workflows 는 공장 작업 지시서입니다. 단계들을 순서·의존성으로 묶은 Workflow 를 받아 단계마다 파드를 하나씩 띄우고, 단계 사이에 주고받는 파일(아티팩트)은 S3 같은 저장소를 거쳐 넘깁니다.
| 구성요소 | 하는 일 | 어디서 도나 |
|---|---|---|
| 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 의 비교 고리
그림 읽는 법:
- 위에서 아래로 한 번의 「새로 고침」입니다. repo-server 가 렌더한 원하는 상태와 클러스터 캐시가 들고 있는 실제 상태를 컨트롤러가 비교(②)하고, 자원마다 상태를 판정(③)해 Application 에 적습니다.
- 동기화는 따로 도는 두 번째 큐(④·⑤)입니다. 비교와 적용을 다른 큐로 나눈 것은, 오래 걸리는 sync 가 수많은 앱의 상태 갱신을 막지 않게 하려는 것입니다.
- 클러스터 캐시는 API 서버를 매번 조회하지 않고 watch 로 모든 자원을 메모리에 들고 있습니다. 비교가 싸지는 대신 컨트롤러 메모리가 큽니다.
- 맨 아래 상자가 이 글 4.3절의 주제, 상태 판정 스크립트를 고르는 순서입니다.
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 이 두 큐에 일꾼 고루틴을 따로 붙입니다.
- 새로 고침 일꾼은
appcontroller.go:1775processAppRefreshQueueItem입니다. 먼저appcontroller.go:2105needRefreshAppStatus로 「정말 다시 비교할 때인가」를 묻습니다. 사용자가 새로 고침을 눌렀거나, 관리하는 자원이 바뀌었거나, 정해 둔 주기(기본 120초에 지터 최대 60초,argocd_application_controller.go:45)가 지났으면 비교합니다. - 비교는
controller/state.go:633CompareAppState입니다. 이 함수가 원하는 상태를state.go:206GetRepoObjs로 repo-server 에 gRPC 로 받아 오고, 실제 상태를controller/cache/cache.go:729GetManagedLiveObjs로 캐시에서 꺼낸 뒤, gitops-engine 의pkg/diff/diff.go:76Diff로 자원마다 비교합니다. - 같은 함수 끝에서 상태를 판정합니다(
state.go:1051에서setApplicationHealth호출). 4.3절에서 자세히 봅니다. - 자동 sync 가 켜져 있으면
appcontroller.go:2322autoSync가 Application 에 operation 을 적고, 그러면 두 번째 큐가 그 앱을 집어 듭니다.
4.2 렌더와 적용: repo-server 와 sync
- repo-server 의 입구는
reposerver/repository/repository.go:631GenerateManifest입니다. 캐시를 먼저 보고(repository.go:1016getManifestCacheEntry), 없으면 저장소를 그 커밋으로 맞춘 뒤repository.go:1714GenerateManifests가repository.go:1921GetAppSourceType로 helm·kustomize·plain·플러그인 중 하나를 골라 렌더합니다. 렌더를 별도 서비스로 뺀 이유는 보안과 자원입니다. helm·kustomize·플러그인은 남의 저장소 내용을 실행하는 일이라, 클러스터 자격 증명을 가진 컨트롤러와 떼어 놓고, 무거운 렌더는 repo-server 만 늘려서 감당합니다. - 적용은
controller/sync.go:102SyncAppState가 gitops-engine 의pkg/sync/sync_context.go:478Sync를 부릅니다. 여기서sync_context.go:954getSyncTasks가 자원을 단계(PreSync·Sync·PostSync 훅)와 웨이브 번호로 줄 세우고, 한 웨이브가 건강해져야 다음 웨이브로 넘어갑니다. 「건강해져야」를 판단하는 것이 다시 4.3의 상태 판정입니다. - 실제 상태 캐시는
gitops-engine/pkg/cache/cluster.go:1094sync가 처음에 모든 API 자원을 목록으로 받고,cluster.go:792watchEvents가 그 뒤의 변경을 watch 로 받아 유지합니다.
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
workflow/controller/controller.go:337Run이 informer 와 일꾼을 띄우고,controller.go:922processNextItem이 Workflow 하나를 꺼내 operator 를 만듭니다.workflow/controller/operator.go:212operate가 한 번의 reconcile 입니다. 먼저operator.go:1216podReconciliation으로 이미 띄운 파드들의 상태를 노드 상태에 반영하고,operator.go:2235executeTemplate으로 진입 템플릿부터 다시 내려가며 다음에 할 일을 정합니다. 템플릿 종류에 따라steps.go:42executeSteps,dag.go:278executeDAG,operator.go:3435executeContainer로 갈립니다. 매번 처음부터 다시 걷되 이미 끝난 노드는 건너뛰는 방식이라, 컨트롤러가 재시작돼도 Workflow 의status.nodes만 있으면 이어서 할 수 있습니다.- 컨테이너 단계는
workflowpod.go:341createWorkflowPod이 파드로 만듭니다. 사용자 컨테이너main옆에init(workflowpod.go:1092newInitContainers)과wait를 붙이고,main의 명령은argoexec emissary로 감쌉니다 (cmd/argoexec/commands/emissary.go:46). v4 에는 init 없이supervisor컨테이너 하나가 init·wait 일을 같이 하는 방식도 들어왔습니다 (workflowpod.go:1127newSupervisorContainer,initlessPod설정). - 파드 안에서
wait는 main 이 끝나기를 기다렸다가(workflow/executor/executor.go:1355Wait),executor.go:449SaveArtifacts로 출력 파일을 저장소에 올립니다. - 저장소 드라이버는
workflow/artifacts/artifacts.go:29NewDriver가 아티팩트에 적힌 종류로 고릅니다. S3 가 맨 앞(38줄)이고, 접근 키는 그 자리에서 Secret 을 읽어 채웁니다. MinIO 는 S3 드라이버로 붙습니다. - 아티팩트에 저장소를 적지 않았으면
workflow/artifactrepositories/artifactrepositories.go:38Resolve가 워크플로 네임스페이스의 기본 ConfigMap, 그다음 컨트롤러 설정의 기본 저장소 순으로 찾습니다.
왜 이렇게 나눴나. 컨트롤러는 클러스터에 하나지만 단계는 수천 개일 수 있습니다. 무거운 파일 전송을 컨트롤러가 하지 않고 단계 파드 안의 argoexec 에 맡기면,
전송량이 늘어도 단계 파드만 늘어납니다. 대신 파드마다 보조 컨테이너가 붙는 비용이 생기고, v4 의 supervisor 방식은 그 비용을 줄이려는 시도로 읽힙니다(설계 의도는 문서로 확인하지 못함, 미확인).
6. 글쓴이의 기여와 이어지는 곳
- argoproj/argo-cd 이슈 #30209 → #30243. 처음 이슈(#30209)는 4.4의 「돌고 있으면 Progressing」 문장이었고, 다른 기여자의 #30212 로 그 한 줄이 고쳐져 닫혔습니다.
남은 세 가지(실패 뒤 다음 Job 이 돌면 Healthy, 성공한 적 없으면 Healthy, 멈춘 CronJob 은 Suspended 가 아님)를 새 이슈 #30243 으로 올려 두었습니다.
근거는 모두 4.4의 표,
health.lua의 3·15·27·35줄입니다. - argoproj/argo-workflows#17246(검토 중). 아티팩트 저장소 문서의 MinIO 설치 절이 지금은 받을 수 없는 이미지와 너무 큰 기본값을 쓰고 있어,
저장소에 이미 있는 quick-start 의 MinIO 구성(
manifests/quick-start/base/minio)을 쓰도록 바꾸는 PR 입니다. 5절의 5번(S3 드라이버로 MinIO 에 붙는 길)이 문서가 설명하는 대상입니다. 문서 검사(cspell)가 이미지 이름을 몰라 사전에 단어를 더한 일도 있었습니다.
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. 어디부터 읽을까
resource_customizations/batch/CronJob/— Lua 56줄과 시험 데이터. 가장 작은 입구입니다.util/lua/lua.go:169GetHealthScript→gitops-engine/pkg/health/health.go:70.controller/appcontroller.go:1775processAppRefreshQueueItem— 한 번의 새로 고침 전체.controller/state.go:633CompareAppState— 길지만 비교의 모든 재료가 모입니다.- Workflows 는
workflow/controller/operator.go:212operate와workflowpod.go:341createWorkflowPod두 곳이면 큰 그림이 잡힙니다. - 마지막으로
workflow/artifacts/artifacts.go:29— 드라이버 하나(s3)를 골라 끝까지 따라가 보기.
9. 첫 기여하기 좋은 곳
resource_customizations에 Lua 와 시험 더하기. 아직 상태 판정이 없는 CRD 가 많습니다.health.lua+health_test.yaml+testdata/*.yaml세 가지면 되고, 전체 시험을 돌리지 않아도go test ./util/lua/하나로 확인할 수 있습니다(4.3).- CronJob 문서 정리(#30243). 4.4의 표 그대로 문서를 맞추고, 덮어쓰기 방법(
resource.customizations.health.batch_CronJob)과ignore-healthcheck주석을 함께 안내하는 일입니다. 메인테이너가 이슈를 받아 주면 바로 PR 로 이어 갈 수 있습니다. - Lua 의 오래된 문법. 내장 Lua 8곳이
table.getn을 씁니다(CronJob 의 27줄 포함). 실행기가 지원하니 지금은 문제가 없지만,#길이 연산자로 바꾸는 정리는 범위가 작습니다. 다만 동작이 같다는 시험을 함께 내야 받아 줄 것입니다. - Workflows 문서. 4.1.5 의 init 없는 supervisor 방식은 코드에 설명이 잘 달려 있지만, 사용자 문서에서 이 설정을 찾기 어려웠습니다(문서 폴더는 이번에 받지 않아 미확인).
- 규칙: Argo 프로젝트에는 생성형 AI 정책(argoproj/argoproj
community/genai.md)이 있고, Argo Workflows 의 PR 템플릿에는 AI 사용을 밝히는### AI절이 있습니다. 두 저장소 모두 DCO 서명(Signed-off-by)을 요구하고, Argo CD 는 PR 제목 형식 검사(.github/pr-title-checker-config.json)와 「시험 없는 PR 은 머지되기 어렵다」는 체크리스트 항목이 있습니다.
10. 한계
- Argo Workflows 는 작은 클러스터에 없어 실측이 없습니다. supervisor 방식, 아티팩트 플러그인, 아카이브(persist/)는 코드만 훑었습니다.
- Argo CD 의 샤딩, ApplicationSet 생성기, 알림, 서버 쪽 diff, 하이드레이터(commitserver)는 다루지 않았습니다.
- CronJob 분류는 Lua 를 옮긴 계산입니다. Argo CD API 로 자원 트리의 실제 판정을 읽지는 않았습니다(로그인이 필요해 하지 않음, 미실측).
티티 한마디. 쉬는 중인 선수한테 「건강해요!」 라고만 적어 두면, 감독님이 「누가 쉬고 있지?」 하고 찾을 때 한 명도 안 나와요. 27명이나 쉬고 있었는데요!
참고
열람일은 모두 2026-10-17 입니다.
- argoproj/argo-cd v3.5.1:
controller/,gitops-engine/pkg/,reposerver/repository/repository.go,util/lua/lua.go,resource_customizations/batch/CronJob/health.lua,docs/operator-manual/health.md - argoproj/argo-workflows v4.1.5:
workflow/controller/,workflow/executor/executor.go,workflow/artifacts/artifacts.go,workflow/artifactrepositories/artifactrepositories.go - 공개 이슈·PR: argoproj/argo-cd#30209·#30212·#30243·#24428·#24429, argoproj/argo-workflows#17246
- 인용한 코드 조각은 위 저장소의 것이며 모두 Apache License 2.0 입니다. 글에는 짧게 줄여 옮겼습니다.