
홈랩에서 GPU를 물고 돌아가는 서비스가 두 개 있다. 하나는 논문 PDF를 통째로 번역해주는 zotero-server이고, 다른 하나는 동영상에서 자막을 뽑아주는 asr-server다. 둘 다 n8n 워크플로우가 프론트엔드 역할을 하고 있어서 원래는 내부 네트워크에서만 쓸 수 있었다.
문제는 이걸 팀원이나 외부 사람한테도 쓸 수 있게 열어줘야 하는 상황이 생겼다는 거다. 별도로 웹 서버를 띄우자니 과하고 n8n webhook URL을 그대로 공유하기엔 너무 길고 못생겼다. translate.museck.com처럼 깔끔한 도메인으로 접근하게 만들고 싶었다.
요구사항은 단순했다.
translate.museck.com에 접속하면 논문 번역 페이지가 뜬다asr.museck.com에 접속하면 동영상 자막 페이지가 뜬다이미 Cloudflare Tunnel로 museck.com을 서비스하고 있으니 서브도메인만 추가하고 Traefik에서 리다이렉트하면 될 것 같았다. 실제로도 그렇게 풀렸는데 가는 길에 몇 가지 함정이 있었다.
Cloudflare Tunnel 설정은 간단하다. 대시보드에서 라우트를 추가하면 된다. translate.museck.com과 asr.museck.com 둘 다 K8s 내부의 Traefik으로 향하게 잡았다. 기존 museck.com 설정과 동일한 패턴이라 크게 어려울 건 없었다.
다만 여기서 첫 번째 삽질이 나왔다. cloudflared 컨테이너 이미지를 latest 태그로 쓰고 있었는데 어느 순간부터 ARM 이미지를 가져오면서 exec format error가 터졌다. AMD64 서버인데 ARM 바이너리를 실행하려니 당연히 안 된다.
이미지 태그를 2025.2.1-amd64로 고정해서 해결했다. latest 태그의 위험성을 다시 한번 느꼈다. 특히 멀티 아키텍처 이미지는 어떤 플랫폼 빌드를 가져올지 Docker 데몬의 판단에 달려 있어서 더 예측이 어렵다.
핵심 아이디어는 이렇다. 사용자가 translate.museck.com의 루트(/)에 접속하면 Traefik이 실제 n8n webhook 경로로 리다이렉트해주는 것이다.
Traefik에는 redirectRegex Middleware가 있다. 정규식으로 URL을 매칭해서 다른 경로로 보내준다.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: translate-redirect
namespace: n8n
spec:
redirectRegex:
regex: "^https?://translate\\.museck\\.com/?$"
replacement: "https://translate.museck.com/webhook/pdf-translate-public"
permanent: false루트 접근만 잡아서 webhook URL로 302 리다이렉트한다. permanent: false로 설정한 건 나중에 경로가 바뀔 수 있어서다. 301을 쓰면 브라우저가 캐싱해버려서 경로 변경 시 골치 아파진다.
리다이렉트까지는 잘 됐는데 webhook 경로 자체를 n8n으로 보내는 IngressRoute에서 문제가 생겼다. 처음에 이렇게 설정했다.
# 이렇게 하면 하위 경로가 404
match: Host(`translate.museck.com`) && Path(`/webhook/pdf-translate-public`)Path는 정확히 그 경로만 매칭한다. 그런데 n8n webhook은 파일 업로드 등의 이유로 하위 경로(/webhook/pdf-translate-public/xxx)도 사용한다. 정확 매칭으로는 이 요청이 전부 404로 떨어졌다.
# PathPrefix로 바꿔야 한다
match: Host(`translate.museck.com`) && PathPrefix(`/webhook/pdf-translate-public`)PathPrefix로 바꾸면 그 경로로 시작하는 모든 요청을 매칭한다. webhook의 하위 경로까지 전부 n8n으로 전달되니 문제가 해결됐다.
ASR 서비스도 동일한 구조로 세팅했다. Middleware 이름과 리다이렉트 경로만 바꾸면 끝이다.
asr.museck.com/ 접근 시 /webhook/video-transcribe-public으로 리다이렉트PathPrefix로 설정패턴이 동일하니 두 번째는 5분도 안 걸렸다. 이게 패턴화의 장점이다.
한 가지 더 손본 게 있다. K8s에서 GPU 서버(WSL)로 트래픽을 보내는 Endpoints 리소스의 IP 주소다. 원래 Tailscale IP를 쓰고 있었는데 LAN IP로 바꿨다. 같은 네트워크 안에서 굳이 Tailscale을 경유할 이유가 없었고 레이턴시도 줄어든다.
이번 작업에서 얻은 교훈 몇 가지.
n8n webhook을 프론트엔드로 활용하면 별도 웹 앱 없이도 API를 사용자 친화적으로 공개할 수 있다. n8n이 이미 UI를 제공하고 파일 업로드와 다운로드도 알아서 처리해주니 추가 개발이 필요 없었다.
Traefik의 redirectRegex Middleware와 PathPrefix IngressRoute 조합은 꽤 범용적인 패턴이다. 도메인 루트에 접속하면 내부 서비스의 특정 경로로 보내는 구조가 필요할 때 두고두고 쓸 수 있다.
그리고 Docker 이미지 태그는 반드시 고정하자. latest는 편하지만 특히 멀티아키텍처 이미지에서는 예상치 못한 플랫폼 바이너리를 가져올 수 있다. CI/CD에서는 SHA나 버전 태그를 쓰는 게 철칙인데 인프라 컴포넌트도 마찬가지다.
Cloudflare Tunnel + Traefik redirect + n8n webhook. 이 조합이면 내부 서비스를 몇 분 만에 깔끔한 도메인으로 공개할 수 있다. 별도 프론트엔드가 필요 없다는 게 핵심이다.