ArgoCD sync-wave 잘못 걸었다가 배포가 멈춘 이야기
지난주 화요일 밤, 스테이징에서 새 서비스를 릴리즈하는 중이었다. ArgoCD 앱 하나 만들고, sync 눌렀고, 커피 한 잔 뜨러 갔다 왔다. 돌아왔더니 sync 상태가 Progressing에서 멈춰 있었다. 40분째. 그때부터 새벽 두 시까지 이걸 풀었다.
뭐가 문제였나
우리 팀은 요즘 ArgoCD 앱에 argocd.argoproj.io/sync-wave annotation을 적극적으로 쓴다. 이유는 단순하다. Helm chart 하나에 CRD, Namespace, ConfigMap, Deployment, Service, Ingress가 다 들어 있으면 순서가 꼬인다. CRD가 만들어지기 전에 Custom Resource가 apply되면 sync가 실패한다. 그래서 CRD는 wave 0, Namespace는 wave 1, ConfigMap/Secret은 wave 2, Deployment는 wave 3, 뭐 이런 식으로 걸어놓는다.
그런데 이 서비스는 좀 특별한 게 있었다. 초기화 컨테이너에서 DB 마이그레이션을 도는데, 그게 끝나야 애플리케이션 Pod가 뜨는 구조였다. 팀 내부 논의 끝에 마이그레이션을 별도 Kubernetes Job으로 빼기로 했다. Job이 성공해야 그다음에 Deployment가 올라오는 게 안전하니까.
그래서 이렇게 걸었다.
# migration Job
apiVersion: batch/v1
kind: Job
metadata:
annotations:
argocd.argoproj.io/sync-wave: "5"
argocd.argoproj.io/hook: PreSync
# app Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
argocd.argoproj.io/sync-wave: "6"
문제는 여기서 발생했다. Job에 hook: PreSync를 걸면서 동시에 sync-wave: 5를 걸어놓은 것.
PreSync hook과 sync-wave가 만나면
솔직히 이 조합이 왜 안 되는지 나도 문서를 처음 정독하기 전엔 몰랐다. ArgoCD는 sync를 여러 phase로 나눠서 처리한다.
- PreSync phase — hook이 걸린 리소스들을 먼저 처리
- Sync phase — 나머지 리소스들을 sync-wave 순서대로 apply
- PostSync phase — post-sync hook
그리고 각 phase 안에서도 sync-wave가 동작한다. 즉 PreSync hook 안에서 wave 5의 Job이 완료되기를 기다린다. 그런데 Sync phase의 wave 5, wave 6은 별개다.
내가 헷갈린 지점은 이거였다. Job에 hook: PreSync를 걸면 그 Job은 PreSync phase에서 도는데, 나는 Sync phase의 wave 6인 Deployment가 그 Job이 끝날 때까지 기다리다고 생각했다. 사실 그건 맞다. PreSync가 실패하면 Sync가 시작 안 되니까.
그런데 Job에 ttlSecondsAfterFinished 설정을 안 걸어놨고, 게다가 Job의 restartPolicy: OnFailure로 되어 있었다. 마이그레이션 스크립트가 커넥션 에러로 한 번 실패했고, Pod가 재시작을 시도하면서 무한 루프에 빠졌다. Job 자체는 여전히 Active 상태였고, ArgoCD는 PreSync가 안 끝났다고 판단해서 계속 기다렸다.
40분 동안 뭘 봤나
처음 10분은 그냥 배포가 느린가 싶어서 커피를 두 잔째 마셨다. 그 다음 10분은 Application 리소스를 봤다. argocd app get svc-foo 를 쳤을 때 이렇게 나왔다.
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Job app-foo svc-foo-migration Synced Progressing PreSync
Progressing이 안 끝난다. Job 이벤트를 봤다.
kubectl -n app-foo describe job svc-foo-migration
거기서 뭐가 문제인지 알았다. Pod가 CrashLoopBackOff였다. 정확히는 마이그레이션 스크립트 안에서 DB 연결이 안 됐다. 왜? 그 DB의 credential은 ExternalSecret으로 관리하는데, 그 ExternalSecret 리소스는 sync-wave 2에 있었다. PreSync가 먼저 돌고, Sync가 그 뒤인데, PreSync의 Job이 Sync에서 만들어질 Secret을 참조하고 있었던 것이다.
한마디로 순서가 완전히 거꾸로였다.
해결
두 가지 방법이 있었다.
첫째, Secret을 PreSync 이전에 만드는 방법. Secret에도 argocd.argoproj.io/hook: PreSync와 sync-wave: 4를 걸어서 Job(wave 5)보다 먼저 만들도록. 문서에는 이 방식이 쓰여있긴 한데, ExternalSecret은 실제 Secret이 만들어지기까지 몇 초 걸린다. Job이 시작될 때 Secret이 있을 거라는 보장이 없다.
둘째, 아예 마이그레이션을 hook에서 빼고 InitContainer로 다시 되돌리는 방법. Deployment 안의 InitContainer라면 Deployment가 sync-wave 6에서 만들어질 때, 그때 시점의 Secret은 이미 존재한다. Sync phase 안에서 wave 2 → wave 6 순서로 apply되니까.
결국 후자를 택했다. 팀 안에서 원래 왜 hook Job으로 바꿨던 건지 다시 얘기해봤는데, "Pod의 로그가 InitContainer 안에 묻히면 마이그레이션 실패를 놓치기 쉽다"는 이유였다. 근데 그건 alerting을 InitContainer failure에 걸면 해결되는 문제였다. sync-wave와 hook을 섞다가 배포가 멈추는 리스크보다 훨씬 낫다.
그 후에 알게 된 것
이 삽질을 하고 나서 팀 인프라 위키에 짧은 룰을 추가했다.
- Hook을 걸 거면 그 hook 리소스가 참조하는 모든 것(Secret, ConfigMap, Namespace)도 같은 phase 안에 있어야 한다
- PreSync Job은 sync-wave와 섞지 말고, Job 자체에 명확한 `activeDeadlineSeconds`와 `backoffLimit`을 걸어라
- Sync가 20분 이상 걸리면 알림이 오도록 Prometheus에 rule 추가
마지막 rule은 argocd_app_info metric을 사용해서 걸었다. Sync가 계속 Progressing이면 알림이 뜬다. 예전에는 이 알림이 없어서 40분 동안 커피만 마셨다.
그리고
최근에 ArgoCD 커뮤니티에서 ApplicationSet progressive sync 얘기가 많이 나온다. 여러 클러스터에 순차 배포할 때 쓰는 기능인데, sync-wave와는 레이어가 다르다. sync-wave는 한 앱 안의 리소스 순서, progressive sync는 여러 앱 간의 순서. 우리 팀은 아직 클러스터 하나만 쓰지만, 멀티 클러스터로 넘어가면 이 조합이 좀 복잡해질 것 같다. 일단 지금은 sync-wave부터 제대로 쓰자.
혹시 sync-wave 관련해서 다른 삽질 겪은 분 있으면 댓글로 남겨주세요. 특히 hook 조합에서 뭐 걸린 사례가 궁금합니다.