# 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로 돌려 봅니다. 알림 설정의 오류는 알림이 안 와야 드러나기 때문에 사후 발견이 늦습니다.

# 참고