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

무색

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

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

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

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

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
모든 것을 API로: 홈랩 10개 프로젝트가 가르쳐준 것 — sumi-e 스타일 키 비주얼
크로스커팅
2026. 1. 27.

모든 것을 API로: 홈랩 10개 프로젝트가 가르쳐준 것

api-firstfastapikuberneteshomelabgpu

GPU가 K8s 밖에 있으면 생기는 일

홈랩을 운영하면서 가장 먼저 부딪힌 문제가 GPU 접근이었다. K8s 클러스터의 VM에는 GPU 패스스루가 안 되니까, GPU가 필요한 서비스는 전부 WSL에서 직접 돌려야 했다. 음성 인식(ASR), 이미지 생성(ComfyUI), LLM 추론(vLLM), 논문 번역(TranslateGemma)... 하나둘 늘어나는 GPU 서비스를 어떻게 K8s 네트워크에 통합할 것인가?

답은 의외로 단순했다. 모든 걸 HTTP API로 만들면 된다.

2주 동안 10개 프로젝트를 진행하면서 내린 결론은 이거다. 내부 서비스든 외부 서비스든, 일단 HTTP API로 감싸 놓으면 나머지는 자연스럽게 풀린다. K8s 라우팅도, 외부 공개도, 자동화 연동도 전부.

5개 GPU 서비스, 1개의 패턴

WSL에서 실행하는 GPU 서비스는 총 5개다.

  • GPU API (:9090) — GPU 사용률, 온도, 프로세스 목록을 제공하는 FastAPI 서버. macOS 위젯에서 실시간 모니터링용
  • ASR Server (:8891) — 화자분리 + 음성인식 + 텍스트교정 3개 모델이 VRAM에 상주하는 자막 생성 서버
  • Zotero Server (:8890) — Zotero 데스크톱에서 원클릭 논문 번역을 지원하는 FastAPI 비동기 서버
  • ComfyUI (:8188) — 노드 기반 이미지 생성 환경. z_image_turbo 모델로 장당 4초 생성
  • vLLM (:8000) — TranslateGemma 2B 모델을 서빙하는 LLM 추론 서버

이 서비스를 K8s에 통합하는 패턴은 놀라울 정도로 동일하다. Service(selector 없음) + Endpoints(수동 IP) + IngressRoute, 이 세 개를 한 세트로 찍어낸다.

WSL의 네트워크는 K8s 노드에서 직접 접근이 안 된다. 그래서 Windows portproxy를 브릿지로 쓴다. WSL 포트를 Windows LAN IP(192.168.31.2)로 포워딩하고, K8s Endpoints에 그 IP를 지정하면 끝이다.

# 모든 서비스에 반복되는 패턴 (gpu-api 예시)
apiVersion: v1
kind: Service
metadata:
  name: gpu-api
  namespace: gpu-api
spec:
  ports:
    - port: 9090
---
apiVersion: v1
kind: Endpoints
metadata:
  name: gpu-api
  namespace: gpu-api
subsets:
  - addresses:
      - ip: 192.168.31.2  # Windows Host
    ports:
      - port: 9090

5개 서비스에 이 패턴을 복사-붙여넣기 수준으로 반복 적용했다. 네임스페이스 이름과 포트 번호만 바꾸면 되니까. Helm이나 Kustomize로 추상화할 수도 있겠지만, 솔직히 이 정도면 YAML 복붙이 더 빠르다.

FastAPI가 반복되는 이유

10개 프로젝트를 돌아보면 FastAPI를 고른 건 우연이 아니었다. GPU 서비스 특성상 비동기 처리가 필수인데, FastAPI가 딱 맞았다.

공통으로 나타나는 패턴은 비동기 태스크 큐다. 요청을 받으면 task_id를 즉시 반환하고, 클라이언트가 상태 엔드포인트를 폴링하는 구조. 논문 번역은 수십 분 걸리고, 자막 생성도 긴 영상이면 5분 넘게 걸리니까 동기 응답은 불가능하다.

# 논문 번역 서버 (zotero_server.py)
@app.post("/translate")
async def translate(file: UploadFile):
    task_id = uuid.uuid4().hex[:12]
    asyncio.create_task(_run_full_pipeline(task_id, file))
    return {"taskId": task_id, "status": "processing"}

# 자막 생성 서버 (worker_server.py)
@app.post("/transcribe/upload")
async def transcribe_upload(file: UploadFile):
    task_id = uuid.uuid4().hex[:12]
    asyncio.create_task(_run_transcription_pipeline(task))
    return {"taskId": task_id, "status": "processing"}

두 서버 코드가 거의 같다. task_id 발급 → 백그라운드 실행 → 상태 폴링 → 결과 다운로드. 이 패턴이 GPU 서비스의 정석이 된 건, GPU라는 공유 자원의 특성 때문이다. 동시에 두 작업을 돌릴 수 없으니 asyncio.Lock으로 직렬화하고, 하나씩 처리한다.

GPU 상태 관리는 한 단계 더 갔다. vLLM 관리에서는 nvidia-smi로 메모리만 확인하던 방식에서 GPU API로 프로세스 레벨 상태를 확인하는 방식으로 진화했다. IDLE, VLLM_LOADING, BUSY, UNKNOWN 네 가지 상태를 두고 각각 다른 액션을 취한다. nvidia-smi만으로는 vLLM이 로딩 중인지 다른 프로세스가 GPU를 쓰는 건지 구분할 수 없었기 때문이다.

내부 API를 외부로 꺼내는 3계층

API를 만들었으면 어디서든 접근할 수 있어야 의미가 있다. 내부 API를 외부에 노출하기까지 3개 계층을 거친다.

1계층: WSL 서비스 → K8s 네트워크. Windows portproxy로 WSL 포트를 LAN IP에 매핑하고, K8s Endpoints에 그 IP를 등록한다. 이러면 K8s 내부 Pod에서 gpu-api.gpu-api.svc.cluster.local 같은 클러스터 DNS로 WSL 서비스에 접근할 수 있다.

2계층: K8s 서비스 → 내부 HTTPS. Traefik IngressRoute로 도메인을 매핑한다. gpu.xssh.org, comfyui.xssh.org 같은 내부 도메인을 붙여서 브라우저에서도 접근할 수 있게 한다.

3계층: 내부 서비스 → 외부 공개. Cloudflare Tunnel이 외부 트래픽을 K8s 내부 Traefik으로 전달한다. translate.museck.com이나 asr.museck.com 같은 공개 도메인을 통해 누구나 접근할 수 있다. Traefik Middleware로 루트 경로를 n8n webhook URL로 리다이렉트하면 별도 프론트엔드 없이 서비스를 공개할 수 있다.

# Cloudflare Tunnel → Traefik → n8n webhook 리다이렉트
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: translate-redirect
spec:
  redirectRegex:
    regex: "^https?://translate\\.museck\\.com/?$"
    replacement: "https://translate.museck.com/webhook/pdf-translate-public"

API화가 만든 자동화 체인

서비스를 API로 만들어 놓으면 예상치 못한 곳에서 가치가 생긴다. 가장 큰 수확은 자동화 연동이었다.

n8n 워크플로우에서 GPU 서비스를 HTTP 노드 하나로 호출할 수 있게 되면서 복잡한 파이프라인이 가능해졌다. PayloadCMS에서 블로그가 발행되면 afterChange 훅이 n8n을 트리거하고, n8n은 Claude API로 소셜 미디어용 요약을 생성한 뒤 Postiz API로 4개 플랫폼에 동시 배포한다. 이 전체 체인이 HTTP API 호출의 연쇄다.

Zotero에서 논문 PDF를 선택하고 번역을 누르면, Zotero 플러그인이 FastAPI 서버에 POST 요청을 보낸다. 서버가 Claude Code를 headless 모드로 호출해서 번역 파이프라인을 돌리고, 완료되면 PDF를 돌려준다. Zotero 플러그인은 서버가 뭘로 만들어졌는지 모른다. HTTP 규약만 맞추면 되니까.

ComfyUI 제어도 같은 맥락이다. comfyui_ctl.py가 GPU API의 /status를 먼저 확인해서 GPU가 유휴 상태인지 체크한다. 유휴면 JupyterLab WebSocket으로 ComfyUI를 띄우고, health check를 폴링하다가 준비가 되면 이미지 생성 워크플로우를 실행한다. 이 전체가 API 호출 체인이다.

삽질에서 배운 것

Tailscale IP vs LAN IP. K8s Endpoints에 Tailscale IP(100.x.x.x)를 넣었다가 접근이 안 됐다. K8s 노드에는 Tailscale이 없으니 당연한 건데 두 번이나 같은 실수를 했다. K8s Endpoints에는 반드시 노드가 접근 가능한 LAN IP를 써야 한다.

n8n으로 대용량 파일 프록시 금지. 자막 서비스를 처음 만들 때 n8n 워크플로우가 파일 업로드를 프록시하게 했다. 수백MB 동영상이 n8n의 1GB 메모리를 초과해서 OOM이 터졌다. 같은 날 15개 노드짜리 워크플로우를 2개 노드로 줄였다. n8n은 HTML 페이지만 서빙하고, 브라우저에서 GPU 서버로 직접 통신하는 구조가 정답이었다.

nvidia-smi만으로는 부족하다. GPU 메모리 사용량만 보고 유휴 여부를 판단하면 vLLM이 로딩 중일 때 유휴로 오판한다. 프로세스 목록까지 확인해야 정확한 상태 판단이 가능하다. 그래서 별도 GPU API를 만들었고, is_idle 플래그와 processes 리스트를 함께 제공하게 했다.

cloudflared 이미지 태그는 아키텍처를 명시해라. latest 태그가 ARM 이미지를 당겨와서 exec format error가 났다. 2025.2.1-amd64처럼 아키텍처를 명시하면 해결된다.

정리

2주 동안 만든 서비스를 되돌아보면 하나의 원칙으로 수렴한다. 뭔가를 만들면 HTTP API로 감싸라.

GPU 추론 서버든, 이미지 생성기든, 논문 번역기든 HTTP API로 감싸 놓으면 K8s에 통합할 수 있고, n8n 워크플로우에서 호출할 수 있고, Cloudflare Tunnel로 외부에 공개할 수 있다. 클라이언트가 Zotero 플러그인이든, 브라우저든, Claude 에이전트든 상관없다. HTTP 규약만 맞으면 된다.

모든 걸 K8s 안에 넣으려고 애쓸 필요도 없다. 외부 서비스를 K8s 서비스 디스커버리에 등록만 하면 하이브리드 환경에서 충분히 유연하게 돌아간다. 오히려 GPU처럼 컨테이너화가 어려운 워크로드일수록 이 패턴이 빛난다.

자주 묻는 질문

K8s 외부 서비스를 클러스터 내에서 접근하려면 어떻게 하나요?
selector 없는 Service와 수동 IP를 지정한 Endpoints를 만들면 됩니다. WSL이나 외부 서버의 포트를 K8s 서비스 디스커버리에 등록하여 클러스터 DNS로 접근할 수 있습니다.
홈랩 GPU 서비스를 외부에 공개하는 방법은?
3계층 구조를 사용합니다. WSL 포트를 Windows portproxy로 LAN에 매핑하고, K8s Endpoints로 클러스터에 통합한 뒤, Cloudflare Tunnel로 외부 도메인을 연결합니다.
FastAPI로 GPU 서비스를 만들 때 비동기 처리는 왜 필요한가요?
GPU 추론은 수 초에서 수십 분까지 걸리므로 동기 응답이 불가능합니다. task_id를 즉시 반환하고 상태 폴링 엔드포인트를 제공하는 비동기 태스크 큐 패턴이 표준입니다.
크로스커팅(13/18)
Prev

Claude Code 서브에이전트 패턴 총정리: 8가지 실전 사례

Next

WSL에서 K8s까지: 10개 서비스가 공유하는 GPU 브릿지 패턴