무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
ntfy + ArgoCD 배포 알림: 글로벌 구독의 함정과 per-app 알림
홈랩 삽질기
2026. 1. 5.

ntfy + ArgoCD 배포 알림: 글로벌 구독의 함정과 per-app 알림

ntfyArgoCDGitOpsKuberneteshomelab

git push를 하고 나면 늘 ArgoCD UI를 열어봐야 했다. 배포가 됐는지, 아니면 중간에 뭔가 꼬였는지. 터미널에서 kubectl get pods를 치는 것도 한두 번이지, 매번 이러자니 번거로웠다.

그래서 배포 알림을 만들기로 했다. ntfy라는 셀프 호스팅 push notification 서버를 K8s에 올리고 ArgoCD notifications controller와 연결하면, 배포 성공 시 자동으로 모바일 알림이 온다. Slack이나 Discord 같은 외부 서비스 없이도 가능하다.

결론부터 말하면 구조 자체는 단순한데 글로벌 구독이라는 함정에 빠져서 시간을 좀 썼다.

ntfy 셀프 호스팅

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 연동

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는 K8s 내부 DNS(ntfy.ntfy.svc.cluster.local)로 접근한다. 외부 URL을 쓸 필요가 없다
  • Content-Type을 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으로 전환

해결 방법은 글로벌 구독을 버리고 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'를 넣어둔 건 혹시 모를 상황에 대한 안전장치다.

Gitea webhook 삽질

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 타입을 쓰는 게 정답이다.

배운 것

알림 시스템을 만들면서 느낀 건, 알림의 가치는 "무엇을 알려주느냐"가 아니라 "무엇을 안 알려주느냐"에 있다는 점이다. 모든 이벤트를 다 알려주면 결국 아무것도 안 보게 된다.

  • mono-repo GitOps에서 배포 알림은 반드시 per-app annotation으로 설정해야 한다. 글로벌 구독은 앱 수만큼 알림이 폭주한다
  • ntfy + ArgoCD 조합은 Slack이나 PagerDuty 없이 무료로 모바일 배포 알림을 구현하는 가장 가벼운 방법이다
  • Gitea + ArgoCD webhook은 gogs 타입을 쓸 것. gitea 타입은 헤더 호환 문제가 있다
  • oncePer 설정은 필수다. 없으면 ArgoCD가 health check를 반복할 때마다 알림이 중복된다

이제 git push만 하면 잠시 후 폰에 알림이 온다. 짧은 메시지 하나. "museck-staging deployed (a1d3f31)". 이 한 줄이 오면 ArgoCD UI를 열 필요가 없다.

자주 묻는 질문

ntfy를 K8s에 셀프 호스팅하면 외부 알림 서비스 없이 배포 알림을 받을 수 있나요?
네. ntfy는 경량 HTTP 기반 push notification 서버로 K8s Pod 하나로 운영 가능합니다. ArgoCD notifications controller와 webhook으로 연동하면 배포 결과를 모바일로 즉시 받을 수 있습니다.
ArgoCD 알림이 너무 많이 올 때 해결 방법은?
글로벌 구독 대신 per-app annotation 방식을 사용합니다. 알림이 필요한 Application에만 annotation을 추가하고 App of Apps root 앱은 제외하면 알림 폭주를 방지할 수 있습니다.
ArgoCD notifications controller와 ntfy를 연결하는 방법은?
ArgoCD notifications ConfigMap에 ntfy를 webhook service로 등록하고 template에 ntfy HTTP API 포맷을 설정합니다. trigger 조건에 맞는 이벤트 발생 시 자동으로 ntfy에 POST 요청이 전송됩니다.
홈랩 삽질기(13/19)
Prev

Postiz 소셜 미디어 자동화 배포: Helm OCI + Temporal 삽질기

Next

Umami Analytics를 K8s에 셀프호스팅하기: GA 없이 웹 분석하는 법