IT/CI CD

ArgoCD sync wave에 배포가 물렸다, 새벽 두 시 회고

gfrog 2026. 7. 21. 09:12

지난 목요일 새벽 두 시. 슬랙에 "prod 배포 30분째 안 끝나요"라는 메시지가 올라왔다. 나는 이미 자고 있었고, 폰 진동에 눈이 번쩍 떠졌다. 노트북을 열어보니 ArgoCD 대시보드가 애매한 상태였다. Application은 Syncing 인데 진행률은 그대로. 사실상 멈춰 있었다.

이 글은 그날 새벽에 뭘 삽질했고, 왜 그런 일이 벌어졌는지에 대한 회고다. 결론부터 말하면 sync wave 하나가 조용히 우리 파이프라인을 인질로 잡고 있었다. 최근 ArgoCD 공식 문서와 오퍼레이션 노트를 다시 정리하다 보니 이 삽질담을 남기는 게 낫겠다 싶었다.

상황: 어디가 막혔는지도 모르겠다

우리 팀은 프로덕션 배포를 GitOps로 굴린다. Argo CD가 이십 몇 개 애플리케이션을 관리하고 있고, 그중 하나가 그날 밤 히스토리를 새로 쓰는 중이었다. 새 서비스 릴리스였는데, PreSync 훅으로 DB 마이그레이션 Job이 붙어 있었다. 그 Job이 끝나야 앱 파드가 뜬다.

문제는 이랬다. Argo CD UI에서는 마이그레이션 Job이 Succeeded로 보였다. 근데 그 뒤 wave에 있는 Deployment 리소스는 아예 시작을 안 했다. 스크린샷을 몇 장 찍어서 팀 채널에 올렸다. "이거 wave가 안 넘어가는데요?"

동료 한 명이 잠결에 답했다. "sync-wave annotation 확인해봤어요?"

첫 번째 삽질: annotation 오타

kubectl get application -n argocd our-service -o yaml을 뜯어봤다. Application 자체는 문제 없다. 그럼 개별 리소스를 봐야지.

kubectl get job db-migrate-1234 -n our-ns -o yaml | grep -A2 annotations

여기서 이상한 걸 봤다.

annotations:
  argocd.argoproj.io/sync-waves: "-1"

sync-waves. 복수형. Argo CD가 인식하는 키는 sync-wave (단수)다. 오타 하나 때문에 이 Job은 wave 0으로 취급됐고, wave 0에 있는 Deployment랑 같은 물결에 실리면서 순서가 꼬였다. 정확히는 Job이 먼저 시작되긴 했지만 Argo CD 입장에서는 "wave 0 리소스 다 healthy 될 때까지 기다린다"였다.

근데 이게 왜 stuck으로 보였을까? 그건 두 번째 문제였다.

두 번째 삽질: Job이 Succeeded인데 Application은 Progressing

Job 리소스는 status.succeeded: 1이었다. kubectl get job으로 봤을 때 확실히 완료. 근데 Application의 sync 상태는 여전히 진행 중이었다.

여기서 십 분쯤 헤맸다. Argo CD 로그를 봤다.

kubectl logs -n argocd deploy/argocd-application-controller --tail=200 | grep our-service

로그에서 이런 라인이 눈에 들어왔다.

wave 0: 1 out of 2 resources are healthy, waiting

Deployment가 healthy가 아니었다. 왜? 파드가 안 뜨고 있었으니까. 왜 안 떴을까? DB 마이그레이션이 도중에 스키마를 잠깐 락을 잡고 있는데, 그 시점에 파드가 뜨면서 startup probe가 DB 커넥션 초기화에서 계속 실패했다. CrashLoopBackOff.

정리하면 이런 그림이다.

  1. sync-wave 오타로 Job이 wave 0에 배정됨
  2. Argo CD가 Job이랑 Deployment를 병렬로 적용
  3. Job은 마이그레이션 중이라 스키마 락 보유
  4. Deployment 파드가 뜨면서 락 대기, startup probe 타임아웃, CrashLoop
  5. Deployment가 healthy 안 되니까 wave 0 완료 판정 안 됨
  6. sync가 영원히 진행 중

정말 아름다운 데드락이었다. 새벽 세 시.

어떻게 풀었나

일단 긴급 조치는 단순했다. Deployment를 kubectl scale --replicas=0으로 죽여서 Job이 조용히 끝나게 두고, 마이그레이션 완료 확인 후 Application을 hard refresh + 다시 sync. 5분쯤 걸렸다.

근본 원인 수정은 이렇다. 매니페스트 리포에 있는 Job에 붙은 annotation을 고쳤다.

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
    argocd.argoproj.io/sync-wave: "-1"

몇 가지 지킬 것:

  • sync-wave 값은 문자열이어야 한다. -1 (숫자)으로 쓰면 어떤 클라이언트에서는 파싱이 이상하게 된다. 반드시 "-1" 쿼트.
  • hookPreSync로 지정하면 아예 별도 phase로 돌기 때문에 wave 순서와 독립적으로 먼저 실행된다. 이게 사실 원래 우리가 원했던 동작.
  • hook-delete-policy를 붙여서 이전 Job이 살아 있을 때 새 Job 생성이 꼬이지 않게 한다.

여기서 하나 더 배웠는데, PreSync 훅과 sync-wave는 서로 다른 축이다. 훅은 sync 프로세스의 phase를 결정하고 (PreSync → Sync → PostSync), sync-wave는 같은 phase 안에서 리소스의 순서를 결정한다. 우리 팀 신규 매니페스트에서 이 두 개를 헷갈려 쓴 흔적을 몇 개 더 찾아냈다.

재발 방지: CI에서 잡자

수동 리뷰로만 걸러내기엔 sync-wave 오타는 너무 조용하다. wave 0으로 fallback되면서 아무 에러도 안 뜬다. 그래서 우리는 두 가지를 추가했다.

첫째, 매니페스트 리포에 kyverno 정책을 하나 붙였다. argocd.argoproj.io/로 시작하는 annotation 중 유효한 키가 아니면 CI가 실패한다. sync-waves, sync-wav, sync_wave 이런 오타들을 다 잡는다. 정확히 말하면 argocd.argoproj.io/sync-wave, argocd.argoproj.io/hook, argocd.argoproj.io/hook-delete-policy 이 목록 외의 키는 거부한다.

둘째, argocd app diff 결과를 PR 코멘트로 붙이는 봇을 다시 손봤다. wave 순서를 표로 그려주게 했다. wave -1에 Job, wave 0에 Deployment, wave 1에 Service 이런 식으로 시각화된다. 사람이 보고 "어? 이 Job wave 0이네?" 하고 알아챌 수 있게.

아직도 마음에 걸리는 것

sync-wave annotation을 문자열로 강제하는 것 자체가 API 설계 관점에서는 좀 이상하긴 하다. 최근 커뮤니티에서도 비슷한 논의가 계속 나온다. 값이 정수인지 검증하는 웹훅을 어드미션 컨트롤러 레벨에서 붙일까 고민 중이다.

또 하나. wave 안에서 healthy 판정 로직이 리소스 타입마다 다르다. Deployment는 replicas와 readyReplicas 비교, StatefulSet은 순차 롤아웃, Job은 succeeded. 이걸 정확히 이해하지 못하면 "왜 이 wave가 안 넘어가지" 하고 또 새벽 세 시에 눈이 떠질 수 있다. 다음에 우리 팀 온보딩 문서에 이거 챕터 하나 추가할 생각이다.

혹시 비슷한 상황 겪으신 분 있으면 어떻게 해결하셨는지 댓글로 남겨주세요. 저는 이제 자러 갑니다. 진짜로.