# ArgoCD Notifications의 네 조각 - 그리고 알림이 소음이 되는 지점
동기화 실패 알림을 슬랙으로 받기로 하고 on-sync-failed를 구독합니다. 다음 날 채널이 같은 메시지로 가득 차 있고, 며칠 뒤에는 아무도 그 채널을 보지 않습니다. 그리고 정작 중요한 장애 하나가 그 사이에 묻힙니다.
문제는 설정이 어려워서가 아닙니다. 애너테이션 한 줄이면 구독이 되기 때문에 "언제 보낼 것인가"를 설계하지 않고 켜게 되는 것이 문제입니다. 이 글에서는 ArgoCD Notifications의 구조가 왜 네 조각으로 나뉘어 있는지, 중복 억제가 실제로 어떻게 동작하는지, 그리고 그 메커니즘이 만드는 부작용을 정리합니다.
# 1. 네 조각과 그 경계
구성 요소는 넷입니다.
| 조각 | 답하는 질문 | 어디에 정의하나 |
|---|---|---|
| Trigger | 언제 보낼 것인가 | argocd-notifications-cm |
| Template | 무엇을 보낼 것인가 | 같은 ConfigMap |
| Service | 어디로 보낼 것인가 | ConfigMap + Secret |
| Subscription | 누가 받을 것인가 | Application 애너테이션 등 |
경계가 이렇게 그어진 이유가 있습니다. 앞의 셋은 플랫폼이 정의하고, 마지막 하나만 애플리케이션 소유자가 정합니다. 트리거와 템플릿을 팀마다 만들게 두면 같은 조건이 이름만 다르게 열 개 생기고, 그때부터 "왜 이 알림이 왔는지"를 아무도 설명하지 못합니다.
규칙으로 정리하면, 트리거와 템플릿은 플랫폼이 소유하고 팀은 구독만 하는 구조가 유지 가능합니다.
구독은 애너테이션으로 붙습니다.
metadata:
annotations:
notifications.argoproj.io/subscribe.on-sync-failed.slack: my-channel
형식은 subscribe.<trigger>.<service>이고 값이 수신자입니다.
# 2. 트리거는 조건이 참인 동안 계속 참이다
트리거의 필드는 셋입니다.
when: 보낼지 판단하는 표현식send: 실행할 템플릿 목록oncePer: 중복을 막는 기준
여기서 첫 번째 오해가 나옵니다. when이 상태 변화를 감지한다고 생각하기 쉽지만, 실제로는 컨트롤러가 애플리케이션을 평가할 때마다 이 표현식을 계산합니다. 동기화가 실패한 상태로 머물러 있으면 평가할 때마다 참입니다.
즉 oncePer 없는 트리거는 조건이 해소될 때까지 반복 발송됩니다. 채널이 같은 메시지로 차는 이유가 이것입니다.
여기에 상태가 요동치는 경우가 겹칩니다. 배포 중에는 헬스 상태가 잠깐 Degraded로 떨어졌다가 회복되는 것이 정상입니다. on-health-degraded를 그대로 구독하면 정상 배포마다 장애 알림이 옵니다. 알림을 무시하는 습관은 여기서 시작됩니다.
# 3. oncePer는 상태를 애플리케이션에 적어 둔다
중복 억제는 oncePer가 담당합니다. 동작은 이렇습니다.
oncePer: app.status.sync.revision
이렇게 걸면 리비전이 바뀔 때만 보냅니다. 같은 커밋으로 반복 시도하며 실패하는 동안에는 한 번만 옵니다.
구현은 애플리케이션 리소스에 상태를 적는 방식입니다. 트리거가 발동하면 그때의 기준값이 애너테이션으로 기록되고, 다음 평가에서 그 값이 그대로면 발송을 건너뜁니다. 여기서 세 가지 부작용이 따라옵니다.
첫째, 기준을 잘못 고르면 침묵합니다. app.status.sync.revision은 리비전 단위로 억제하므로, 같은 커밋으로 재시도하다 계속 실패하는 상황을 한 번만 알립니다. 첫 실패 알림을 놓치면 그 뒤로는 조용합니다. 원하는 동작일 수도 있고 아닐 수도 있는데, 이 선택을 의식적으로 하는 사람이 드뭅니다.
반대로 매 시도마다 알림을 받고 싶다면 시도마다 값이 바뀌는 필드를 기준으로 삼아야 하고, 그러면 2절의 폭주로 돌아갑니다. oncePer는 "덜 받기"와 "놓치기" 사이의 손잡이이고, 중간값이 없습니다.
실용적인 해법은 두 트리거를 나누는 것입니다.
# 사람을 부르는 알림 - 리비전당 한 번
trigger.on-sync-failed-alert: |
- when: app.status.operationState.phase in ['Error', 'Failed']
oncePer: app.status.sync.revision
send: [app-sync-failed]
# 기록용 - 억제 없이 전부, 사람이 안 보는 채널로
trigger.on-sync-failed-log: |
- when: app.status.operationState.phase in ['Error', 'Failed']
send: [app-sync-failed-brief]
같은 조건이라도 목적이 다르면 트리거를 나눕니다. 하나로 합치려 하면 억제 강도를 정할 수 없습니다.
둘째, 애플리케이션 오브젝트가 커집니다. 구독과 트리거 조합마다 상태가 붙습니다. 수백 개 애플리케이션에 여러 트리거를 걸면 etcd에 저장되는 오브젝트가 그만큼 부풀고, 목록 조회가 무거워집니다.
셋째, GitOps와 충돌합니다. 애플리케이션 리소스를 Git으로 관리하면 컨트롤러가 붙인 이 애너테이션이 Git에 없는 필드로 나타납니다. ArgoCD가 애플리케이션 자신을 관리하는 구성이라면 계속 OutOfSync로 표시되고, 동기화할 때마다 상태가 지워집니다. 상태가 지워지면 억제도 초기화되어 알림이 다시 옵니다.
이 부류의 충돌은 무시 규칙으로 다뤄야 합니다. 컨트롤러가 런타임에 쓰는 필드를 Git이 되돌리는 문제 일반은 ArgoCD ignoreDifferences가 동작하지 않는 세 가지 이유 (opens new window)에 정리했습니다.
# 4. 무엇을 사람에게 보낼 것인가
기술적 설정보다 이 결정이 알림의 수명을 정합니다. 기준을 하나 두면 이렇습니다. 사람이 지금 무언가를 해야 하는가. 아니라면 사람 채널에 보내지 않습니다.
| 이벤트 | 사람이 할 일 | 어디로 |
|---|---|---|
| 동기화 실패 (재시도 중) | 없음 - 자동 재시도가 처리 | 로그 채널 |
| 동기화 실패 (재시도 소진) | 있음 | 사람 채널 |
헬스 Degraded (배포 중) | 없음 | 보내지 않음 |
헬스 Degraded (배포 후 지속) | 있음 | 사람 채널 |
| 동기화 성공 | 없음 | 로그 채널 또는 없음 |
동기화 상태 Unknown | 있음 - 대개 설정 문제 | 사람 채널 |
배포 중 Degraded를 거르는 것이 특히 중요합니다. 운영 중인 애플리케이션이 진짜로 나빠진 것과 롤아웃 중간 상태를 조건에서 구분해야 합니다.
trigger.on-health-degraded-real: |
- when: >
app.status.health.status == 'Degraded' and
(app.status.operationState == nil or
app.status.operationState.phase not in ['Running'])
oncePer: app.status.sync.revision
send: [app-health-degraded]
진행 중인 작업이 없을 때의 Degraded만 잡습니다. 완벽하지는 않지만(작업이 끝난 직후 잠깐 걸릴 수 있음) 배포 창의 대부분을 걸러 냅니다. 더 정확하게 하려면 지연 평가가 필요한데, ArgoCD Notifications에는 그 기능이 없으므로 이 알림을 메트릭 기반 알림으로 옮기는 것이 맞는 경우가 많습니다.
여기서 경계가 하나 그어집니다. ArgoCD Notifications는 배포 파이프라인의 이벤트를 알리는 도구입니다. "서비스가 건강한가"는 메트릭과 알림 규칙이 답할 질문이고, 두 가지를 같은 도구로 하려 하면 둘 다 어설퍼집니다.
# 5. 구독을 어디에 둘 것인가
애플리케이션마다 애너테이션을 다는 방식은 개수가 늘면 관리되지 않습니다. 수백 개 애플리케이션에 같은 애너테이션을 복사해 두면, 채널을 바꿀 때 전부 고쳐야 합니다.
선택지가 셋입니다.
(A) ApplicationSet 템플릿에 넣는다. 생성 주체가 하나이므로 한 곳만 고치면 됩니다. 애플리케이션을 ApplicationSet으로 만드는 구성이라면 가장 자연스럽습니다.
template:
metadata:
annotations:
notifications.argoproj.io/subscribe.on-sync-failed-alert.slack: '{{.team}}-alerts'
(B) 전역 구독을 쓴다. ConfigMap에 기본 구독을 정의하고 선택자로 대상을 좁힙니다. 팀이 아무것도 안 해도 기본 알림이 걸린다는 점이 장점이자 단점입니다.
(C) 애플리케이션 애너테이션. 예외적인 앱에만 씁니다.
기준을 정하면, 전역 기본값으로 최소한의 안전망을 깔고(예: 동기화 상태 Unknown), 팀별 상세 구독은 생성 템플릿에 두는 조합이 유지 가능합니다. 개별 애너테이션은 예외 처리 수단으로만 남깁니다.
# 6. 보내기 전에 확인하는 방법
알림 설정의 어려운 점은 틀렸다는 것을 알림이 안 와야 알게 된다는 것입니다. 그래서 실제 상태로 미리 돌려 보는 것이 중요합니다. CLI가 이것을 지원합니다.
# 정의된 트리거와 템플릿 확인
argocd admin notifications trigger get
argocd admin notifications template get
# 로컬 파일로 검증 - 클러스터에 반영하기 전에
argocd admin notifications trigger get \
--config-map ./argocd-notifications-cm.yaml --secret :empty
# 실제 애플리케이션 상태에 템플릿을 적용해 보낸다
argocd admin notifications template notify \
app-sync-succeeded guestbook --recipient slack:my-channel
컨트롤러 파드 안에서 실행할 수도 있습니다.
kubectl exec -it -n argocd argocd-notifications-controller-<hash> -- \
/usr/local/bin/argocd admin notifications trigger get
트리거 조건이 지금 참인지 보려면 표현식에 쓰는 필드를 직접 확인하는 것이 빠릅니다.
# 조건에 쓰는 값들을 한 번에 본다
kubectl -n argocd get application myapp -o jsonpath='{
"sync="}{.status.sync.status}{
" health="}{.status.health.status}{
" phase="}{.status.operationState.phase}{
" rev="}{.status.sync.revision}{"\n"}'
# 억제 상태가 어떻게 기록돼 있는가
kubectl -n argocd get application myapp \
-o jsonpath='{.metadata.annotations}' | tr ',' '\n' | grep notifications
마지막 명령이 유용합니다. 알림이 안 오는 이유가 조건 불충족인지 억제인지 여기서 갈립니다. 억제 상태가 남아 있는데 다시 받고 싶다면 해당 애너테이션을 지우면 됩니다.
# 억제 상태 초기화 (키 이름은 위 출력에서 확인한 것을 쓴다)
kubectl -n argocd annotate application myapp <해당-키>-
발송 자체가 실패하는 경우는 컨트롤러 로그에 남습니다.
kubectl -n argocd logs deploy/argocd-notifications-controller --tail=100 | grep -i "trigger\|notif\|error"
# 7. 트러블슈팅
| 증상 | 원인 | 조치 |
|---|---|---|
| 같은 알림이 반복해서 옴 | oncePer 없음, 조건이 계속 참 | oncePer 추가 |
| 배포할 때마다 장애 알림 | Degraded가 롤아웃 중간 상태 | 진행 중 작업을 조건에서 제외 |
| 실패가 이어지는데 알림은 한 번 | oncePer가 리비전 단위 | 목적별로 트리거 분리 |
| 알림이 아예 안 옴 | 억제 상태가 남아 있음 | 애너테이션 확인 후 초기화 |
| 동기화할 때마다 알림 재발송 | GitOps가 상태 애너테이션을 지움 | 무시 규칙 추가 |
애플리케이션이 계속 OutOfSync | 컨트롤러가 붙인 애너테이션 | 위와 동일 |
| 채널 변경에 수백 개 앱 수정 | 개별 애너테이션 관리 | 생성 템플릿 또는 전역 구독 |
| 트리거는 맞는데 발송 실패 | 서비스 자격 증명·수신자 오류 | 컨트롤러 로그, template notify로 확인 |
| 알림을 아무도 안 봄 | 행동이 필요 없는 알림이 섞임 | 사람 채널과 로그 채널 분리 |
# 8. 마무리
- 네 조각의 경계는 소유권의 경계입니다. 트리거·템플릿·서비스는 플랫폼이, 구독만 팀이 갖는 구조여야 규칙이 유지됩니다.
- 트리거의
when은 상태가 변할 때가 아니라 매 평가마다 계산됩니다.oncePer가 없으면 조건이 해소될 때까지 계속 보냅니다. oncePer는 상태를 애플리케이션 애너테이션에 적습니다. 그래서 GitOps 동기화가 그 상태를 지우면 억제가 초기화되고 알림이 다시 옵니다.oncePer의 기준은 "덜 받기"와 "놓치기"를 가르는 손잡이입니다. 목적이 다르면 같은 조건이라도 트리거를 나누는 편이 낫습니다.- 배포 중
Degraded를 거르지 않으면 정상 배포마다 장애 알림이 옵니다. 알림 무시 습관이 여기서 만들어집니다. - ArgoCD Notifications는 배포 이벤트를 알리는 도구입니다. 서비스 건강 여부는 메트릭 기반 알림의 몫이고, 한 도구로 둘을 하려 하면 둘 다 어설퍼집니다.
- 설정은 반영 전에
argocd admin notifications로 돌려 봅니다. 알림 설정의 오류는 알림이 안 와야 드러나기 때문에 사후 발견이 늦습니다.