
git push를 하고 나면 늘 ArgoCD UI를 열어봐야 했다. 배포가 됐는지, 아니면 중간에 뭔가 꼬였는지. 터미널에서 kubectl get pods를 치는 것도 한두 번이지, 매번 이러자니 번거로웠다.
그래서 배포 알림을 만들기로 했다. ntfy라는 셀프 호스팅 push notification 서버를 K8s에 올리고 ArgoCD notifications controller와 연결하면, 배포 성공 시 자동으로 모바일 알림이 온다. Slack이나 Discord 같은 외부 서비스 없이도 가능하다.
결론부터 말하면 구조 자체는 단순한데 글로벌 구독이라는 함정에 빠져서 시간을 좀 썼다.
ntfy는 Go로 만든 경량 push notification 서버다. HTTP PUT/POST 하나로 메시지를 보낼 수 있고, 웹 UI와 모바일 앱(Android/iOS)을 기본 제공한다. 공식 서버(ntfy.sh)를 써도 되지만 홈랩이니까 직접 올렸다.
배포는 간단하다. ConfigMap으로 서버 설정을 넣고 Deployment + Service + IngressRoute를 만들면 끝이다.
# ntfy ConfigMap (server.yml)
apiVersion: v1
kind: ConfigMap
metadata:
name: ntfy-config
namespace: ntfy
data:
server.yml: |
base-url: https://ntfy.xssh.org
listen-http: ":80"
cache-file: /var/cache/ntfy/cache.db
cache-duration: "12h"
behind-proxy: true테스트도 curl 한 줄이면 된다.
curl -d 'hello from homelab' ntfy.xssh.org/test-topic모바일 앱에서 ntfy.xssh.org/test-topic을 구독하면 바로 알림이 뜬다. 여기까진 5분도 안 걸렸다.
ArgoCD에는 notifications controller가 내장되어 있다. Helm chart의 notifications 섹션에서 활성화하면 되는데, 여기서 첫 삽질이 시작됐다.
Helm chart v9.4.0 기준으로 notifications 설정 키 구조가 예전과 다르다. 인터넷에 돌아다니는 예제 대부분은 notifications.cm 아래에 설정을 넣는 방식인데 현재 버전에서는 무시된다. 올바른 키는 notifiers, templates, triggers다.
최종 설정은 이렇게 생겼다.
notifications:
enabled: true
notifiers:
service.webhook.ntfy: |
url: http://ntfy.ntfy.svc.cluster.local/homelab-deploy
headers:
- name: Content-Type
value: text/plain
templates:
template.app-deployed: |
webhook:
ntfy:
method: POST
body: "{{.app.metadata.name}} deployed ({{.app.status.sync.revision | substr 0 8}})"
triggers:
trigger.on-deployed: |
- when: >-
app.status.operationState.phase in ['Succeeded']
and app.status.health.status == 'Healthy'
and app.metadata.name != 'root'
oncePer: app.status.sync.revision
send: [app-deployed]핵심 포인트를 짚어보면 이렇다.
ntfy.ntfy.svc.cluster.local)로 접근한다. 외부 URL을 쓸 필요가 없다text/plain으로 지정해야 ntfy가 body를 메시지 본문으로 인식한다Succeeded + Healthy를 둘 다 확인해야 실제로 Pod가 정상 구동된 시점에 알림이 간다oncePer로 같은 revision에 대한 중복 알림을 막는다처음에는 글로벌 구독을 썼다. subscriptions 설정에서 모든 앱에 on-deployed 트리거를 걸어두는 방식이다. 설정이 간단하니까 당연히 이렇게 하는 거라고 생각했다.
문제는 homelab 레포가 mono-repo라는 점이었다. Kustomize overlay 하나를 수정해서 push하면 ArgoCD의 root 앱이 sync되고, root 앱이 관리하는 모든 자식 앱의 revision도 바뀐다. 결과적으로 20개 넘는 앱이 전부 sync를 시작하고 각각 알림을 보냈다.
museck 하나만 배포했는데 알림이 20개씩 오니까 알림을 끄고 싶어졌다. 알림 시스템을 만든 지 10분 만에 알림 피로(notification fatigue)를 체험한 셈이다.
해결 방법은 글로벌 구독을 버리고 per-app annotation을 쓰는 것이다. 알림을 받고 싶은 앱의 Application 리소스에만 annotation을 추가한다.
# museck-staging Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: museck-staging
annotations:
notifications.argoproj.io/subscribe.on-deployed.ntfy: ""이제 museck-staging과 museck-production에서만 알림이 온다. 나머지 18개 앱은 조용하다.
하나 더 추가한 건 root 앱 제외 조건이다. App of Apps 패턴에서 root Application은 자식 앱이 sync될 때마다 같이 상태가 바뀐다. root 앱에 annotation을 안 달아도 글로벌 구독 시절에는 문제가 됐지만, per-app 방식에서는 annotation 자체를 안 달면 되니까 자연스럽게 해결된다. 트리거 조건에 app.metadata.name != 'root'를 넣어둔 건 혹시 모를 상황에 대한 안전장치다.
ArgoCD가 Git 변경을 감지하는 속도를 높이려면 webhook을 설정해야 한다. 기본 polling 간격이 3분이라 push 후 알림이 늦게 올 수 있기 때문이다.
Gitea에서 webhook을 만들 때 타입을 gitea로 설정했더니 ArgoCD 로그에 Unknown webhook event가 찍혔다. ArgoCD는 X-Gitea-Event 헤더를 인식하지 못한다. gogs 타입으로 바꾸면 X-Gogs-Event 헤더가 전송되고 ArgoCD가 정상 처리한다.
Gitea가 Gogs에서 fork된 프로젝트라 이런 호환성 이슈가 있다. ArgoCD 쪽에서 Gitea를 공식 지원하지 않는 한 gogs 타입을 쓰는 게 정답이다.
알림 시스템을 만들면서 느낀 건, 알림의 가치는 "무엇을 알려주느냐"가 아니라 "무엇을 안 알려주느냐"에 있다는 점이다. 모든 이벤트를 다 알려주면 결국 아무것도 안 보게 된다.
gogs 타입을 쓸 것. gitea 타입은 헤더 호환 문제가 있다oncePer 설정은 필수다. 없으면 ArgoCD가 health check를 반복할 때마다 알림이 중복된다이제 git push만 하면 잠시 후 폰에 알림이 온다. 짧은 메시지 하나. "museck-staging deployed (a1d3f31)". 이 한 줄이 오면 ArgoCD UI를 열 필요가 없다.