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

무색

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

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

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

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

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
WSL GPU 서비스를 K8s에 연결하는 수동 Endpoints 패턴
홈랩 삽질기
2026. 1. 3.

WSL GPU 서비스를 K8s에 연결하는 수동 Endpoints 패턴

kuberneteswslgpuhomelabnetworking

GPU는 WSL에, 서비스는 K8s에

홈랩 K8s 클러스터를 운영하면서 한 가지 골치 아픈 문제가 있었다. GPU 워크로드를 돌릴 수가 없다는 거다. K8s 노드가 VM으로 돌아가고 있어서 GPU 패스스루가 안 되기 때문이다. RTX 5090은 Windows 호스트의 WSL에서만 접근 가능하다.

그런데 문제는 이 GPU 서비스를 K8s 안의 다른 서비스에서 갖다 써야 한다는 점이다. n8n 워크플로우에서 ASR(음성인식) API를 호출하고 싶고, 블로그 자동화에서 ComfyUI 이미지 생성을 쓰고 싶은데 전부 WSL에서 돌아가고 있으니까.

결론부터 말하면 "수동 Endpoints" 패턴으로 해결했다. 같은 패턴을 5개 서비스에 반복 적용했고 꽤 깔끔하게 동작한다.

전체 아키텍처

먼저 전체 그림을 보자. 요청 흐름은 아래처럼 3단계를 거친다.

  1. K8s Pod이나 Traefik이 Service DNS로 요청 (예: gpu-api.gpu-api.svc.cluster.local:9090)
  2. K8s Endpoints가 Windows 호스트 LAN IP(192.168.31.2)로 트래픽 전달
  3. Windows portproxy 규칙이 해당 포트를 WSL 내부 IP로 포워딩

이 구조에서 K8s 입장에서 보면 gpu-api, comfyui, zotero-server 같은 서비스가 마치 클러스터 안에 있는 것처럼 보인다. 실제로는 WSL에서 돌아가고 있지만 K8s 서비스 디스커버리에 등록되어 있으니 클러스터 DNS로 접근이 된다.

수동 Endpoints 패턴이란

보통 K8s Service를 만들면 selector로 Pod을 지정하고 Endpoints가 자동 생성된다. 그런데 selector를 아예 안 넣으면? Service는 생기지만 Endpoints가 비어 있다. 여기에 수동으로 Endpoints 리소스를 만들어서 외부 IP를 지정해주면 K8s 네트워크에서 그 IP로 트래픽이 흘러간다.

공식 문서에도 나오는 패턴이긴 한데 실제로 써본 사람은 많지 않은 것 같다. 나도 이번에 처음 적용해봤다.

한 서비스의 매니페스트는 이렇게 생겼다.

# Service (selector 없음)
apiVersion: v1
kind: Service
metadata:
  name: gpu-api
  namespace: gpu-api
spec:
  ports:
    - port: 9090
      targetPort: 9090
---
# Endpoints (수동 IP 지정)
apiVersion: v1
kind: Endpoints
metadata:
  name: gpu-api
  namespace: gpu-api
subsets:
  - addresses:
      - ip: 192.168.31.2  # Windows Host LAN IP
    ports:
      - port: 9090

핵심은 Service에 selector가 없다는 것이다. selector가 없으면 K8s가 Endpoints를 자동 생성하지 않으므로 우리가 직접 만든 Endpoints가 쓰인다. Endpoints의 addresses.ip에 Windows 호스트의 LAN IP를 넣어주면 끝이다.

여기에 Traefik IngressRoute까지 붙이면 외부에서 HTTPS로도 접근할 수 있다.

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: gpu-api
  namespace: gpu-api
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`gpu.xssh.org`)
      kind: Rule
      services:
        - name: gpu-api
          port: 9090
  tls: {}

Namespace + Service(selector 없음) + Endpoints(수동 IP) + IngressRoute. 이 네 가지가 한 세트다. 이걸 서비스마다 복사해서 포트 번호랑 이름만 바꿔주면 된다.

Windows portproxy가 필요한 이유

WSL의 네트워크 인터페이스는 NAT 뒤에 있어서 외부에서 직접 접근이 안 된다. K8s 노드에서 WSL 내부 IP(보통 172.x.x.x)로 직접 패킷을 보낼 수 없다는 뜻이다.

그래서 Windows의 netsh interface portproxy 기능을 중간 다리로 쓴다. Windows LAN IP(192.168.31.2)의 특정 포트로 들어오는 트래픽을 WSL 내부 IP로 포워딩해주는 규칙을 추가하면 된다.

# Windows PowerShell (관리자)
netsh interface portproxy add v4tov4 \
  listenport=9090 listenaddress=0.0.0.0 \
  connectport=9090 connectaddress=172.28.x.x

이렇게 하면 K8s Endpoints에서 192.168.31.2:9090으로 보낸 트래픽이 Windows portproxy를 거쳐 WSL 내부의 서비스까지 도달한다.

5개 서비스에 적용한 결과

이 패턴을 다음 서비스에 적용했다.

  • GPU 상태 API (gpu.xssh.org, 포트 9090) - FastAPI로 만든 GPU 사용률/온도/프로세스 모니터링. macOS 위젯에서 호출한다.
  • ComfyUI (comfyui.xssh.org, 포트 8188) - Stable Diffusion 이미지 생성. z_image_turbo 모델 사용.
  • Zotero Server (내부 전용, 포트 8890) - 논문 번역 API. Claude와 TranslateGemma를 조합해서 학술 논문을 번역한다. n8n 워크플로우에서 호출.
  • ASR Server (내부 전용, 포트 8891) - 화자분리 + 음성인식 + 정렬 파이프라인. GPU 모델 3개를 순차 실행한다.
  • vLLM (내부 전용, 포트 8000) - LLM 추론 서버. K8s 내부에서만 접근.

gpu-api와 ComfyUI는 Traefik IngressRoute를 붙여서 외부 HTTPS 접근을 열어뒀고 나머지 셋은 K8s 내부에서만 접근하도록 IngressRoute 없이 구성했다.

5개 서비스에 같은 패턴을 반복하다 보니 매니페스트가 거의 복사-붙여넣기 수준이 되었다. 이름과 포트 번호만 다를 뿐 구조가 동일하다. 나중에 Helm 차트나 Kustomize 컴포넌트로 추상화하면 좋겠다는 생각이 들었다.

Tailscale IP로 삽질한 이야기

처음에 Endpoints IP를 Tailscale IP(100.x.x.x)로 넣었다. 내 맥북에서 Tailscale로 잘 붙으니까 K8s에서도 되겠지 싶었던 거다.

당연히 안 됐다. K8s 노드에는 Tailscale이 설치되어 있지 않으니 100.x.x.x 대역을 라우팅할 방법이 없었다. K8s 노드가 접근할 수 있는 건 LAN 대역(192.168.31.x)뿐이다.

웃긴 건 이 실수를 GPU API에서 한 번 하고 ASR 서버에서 또 한 번 했다는 거다. fix 커밋을 두 번이나 남기고 나서야 "Endpoints IP는 반드시 K8s 노드가 접근 가능한 IP여야 한다"는 걸 몸으로 외웠다.

이 패턴이 유용한 상황

수동 Endpoints 패턴은 GPU 워크로드 말고도 다양한 상황에서 쓸 수 있다.

  • 레거시 시스템이 K8s 밖에서 돌아가지만 클러스터 안에서 접근해야 할 때
  • 하드웨어에 바인딩된 서비스(GPU, 특수 하드웨어)를 컨테이너화하기 어려울 때
  • 외부 SaaS나 온프레미스 DB를 K8s 서비스 디스커버리에 등록하고 싶을 때
  • 마이그레이션 과도기에 일부 서비스만 K8s 밖에 남겨둬야 할 때

모든 걸 K8s 안에 넣으려고 애쓰기보다 외부 서비스를 K8s 서비스 디스커버리에 등록하는 방식이 하이브리드 환경에서는 훨씬 현실적이다. Pod 안에서 curl gpu-api.gpu-api.svc.cluster.local:9090 치면 WSL의 FastAPI 서버가 응답하는 게 꽤 만족스럽다.

정리하면

K8s 외부에서 돌아가는 서비스를 클러스터 네트워크에 통합하는 건 생각보다 간단했다. selector 없는 Service + 수동 Endpoints + (필요하면) IngressRoute. 이 조합이면 거의 모든 외부 서비스를 K8s DNS에 등록할 수 있다.

WSL + Windows portproxy 조합은 GPU가 있는 Windows 머신에서만 쓰는 특수한 상황이지만 수동 Endpoints 패턴 자체는 범용적이다. K8s를 운영하면서 "이건 컨테이너에 못 넣겠는데" 하는 서비스가 있다면 한번 시도해보길 추천한다.

다만 매니페스트가 서비스 수만큼 복제되는 건 좀 거슬린다. 다음에는 Kustomize 컴포넌트로 템플릿화해서 이름과 포트만 파라미터로 바꿀 수 있게 만들어볼 생각이다.

자주 묻는 질문

K8s에서 외부 서비스를 클러스터 DNS에 등록하는 방법은?
selector 없는 Service와 수동 Endpoints 리소스를 조합하면 됩니다. Endpoints의 addresses.ip에 외부 서비스 IP를 지정하면 K8s DNS로 해당 서비스에 접근할 수 있습니다.
WSL의 GPU 서비스를 K8s Pod에서 호출할 수 있나요?
네. 수동 Endpoints에 Windows LAN IP를 넣고 Windows portproxy로 WSL 내부로 포워딩하면 K8s Pod에서 서비스 DNS로 WSL GPU 서비스를 호출할 수 있습니다.
수동 Endpoints에 Tailscale IP를 넣으면 안 되는 이유는?
K8s 노드에 Tailscale이 설치되어 있지 않으면 100.x.x.x 대역을 라우팅할 수 없습니다. Endpoints IP는 반드시 K8s 노드가 직접 접근 가능한 LAN IP여야 합니다.
홈랩 삽질기(9/19)
Prev

n8n 워크플로우 자동화, K8s 홈랩에 셀프 호스팅하기

Next

내부 AI 서비스를 Cloudflare Tunnel로 외부에 공개하기