목표와 현재 결과

vLLM Deployment 앞에 ClusterIP Service를 두고 port-forward 없이 클러스터 내부에서 호출되는지 확인했다. Service vllm-qwen이 ClusterIP 10.43.163.137로 생성됐고, EndpointSlice vllm-qwen-xfzqz에 vLLM Pod IP 10.42.0.21이 연결된 것을 확인했다.

임시 Pod에서 내부 DNS 이름 vllm-qwen.llm.svc.cluster.local/v1/models와 Chat Completions를 호출해 모두 정상 응답을 받았다. /v1/modelsrootQwen/Qwen2.5-3B-Instruct-AWQ로 표시되어 3B AWQ checkpoint가 실제로 서빙 중임이 함께 확인됐다. 이로써 이전 단계에서 1/1 Ready/health 200까지만 확인했던 3B AWQ의 API 동작 검증이 완료됐다.

외부 접근 방식은 이 단계에서 결정하지 않았다.

1. Service 정의

selector는 Deployment가 붙이는 Pod label과 정확히 일치해야 한다. 이 cluster의 Pod는 app=vllmversion=ver-0을 가진다.

apiVersion: v1
kind: Service
metadata:
  name: vllm-qwen
  namespace: llm
  labels:
    app: vllm
    version: ver-0
spec:
  type: ClusterIP
  selector:
    app: vllm
    version: ver-0
  ports:
    - name: http
      port: 8000
      targetPort: 8000
      protocol: TCP
설정목적
typeClusterIP외부 노출 없이 내부 endpoint만 확보
selectorapp=vllm, version=ver-0실제 Pod label과 일치
ports[].namehttpServiceMonitor가 port를 이름으로 참조
targetPort8000container port에 이름이 없어 숫자로 지정

port 이름을 http로 둔 것은 다음 단계의 ServiceMonitor가 port를 숫자가 아니라 이름으로 참조하기 때문이다. 이 이름이 어긋나면 Service가 정상이어도 Prometheus target이 등록되지 않는다.

2. Service와 EndpointSlice 확인

Service는 selector가 어긋나도 정상적으로 생성되고 오류를 내지 않는다. 실제로 Pod가 연결됐는지는 EndpointSlice로 확인해야 한다.

kubectl -n llm get svc vllm-qwen -o wide
kubectl -n llm get endpointslices -l kubernetes.io/service-name=vllm-qwen
key해석
CLUSTER-IP10.43.163.137고정된 내부 주소 확보
EXTERNAL-IP<none>외부 노출 없음
SELECTORapp=vllm,version=ver-0대상 Pod 선택 조건
EndpointSlicevllm-qwen-xfzqz자동 생성된 endpoint 목록
ENDPOINTS10.42.0.21vLLM Pod IP가 연결됨

ENDPOINTS에 주소가 채워졌다는 것이 selector와 Pod label이 일치한다는 직접 근거다. 비어 있었다면 Service는 존재하지만 트래픽이 어디로도 가지 않는 상태이며, 이때 원인을 vLLM에서 찾기 시작하면 한참 돌아가게 된다.

kubectl get endpoints는 지원 중단 경고를 낸다

core/v1 Endpoints는 Kubernetes 1.33부터 deprecated이고 discovery.k8s.io/v1 EndpointSlice가 표준이다. 예전 명령을 써도 값은 정상이며 하위 호환을 위해 Endpoints 객체도 계속 생성되지만 아래 경고가 붙는다.

Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice

3. selector와 Pod label 대조

kubectl -n llm get svc vllm-qwen -o jsonpath='{.spec.selector}{"\n"}'
kubectl -n llm get pod --show-labels
항목해석
Service selectorapp=vllm, version=ver-0두 label을 모두 요구
Pod labelapp=vllm, version=ver-0, pod-template-hash=865f7b887bselector 조건을 포함
Pod 상태1/1 Running, restart 0정상

Pod에는 ReplicaSet이 붙이는 pod-template-hash가 추가로 존재하지만, selector는 지정한 label을 모두 포함하는지만 보므로 매칭에 영향을 주지 않는다.

selector에 version이 포함되어 있다

현재 selector는 app=vllm,version=ver-0이라 version 값이 다른 Pod는 이 Service의 대상이 되지 않는다. blue/green이나 수동 canary에서는 이 성질을 이용하지만, 단순 교체를 의도하고 새 version label로 배포하면 Pod가 Ready인데도 트래픽이 가지 않는 상태가 된다.

4. 클러스터 내부 호출 검증

port-forward 없이 임시 Pod에서 내부 DNS 이름으로 호출했다.

kubectl -n llm run curl-test \
  --rm -it --restart=Never \
  --image=curlimages/curl:latest -- \
  curl -fsS http://vllm-qwen.llm.svc.cluster.local:8000/v1/models
key해석
data[0].idqwen2.5-3bAPI 요청에 사용할 model 이름
data[0].rootQwen/Qwen2.5-3B-Instruct-AWQ실제 적재된 checkpoint
data[0].max_model_len4096manifest의 --max-model-len 반영
data[0].owned_byvllmvLLM이 제공하는 model

root가 AWQ checkpoint로 표시된 것이 3B AWQ 모델이 실제로 서빙되고 있다는 확인이다. id--served-model-name으로 지정한 짧은 이름이고, root가 원본 checkpoint를 가리킨다.

kubectl -n llm run curl-test \
  --rm -it --restart=Never \
  --image=curlimages/curl:latest -- \
  curl -sS http://vllm-qwen.llm.svc.cluster.local:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{"model":"qwen2.5-3b","messages":[{"role":"user","content":"짧게 응답해줘."}],"max_tokens":64}'
key해석
choices[0].message.content알겠습니다. 짧게 답변하겠습니다.3B AWQ 모델의 실제 응답
finish_reasonstop토큰 상한이 아니라 정상 종료
usage.prompt_tokens37입력 토큰
usage.completion_tokens10생성 토큰
usage.total_tokens47합계
system_fingerprintvllm-0.26.0-b0b11f7d응답을 생성한 vLLM build

finish_reasonstop이므로 max_tokens=64 상한에 걸린 것이 아니라 모델이 스스로 응답을 마쳤다. 이 호출로 Service → EndpointSlice → Pod → vLLM 경로가 끝까지 이어진 것을 확인했으며, port-forward는 사용하지 않았다.

5. 확인된 범위와 남은 결정

항목상태
ClusterIP 고정 endpoint확보
EndpointSlice Pod 연결확인
내부 DNS /v1/models확인
내부 DNS Chat Completions확인
3B AWQ checkpoint 서빙확인
ServiceMonitor 선행 조건충족

외부 접근은 결정하지 않았다. vLLM의 OpenAI 호환 API는 기본적으로 인증이 없어, 공개 IP에 노출하면 누구나 GPU를 사용할 수 있고 시간당 과금이 그대로 발생한다.

선택지상태비고
SSH 터널미실행방화벽을 열지 않아도 됨
NodePort와 방화벽 소스 IP 제한미실행실험 종료 후 규칙 삭제 필요
Tailscale 서브넷 라우터미실행ClusterIP를 tailnet에서 직접 호출
LoadBalancer검토 대상 아님고정 IP 비용과 인증 계층이 함께 필요

Service 작업에서 실제로 시간이 걸리는 지점은 manifest 작성이 아니라 selector와 Pod label의 일치 확인이었다. EndpointSlice에 주소가 채워졌는지 먼저 보는 것이 가장 빠른 확인 경로다.