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

무색

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

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

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

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

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
WSL에서 K8s까지: 10개 서비스가 공유하는 GPU 브릿지 패턴 — sumi-e 스타일 키 비주얼
크로스커팅
2026. 1. 28.

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

kuberneteswsl2homelabgpunetworking

GPU는 WSL에, 서비스는 K8s에

홈랩 K8s 클러스터를 운영하면서 가장 골치 아팠던 문제가 하나 있다. GPU다. Proxmox VM 위에 K8s 노드를 올렸는데 GPU 패스스루가 안 된다. 정확히 말하면 Hyper-V 위의 Proxmox라서 중첩 가상화 환경이고, 이 구조에서 GPU를 VM에 넘기는 건 현실적으로 불가능하다.

그런데 GPU가 필요한 서비스는 계속 늘어났다. 음성인식(ASR), 이미지 생성(ComfyUI), LLM 추론(vLLM), 논문 번역(TranslateGemma), GPU 상태 모니터링까지. RTX 5090 하나를 이 서비스 전부가 시분할로 나눠 써야 한다. GPU는 WSL에서만 접근 가능하고, 나머지 인프라(n8n 워크플로, Traefik 라우팅, Cloudflare Tunnel)는 전부 K8s에 있다.

이 간극을 메우려고 만든 게 "WSL-K8s 브릿지" 패턴이다. 2주 동안 10개 프로젝트에서 반복 적용하면서 자연스럽게 정착된 구성인데, 나름 범용적이라 정리해둔다.

문제: WSL2의 네트워크 격리

WSL2는 Hyper-V 경량 VM으로 돌아가서 자체 네트워크 인터페이스를 갖는다. 문제는 이 IP가 호스트 전용이라는 점이다. K8s 노드(Proxmox VM)에서 WSL 내부 IP로 직접 접근할 수 없다. WSL 안에서 FastAPI 서버를 띄워봤자 같은 LAN에 있는 다른 장비에서는 접근이 안 된다.

그래서 중간 다리가 필요하다. Windows가 그 역할을 한다.

3단 구조: WSL → Windows → K8s

패턴은 세 단계로 나뉜다.

1단계: Windows portproxy

WSL 내부에서 돌아가는 서비스(예: GPU API, 포트 9090)를 Windows LAN IP로 노출한다.

netsh interface portproxy add v4tov4 \
  listenport=9090 listenaddress=0.0.0.0 \
  connectport=9090 connectaddress=localhost

이 한 줄이면 LAN의 어디에서든 192.168.31.2:9090으로 WSL 서비스에 접근할 수 있다. 방화벽 인바운드 규칙도 추가해야 한다는 걸 잊으면 한참 삽질하게 된다.

2단계: K8s 수동 Endpoints

K8s에서 외부 서비스를 내부 서비스처럼 쓰려면 Pod selector가 없는 Service와 직접 IP를 지정한 Endpoints를 만들면 된다.

apiVersion: v1
kind: Service
metadata:
  name: gpu-api
  namespace: gpu-api
spec:
  ports:
    - port: 9090
      targetPort: 9090
---
apiVersion: v1
kind: Endpoints
metadata:
  name: gpu-api  # Service와 이름 일치 필수
  namespace: gpu-api
subsets:
  - addresses:
      - ip: 192.168.31.2  # Windows Host IP
    ports:
      - port: 9090

핵심은 Service에 selector가 없다는 것이다. selector가 없으면 K8s가 자동으로 Endpoints를 만들지 않아서, 같은 이름의 Endpoints를 수동으로 생성해야 한다. 여기에 Windows LAN IP를 넣으면 K8s 내부 DNS(gpu-api.gpu-api.svc.cluster.local)로 WSL 서비스에 접근 가능해진다.

3단계: Traefik IngressRoute로 HTTPS 노출

외부 접근이 필요한 서비스는 Traefik IngressRoute를 추가하면 된다.

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 + Endpoints + IngressRoute)가 하나의 세트다. 서비스를 추가할 때마다 이 매니페스트를 복사해서 이름과 포트만 바꾸면 된다.

이 패턴을 쓰는 10개 서비스

2주간 진행한 프로젝트에서 이 패턴을 적용한 서비스 목록이다.

  • GPU 상태 API (gpu.xssh.org, 포트 9090) — FastAPI로 nvidia-smi를 감싸서 GPU 사용률/온도/프로세스를 HTTP API로 제공한다. macOS 위젯, ComfyUI 컨트롤러, 번역 서버 모두 이 API의 is_idle 플래그를 보고 GPU 점유 여부를 판단한다.
  • ComfyUI (comfyui.xssh.org, 포트 8188) — 노드 기반 이미지 생성 UI. 온디맨드로 시작/종료하며 GPU를 점유한다.
  • ASR Server (K8s 내부, 포트 8891) — 화자분리 + 음성인식 + 텍스트 교정 파이프라인. n8n과 웹 UI에서 K8s DNS로 호출한다.
  • vLLM (K8s 내부, 포트 8000) — LLM 추론 서버. 논문 번역용 TranslateGemma-27B 모델을 서빙한다.
  • Zotero Server (K8s 내부, 포트 8890) — PDF 논문 번역 API. n8n 워크플로에서 호출한다.
  • JupyterLab (lab.xssh.org, 포트 8888) — GPU 개발 환경. ComfyUI 원격 제어에도 활용한다.
  • Pulse 대시보드 (pulse.xssh.org, 포트 7655) — K8s/Proxmox 상태 모니터링. WSL Docker에서 실행된다.
  • OpenClaw (openclaw.xssh.org, 포트 18789) — AI 에이전트 게이트웨이. WSL Docker에서 실행한다.
  • museck.com — 이 블로그 자체도 K8s에서 돌아가며, 빌드 과정에서 동일한 인프라 패턴(Deployment + Service + IngressRoute)을 사용한다.
  • n8n 워크플로 — K8s에서 실행되면서 위의 WSL 서비스(ASR, Zotero 등)를 K8s 내부 DNS로 호출하는 소비자 역할이다.

HTTPS가 필요한 건 IngressRoute를 추가하고 그렇지 않은 건 K8s 내부 DNS로만 쓴다. 똑같은 매니페스트 구조에서 IngressRoute 유무만 다르다.

왜 ExternalName 대신 수동 Endpoints인가

K8s에는 외부 서비스를 등록하는 공식 방법으로 ExternalName Service가 있다. 하지만 이건 DNS CNAME을 반환하는 거라서 IP 주소를 직접 지정할 수 없다. 홈랩 LAN IP(192.168.31.2)에 도메인이 없으니 ExternalName은 못 쓴다.

수동 Endpoints는 IP를 직접 넣으니 DNS 의존성이 없다. 장비가 고정 IP라면 이쪽이 더 간단하고 확실하다.

삽질 모음: 같은 실수를 세 번 했다

Tailscale IP vs LAN IP

처음 ASR 서버를 연결할 때 Endpoints IP를 Tailscale 주소(100.x.x.x)로 넣었다. 내 노트북에서는 Tailscale이 있어서 잘 되는데 K8s 노드에는 Tailscale이 없다. 당연히 접근 불가. GPU 서버에서도 같은 실수를 반복해서 fix 커밋만 두 개 남겼다. K8s 노드가 접근할 수 있는 네트워크가 뭔지 먼저 확인하자.

Windows 방화벽

portproxy를 설정해도 방화벽 인바운드 규칙이 없으면 외부에서 접근이 안 된다. OpenClaw을 배포할 때 포트 18789에 대한 방화벽 규칙을 빼먹어서 30분을 날렸다. portproxy 추가할 때 방화벽도 같이 추가하는 습관을 들이자.

Service 이름과 Endpoints 이름 불일치

Service와 Endpoints의 metadata.name이 정확히 같아야 한다. namespace도 같아야 한다. 오타 하나로 연결이 안 되는데 에러 메시지가 명확하지 않아서 디버깅이 귀찮다. kubectl get endpoints -n <ns>로 Endpoints가 제대로 연결됐는지 확인하는 게 첫 번째 디버깅 단계다.

WSL systemd PATH 문제

GPU API를 systemd user service로 등록했는데 nvidia-smi를 못 찾았다. 일반 쉘에서는 /usr/lib/wsl/lib/가 PATH에 있지만 systemd는 다른 PATH를 쓴다. feat 커밋 직후 1분 만에 fix 커밋이 나온 이유다. 환경변수로 절대 경로를 명시해서 해결했다.

네트워크 디버깅 체크리스트

이 패턴에서 "연결이 안 돼요"가 나오면 아래 순서로 확인한다.

  1. WSL 내부에서 서비스가 살아있는지: curl localhost:9090/health
  2. Windows에서 portproxy가 동작하는지: netsh interface portproxy show all
  3. 방화벽 인바운드 규칙 확인: 해당 포트가 열려 있는지 Windows 방화벽에서 확인
  4. K8s 노드에서 Windows IP로 접근 가능한지: kubectl run tmp --rm -it --image=curlimages/curl -- curl 192.168.31.2:9090
  5. Endpoints가 올바른 IP를 가리키는지: kubectl get endpoints -n <namespace>
  6. K8s DNS가 Service를 찾는지: kubectl run tmp --rm -it --image=busybox -- nslookup gpu-api.gpu-api.svc.cluster.local

대부분 2번(portproxy 누락)이나 3번(방화벽)에서 걸린다. K8s 쪽 문제인 경우는 거의 없었다.

패턴의 한계와 개선 방향

이 패턴이 만능은 아니다. 단점도 분명하다.

매니페스트 복사-붙여넣기가 심하다. 5개 서비스에 동일한 구조를 반복 적용하면서 YAML이 거의 복사 수준이 됐다. Helm 차트나 Kustomize 컴포넌트로 추상화하면 관리가 훨씬 편해질 텐데 아직 손을 못 댔다.

IP가 바뀌면 Endpoints를 수동으로 고쳐야 한다. Windows IP가 DHCP로 바뀌면 모든 Endpoints를 업데이트해야 한다. 지금은 공유기에서 고정 IP를 할당해놓아서 괜찮지만 구조적으로 취약한 부분이다.

헬스체크가 없다. 수동 Endpoints는 K8s가 헬스체크를 하지 않는다. WSL 서비스가 죽어도 Endpoints IP는 그대로 남아있어서 요청이 계속 들어간다. 서비스별로 헬스체크 로직을 직접 구현하거나 외부 모니터링을 붙여야 한다.

정리

모든 것을 K8s 안에 넣으려 하기보다는 외부 서비스를 K8s 서비스 디스커버리에 등록하는 방식이 하이브리드 환경에서 더 현실적이다. GPU처럼 컨테이너화가 어려운 리소스가 있을 때 특히 그렇다.

이 패턴은 홈랩이 아니더라도 적용할 수 있다. 온프레미스 GPU 서버를 K8s 클러스터에 연동하거나 레거시 서비스를 K8s 네트워크에 편입시킬 때 동일한 접근이 통한다. 핵심은 간단하다. selector 없는 Service + 수동 Endpoints. 나머지는 평소 K8s 쓰던 대로 하면 된다.

자주 묻는 질문

WSL2의 GPU 서비스를 K8s에서 접근하려면 어떻게 하나요?
Windows portproxy로 WSL 포트를 LAN IP에 매핑하고, K8s에서 selector 없는 Service와 수동 Endpoints로 해당 IP를 클러스터 내부에 노출합니다.
K8s 수동 Endpoints와 ExternalName Service의 차이는?
ExternalName은 CNAME DNS 리디렉션으로 포트 지정이 불가능하고, 수동 Endpoints는 IP와 포트를 직접 지정해 포트가 다른 여러 서비스를 유연하게 연결할 수 있습니다.
WSL2 IP가 재부팅마다 바뀌는 문제는 어떻게 해결하나요?
Windows portproxy가 고정된 LAN IP에서 WSL IP로 포워딩하므로, K8s Endpoints는 Windows LAN IP만 참조합니다. WSL IP가 바뀌면 portproxy 규칙만 업데이트하면 됩니다.
크로스커팅(14/18)
Prev

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

Next

9개 프로젝트에서 발견한 Config as Code의 공통 원칙