
git push 한 번에 스테이징 서버가 업데이트되고, 블로그 글을 발행하면 4개 소셜 미디어에 동시 포스팅된다. 홈랩에서 11개 프로젝트를 만들면서 이 "도미노 자동화"가 어느새 인프라 전체를 관통하게 됐다.
처음부터 이렇게 설계한 건 아니다. 하나씩 만들다 보니 서비스끼리 웹훅과 이벤트로 연결되기 시작했고, 어느 순간 하나를 건드리면 나머지가 알아서 반응하는 구조가 만들어졌다. 이 글에서는 실제로 돌아가고 있는 자동화 체인 세 가지를 뜯어보고, n8n이 이 체인들의 허브 역할을 어떻게 하는지 정리해본다.
가장 기본이 되는 체인이다. 코드를 master에 push하면 끝. 나머지는 전부 자동이다.
Gitea가 push 이벤트를 받으면 act_runner가 워크플로우를 실행한다. Docker-in-Docker 환경에서 이미지를 빌드하고 Gitea 레지스트리에 push한 뒤, homelab 레포의 staging kustomization.yaml에 새 이미지 태그를 써넣는다. ArgoCD가 이 변경을 감지하면 K8s 클러스터에 롤링 업데이트를 수행하고, 배포가 완료되면 ntfy를 통해 내 폰에 push 알림이 온다.
총 5개 서비스가 순차적으로 반응하는데(Gitea → act_runner → Registry → ArgoCD → ntfy) 각 서비스는 자기 역할만 안다. act_runner는 ArgoCD의 존재를 모르고, ArgoCD는 ntfy를 모른다. 연결 고리는 오직 "파일 변경"뿐이다.
Production 배포는 한 단계가 더 있다. Gitea UI에서 workflow_dispatch를 수동으로 트리거하는데, 이때 태그를 빈칸으로 두면 현재 staging에 배포된 태그를 자동으로 가져온다. staging에서 검증된 이미지를 그대로 production으로 승격시키는 패턴이다.
이 체인은 좀 더 복잡하다. AI 에이전트가 글을 쓰는 것부터 시작한다.
blog-writer 서브에이전트가 글감 파일을 받아 Lexical richText JSON으로 블로그 글을 작성하고, ComfyUI로 이미지를 생성한 다음, MCP 프로토콜을 통해 PayloadCMS에 draft로 저장한다. 사용자가 관리자 패널에서 확인하고 published로 바꾸면 그때부터 두 번째 도미노가 넘어진다.
PayloadCMS의 afterChange 훅이 발행을 감지한다. 정확히는 _status가 published로 바뀌는 순간만 잡는다.
const isPublishing =
doc._status === 'published' &&
(operation === 'create' || previousDoc?._status \!== 'published')이 조건을 통과하면 n8n 웹훅으로 POST 요청이 날아간다. n8n 워크플로우는 블로그 콘텐츠를 다시 조회해서 Claude API에 넘긴다. Claude가 LinkedIn, X, Threads, Instagram 각 플랫폼에 맞는 요약을 JSON으로 생성하면, n8n이 Postiz API를 통해 4개 플랫폼에 동시 배포한다.
블로그 글 하나를 발행하면 6개 노드를 거쳐 4개 플랫폼에 콘텐츠가 퍼진다. 내가 하는 건 draft를 확인하고 발행 버튼을 누르는 것뿐.
translate.museck.com에 접속하면 PDF 논문을 번역해주고, asr.museck.com에서는 동영상 자막을 생성해준다. 이 두 서비스의 체인 구조가 흥미로운데, 하나의 URL 뒤에 세 겹의 프록시가 숨어있다.
Cloudflare Tunnel이 외부 트래픽을 K8s 내부 Traefik으로 전달한다. Traefik은 루트 경로(/)를 n8n 웹훅으로 리다이렉트해서 HTML 페이지를 서빙하고, /api/* 경로는 StripPrefix 후 GPU 서버로 직접 라우팅한다. 파일 업로드처럼 무거운 작업은 n8n을 우회해서 GPU 서버와 브라우저가 직접 통신한다.
처음에는 n8n이 파일 업로드까지 처리했다. 수백 MB짜리 동영상이 n8n을 경유하면서 OOM으로 뻗었다. 아키텍처를 바꿔서 n8n은 HTML 서빙만 담당하고, 실제 데이터는 브라우저에서 GPU 서버로 직접 보내는 구조로 전환했다. 이 경험이 하나의 교훈을 남겼는데, n8n 같은 워크플로우 도구는 오케스트레이션에 쓰고 대용량 데이터 파이프는 직접 연결하는 게 맞다.
세 체인 모두에 n8n이 등장한다. 배포 체인에서는 직접 관여하지 않지만, 콘텐츠 체인과 서비스 체인의 허브 역할을 한다. K8s에 SQLite + NFS 듀얼 볼륨으로 배포해뒀고, ArgoCD가 GitOps로 관리한다.
n8n의 진짜 강점은 웹훅이다. 어떤 서비스든 HTTP POST를 보낼 수 있으면 n8n과 연결된다. PayloadCMS의 afterChange 훅, 외부 사용자의 브라우저 요청, Gitea의 push 이벤트 모두 웹훅 하나로 n8n에 들어온다. 여기서 API 호출, AI 처리, 파일 저장, 외부 서비스 배포까지 노드 몇 개로 연결하면 하나의 자동화 체인이 완성된다.
NFS 공유 스토리지를 마운트해둔 것도 유용하다. 번역된 PDF가 n8n의 /shared 경로를 통해 Obsidian vault에 바로 저장된다. Syncthing이 이걸 다른 기기로 동기화해주니까 번역 결과를 별도로 다운로드할 필요가 없다.
도미노가 중간에 멈추면 어디서 멈췄는지 찾아야 한다. 각 체인별로 겪은 실제 장애와 디버깅 경험을 정리해본다.
ArgoCD notifications를 처음 설정했을 때 글로벌 구독을 사용했다. homelab 레포에 push할 때마다 20개가 넘는 앱이 전부 sync되면서 알림이 폭주했다. 하루에 수십 개씩 알림이 울리니까 진짜 중요한 알림을 놓치게 된다.
per-app annotation 방식으로 바꿨다. 알림이 필요한 앱(museck-staging, museck-production)에만 annotation을 달아두는 식이다. App of Apps 패턴의 root 앱은 자식 sync마다 같이 반응하니까 트리거 조건에서 제외했다.
# 트리거 조건: root 앱 제외 + 같은 revision 중복 방지
when: app.status.operationState.phase in ['Succeeded']
and app.status.health.status == 'Healthy'
and app.metadata.name \!= 'root'
oncePer: app.status.sync.revision블로그에서 소셜 미디어로 이어지는 체인을 만들 때 Postiz API 문서가 불완전해서 같은 날 3번이나 수정 커밋을 찍었다. 엔드포인트가 /api/posts가 아니라 /public/v1/posts였고, 인증 형식이 Bearer 토큰이 아니라 raw key였다. body 구조도 문서와 달라서 시행착오로 알아냈다.
Claude API 응답도 변수였다. JSON만 반환하라고 지시해도 가끔 markdown code fence로 감싸서 내보낸다. 파싱 단계에 fence 제거 로직을 넣어둬야 체인이 끊기지 않는다.
CI/CD 체인의 첫 번째 고리가 가장 많이 삽질한 부분이다. Gitea Actions의 act_runner가 Docker-in-Docker 사이드카로 동작하는데, 이 환경에서는 actions/checkout 같은 표준 액션이 안 된다. git clone으로 직접 소스를 받아야 했고, Docker CLI 버전도 DinD 사이드카의 API 버전에 맞춰야 했다. 7개 커밋에 걸쳐 하나하나 해결했다.
Cloudflare Tunnel 연결이 갑자기 exec format error로 죽었다. cloudflared의 latest 태그가 ARM 이미지를 가져온 것이다. 2025.2.1-amd64로 아키텍처를 명시해서 해결했는데, latest 태그의 위험성을 다시 한번 체감했다.
11개 프로젝트를 거치면서 몇 가지 패턴이 반복됐다.
각 서비스는 자기 일만 하게 만든다. act_runner는 이미지를 빌드하고, ArgoCD는 매니페스트를 동기화하고, ntfy는 알림을 보낸다. 서로의 존재를 모르되 결과물(파일 변경, HTTP 요청)로만 연결된다. 이게 도미노 자동화의 핵심이다.
웹훅은 가장 단순하고 강력한 접착제다. HTTP POST 하나면 어떤 서비스든 연결할 수 있다. PayloadCMS의 afterChange 훅, ArgoCD의 notification webhook, n8n의 webhook trigger 모두 같은 원리다.
사람이 개입할 지점을 명확히 정해야 한다. 완전 자동화가 항상 정답은 아니다. 블로그 발행 전 draft 확인, production 배포 전 수동 트리거처럼 Human-in-the-loop을 어디에 넣을지가 설계의 핵심이다.
알림은 적게, 정확하게. 글로벌 구독으로 모든 앱의 알림을 받았을 때 알림 피로가 심각했다. per-app annotation으로 바꾸고 나서야 알림이 의미를 가지게 됐다.
한 가지 아쉬운 점이 있다면 체인 전체의 상태를 한눈에 볼 수 있는 대시보드가 아직 없다. ArgoCD UI, n8n 실행 로그, ntfy 알림 기록을 각각 확인해야 한다. 체인이 길어질수록 이 모니터링 문제가 커질 텐데, 다음 프로젝트에서 해결해야 할 숙제다.