ArgoCD app-of-apps에서 sync-wave 잘못 짜서 새벽에 깬 이야기
지난주 화요일 새벽 3시쯤. 휴대폰이 울렸다. 온콜 알림이었다. prod의 메인 API가 503을 뱉고 있다는. 솔직히 그날따라 몸이 너무 무거워서 한 5분쯤은 침대에서 멍하니 알림을 봤다. 그 5분이 정말 길었다.
원인을 미리 말하자면, 전날 머지된 PR에서 ArgoCD argocd.argoproj.io/sync-wave 값을 한 줄 바꾼 게 문제였다. app-of-apps 패턴을 쓰는 우리 클러스터에서, 그 한 줄이 컨트롤 플레인 컴포넌트보다 워크로드를 먼저 띄우게 만들었다. 결과적으로 cert-manager가 뜨기 전에 ingress webhook이 호출돼서 발급이 꼬였고, 그게 ALB target health check를 흔들었고, 503이 떴다.
새벽 3시반에 침대에서 일어나면서 든 생각: "sync wave 그거 한 번 정리해야지 했던 게 1년 됐는데..."
어쩌다 이 사고가 났는가
우리 팀은 ArgoCD를 app-of-apps 패턴으로 쓴다. 루트 Application 하나가 child Application 30~40개를 끌고 들어가는 구조. 각 child는 또 그 안에서 자기 manifests를 sync한다. 이 구조 자체는 흔하다.
문제는 sync-wave가 두 레벨에서 동작한다는 점이다. 루트가 child Application들을 sync하는 순서, 그리고 각 child가 자기 안에서 리소스를 sync하는 순서. 둘은 별개로 돌아간다. Argo CD 공식 문서에는 sync-wave 값이 낮은 것부터 sync된다고 되어 있는데, app-of-apps에서는 이게 종종 함정이 된다.
전날 머지된 PR은 이런 내용이었다. 누군가 새 monitoring stack을 추가하면서 빨리 떠야 한다고 wave를 -5로 박았다. 그 자체는 합리적이다. 모니터링은 일찍 떠야 다른 거 디버깅이 된다. 근데 그 사람이 모르고 있던 게, 우리 cert-manager Application의 wave는 -3이었다는 거다.
그러니까 sync 순서가 이렇게 됐다:
wave -5: monitoring stack (Prometheus, Grafana)
wave -3: cert-manager
wave 0: ingress-nginx, app workloads
wave 3: cronjobs
뭐가 문제냐고? 우리 monitoring stack은 ServiceMonitor를 자동 등록하기 위해 prometheus-operator의 CRD를 쓴다. 근데 그 ServiceMonitor가 일부 워크로드를 가리키게 되어 있었고, 그 워크로드들 중 일부는 cert-manager가 발급해주는 인증서가 있어야 시작되는 Pod였다. ArgoCD는 wave -5의 리소스가 Healthy가 될 때까지 다음 wave로 진행을 안 한다. 그런데 ServiceMonitor가 가리키는 Pod가 떠야 monitoring stack이 Healthy로 판단되는 구조였고, 그 Pod는 cert-manager(wave -3) 없이는 못 뜨고...
dead lock이었다. 한 5분 동안 클러스터가 이 상태로 멈춰있었다.
새벽에 한 삽질
처음에 봤을 때 ArgoCD UI에서 root Application이 "Progressing" 상태로 멈춰 있었다. monitoring child가 "Degraded". 그 안의 ServiceMonitor가 빨간색. 처음엔 그냥 monitoring 문제인 줄 알고 그쪽 manifests만 본 게 첫 번째 삽질이었다. 한 10분 정도 prometheus-operator 로그 뒤지면서 "왜 이게 안 뜨지?" 했다.
kubectl get application -n argocd -o wide
# monitoring Unknown OutOfSync 2026-05-13T18:01:23Z
# cert-manager Unknown OutOfSync -
# ingress Unknown OutOfSync -
여기서 좀 이상한 걸 봤다. cert-manager가 OutOfSync인데 이게 왜? 보통 cert-manager는 떠 있어야 정상이다. 자세히 보니까 sync 자체가 시작 안 됐다. 그제서야 "아 sync wave..." 싶었다.
확인해보니 cert-manager Application의 wave가 -3, 그날 아침 머지된 monitoring이 -5. ArgoCD는 monitoring(wave -5)을 먼저 Healthy로 만들려고 했는데, monitoring이 의존하는 cert-manager(wave -3)는 아직 sync를 시작도 못 한 상태였다.
근데 더 큰 문제는 ALB였다. 우리 ingress-nginx는 NLB → ingress-nginx Pod 구조인데, ingress-nginx Pod도 sync wave 0이라 떠 있었고 traffic은 받고 있었다. 그런데 cert-manager가 안 떠서 TLS handshake가 꼬였고, ALB target health check가 fail하기 시작했다. 그래서 503이 떴던 거다.
급한 대로 monitoring Application의 wave를 다시 2로 올리는 PR을 만들고 머지했다. ArgoCD가 reconcile하는 데 또 한 90초쯤 걸렸고, cert-manager가 뜨면서 인증서가 발급됐고, ingress가 정상으로 돌아왔다. 총 다운타임 32분.
무엇이 잘못됐는가
회고를 다음 날 점심 먹으면서 했다. PR 머지한 동료한테 뭐라 한 게 아니라(그 사람도 좀 미안해 했지만), 우리 팀 프로세스가 못 막아준 게 문제였다. 정리해보면:
첫 번째, sync-wave 값이 코드에 흩어져 있었다. 누가 어떤 wave를 쓰는지 한눈에 볼 수 있는 곳이 없었다. 새 Application 추가할 때 "여기는 어떤 wave가 맞을까"를 판단할 근거가 없으니까, 그냥 적당히 박는 거다.
두 번째, dependency가 implicit하다. monitoring이 cert-manager에 의존한다는 사실은 어디에도 명시되어 있지 않았다. 그냥 우리 머릿속에만 있었다. 사람이 바뀌면 모르는 거다.
세 번째, ArgoCD가 deadlock 상황을 alarm으로 안 띄웠다. "Progressing" 상태로 30분 멈춰있는 것 자체가 abnormal인데, 그걸 잡는 룰이 없었다. 보통 우리 alert은 Pod 단위로 보고 있었지 Application 단위로는 안 보고 있었다.
임시방편으로 한 것들
장기 솔루션 가기 전에 일단 며칠 안에 할 수 있는 것부터 했다.
sync-wave 표를 만들어서 README에 박았다. 어떤 wave가 어떤 컴포넌트인지, 의존 관계는 뭔지. 누가 새 Application 추가할 때 보고 결정하라고. 표 자체는 별 거 아니지만, 이게 없는 것보다는 훨씬 낫다.
wave -10: CRDs (prometheus-operator, cert-manager, gateway-api)
wave -8: webhook controllers
wave -5: cert-manager, external-secrets
wave -3: ingress-nginx, kyverno
wave 0: 일반 app workloads
wave 3: monitoring stack (ServiceMonitor 의존성 때문에 뒤로)
wave 5: cronjobs, batch
ArgoCD app-of-apps 변경 시 review 체크리스트도 추가했다. PR 템플릿에 "sync-wave 값을 변경했나? 변경했다면 표를 함께 업데이트했나?" 같은 항목 두 줄.
Prometheus alert을 하나 추가했다. Application이 Progressing 또는 Degraded 상태로 10분 이상 머무르면 페이지하는 룰. 이게 정말 간단한 건데 왜 1년 동안 안 했나 싶었다.
- alert: ArgoCDApplicationStuck
expr: |
argocd_app_info{sync_status!="Synced"}
and on(name) (time() - argocd_app_sync_total) > 600
for: 10m
labels:
severity: critical
annotations:
summary: "ArgoCD app {{ $labels.name }} stuck in non-synced state for >10min"
길게 봐서는
장기로는 sync-wave 자체를 줄이는 방향으로 가야 할 것 같다. 최근에 ArgoCD ApplicationSet의 Progressive Syncs를 보고 있는데, group 단위로 sync를 진행하고 각 group이 Healthy가 되어야 다음으로 넘어가는 구조다. 이게 sync-wave보다 명시적이다.
그리고 의존성을 Argo CD wave 숫자로 표현하는 것 자체가 좀 모자란 것 같다. 진짜로는 DAG로 표현해야 하는 건데, 현실은 정수 하나로 줄이고 있는 거다. 이 부분은 우리도 더 고민해봐야 한다.
아직 검증 중인 게 많은데, 다음번 새벽 페이지에서는 이런 dead lock이 안 나오기를 바랄 뿐이다. 혹시 다른 팀에서 app-of-apps 운영하면서 sync-wave 관리 어떻게 하시는지 궁금하다. 댓글 남겨주시면 감사하겠다.
추가로, 비슷한 문제 겪으신 분들 Argo CD sync-wave 공식 문서와 Progressive Syncs 문서 한 번 다시 보시는 거 추천드립니다.