본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 25. 16:58

Kubernetes에서 첫 번째 LLM API 구축하기: 모델부터 curl 요청까지

요약

Kubernetes 환경에서 vLLM을 사용하여 LLM API를 구축하고 배포하는 실습 가이드입니다. GPU 노드 설정부터 OpenAI 호환 API 노출 및 curl 요청까지의 전 과정을 다룹니다.

핵심 포인트

  • Kubernetes를 활용한 LLM 모델 배포 및 API 노출 방법
  • vLLM 서빙 엔진의 역할과 Kubernetes의 인프라 관리 역할 구분
  • Qwen 모델을 활용한 GPU 메모리 기반의 단계별 실습 제안
  • OpenAI 호환 API 규격 구현 및 호출 프로세스

시리즈 링크

지금까지 이 시리즈를 통해 멘탈 모델 (mental model), 토큰 (tokens), 모델 크기 (model size), GPU 노드 준비 상태 (GPU node readiness), 그리고 OpenAI의 Kubernetes 스케일링 교훈을 다루었습니다.

이제 무언가를 실행해 보아야 합니다.

이번 파트에서는 실제 모델을 Kubernetes GPU 노드에 배포하고, 이를 OpenAI 호환 API로 노출한 뒤, curl로 호출해 보겠습니다. 사용할 모델은 다음과 같습니다:

Qwen/Qwen2.5-1.5B-Instruct

이 모델은 첫 번째 단일 GPU 실습을 하기에 충분히 작으면서도, 실제 채팅 모델처럼 동작합니다. 만약 GPU 메모리가 매우 작다면 Qwen/Qwen2.5-0.5B-Instruct를 시도해 보세요. 메모리가 더 여유 있고 더 큰 테스트를 원한다면 Qwen/Qwen2.5-7B-Instruct를 시도해 보세요.

생각나는 가장 큰 모델로 시작하지 마세요. 노드가 실제로 로드할 수 있는 모델로 시작하세요. 여기서 목표는 벤치마크 점수를 높이는 것이 아닙니다. 목표는 Kubernetes GPU 용량 확보부터 작동하는 LLM API 요청까지 도달하는 것입니다.

이 설정에서 vLLM이 하는 역할

Kubernetes가 단독으로 모델을 서빙하는 것은 아닙니다. Kubernetes는 Pod를 스케줄링하고, 네트워킹을 부여하며, Secret을 마운트하고, NVIDIA 디바이스 플러그인 (NVIDIA device plugin)에 GPU를 요청합니다. 그 이후에 컨테이너 내부의 모델 서버가 LLM 특화 작업을 수행해야 합니다.

이 가이드에서 사용하는 모델 서버가 바로 vLLM입니다. vLLM은 모델 가중치(model weights)를 다운로드하고, 이를 GPU 메모리에 로드하며, HTTP 서버를 시작하고, OpenAI 호환 요청(OpenAI-compatible requests)을 수락하며, 내부적으로 작업을 배치(batching)하고, 모델을 실행하며, 생성된 토큰을 스트리밍하거나 반환합니다.

이러한 구분은 중요합니다. Kubernetes Deployment가 단순히 nvidia.com/gpu: 1을 가지고 있다고 해서 마법처럼 LLM API가 되는 것은 아닙니다. 해당 컨테이너가 Hugging Face 모델을 로드하는 방법과 /v1/chat/completions와 같은 경로(route)를 노출하는 방법을 아는 서빙 엔진(serving engine)을 시작하기 때문에 LLM API가 되는 것입니다.

vLLM은 구조를 숨기지 않으면서도 많은 복잡한 세부 사항을 숨겨주기 때문에 첫 번째 서빙 엔진으로 적합합니다. 여러분은 여전히 모델 이름, GPU 요청, 포트, 토큰 Secret, 로그, Service, 그리고 curl 요청을 확인할 수 있습니다. 하지만 배포가 작동하는지 증명하기 위해 직접 배치 루프(batching loop), 토크나이저 경로(tokenizer path), HTTP 서버, 또는 OpenAI 호환 API 래퍼(API wrapper)를 작성할 필요는 없습니다.

vLLM은 엔진입니다. 우리가 관심을 갖는 것은 그것이 제공하는 모델 API입니다.

사전 요구 사항 (Prerequisites)

이미 Part 4에서 GPU 노드 설정을 완료했다고 가정합니다. 즉, NVIDIA 드라이버 스택(driver stack), 컨테이너 런타임(container runtime), GPU Operator 또는 NVIDIA device plugin, 레이블(labels), 그리고 기본적인 GPU 확인 작업이 이미 완료되어 작동하고 있다는 의미입니다.

여기서 GPU Operator를 다시 설치하지는 않습니다. 모델을 배포하기 전에, Kubernetes가 GPU 용량을 인식할 수 있는지 확인하십시오:

kubectl get nodes -o=custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia\.com/gpu

유용한 출력 결과는 다음과 같습니다:

NAME            GPU
gpu-worker-01   1

만약 GPU 열이 비어 있거나, <none>이거나, 누락되었다면 여기서 중단하십시오. 노드가 nvidia.com/gpu를 광고(advertise)하기 전까지 Kubernetes는 이 워크로드(workload)를 스케줄링할 수 없습니다.

먼저 Hugging Face 토큰 생성하기

Qwen/Qwen2.5-1.5B-Instruct가 공개 모델임에도 불구하고, 우리는 여전히 Hugging Face 토큰을 사용할 것입니다. 이는 의도된 사항입니다.

실제 팀들은 종종 공개 모델 (public model)로 시작하여 나중에 게이트형 모델 (gated model), 프라이빗 모델 (private model), 라이선스 모델 (licensed model) 또는 조직 리포지토리 (organization repository)로 교체합니다. 만약 토큰 경로가 이미 배포 (Deployment)의 일부라면, 이러한 교체 작업은 훨씬 덜 번거롭습니다.

먼저 토큰을 생성하세요:

  1. Hugging Face 공식 토큰 문서를 엽니다: https://huggingface.co/docs/hub/security-tokens
  2. 읽기 권한 (read access)이 있는 토큰을 생성합니다.
  3. 토큰 값을 복사하여 준비해 둡니다.

이 시점부터는 여러분이 토큰 값을 가지고 있다고 가정하겠습니다. Git에 토큰을 붙여넣지 마세요. 배포 (Deployment) 매니페스트 (manifest)에 직접 넣지도 마세요. Kubernetes Secret에 넣으세요.

네임스페이스 (namespace) 및 Secret 생성

첫 번째 LLM 워크로드 (workload)를 기본 (default) 네임스페이스에서 분리하세요:

kubectl create namespace llm-demo

셸 (shell)에 토큰을 설정합니다:

export HF_TOKEN="hf_your_token_here"

Secret을 생성합니다:

kubectl create secret generic hf-token \
  -n llm-demo \
  --from-literal=HF_TOKEN="${HF_TOKEN}"

존재 여부를 확인합니다:

kubectl get secret hf-token -n llm-demo

예상 결과:

NAME       TYPE     DATA   AGE
hf-token   Opaque   1      10s

존재하는 것만으로 충분합니다. 특별한 이유가 없다면 토큰을 다시 출력하지 마세요.

모델 API 배포

vLLM은 모델 서버와 OpenAI 호환 HTTP API를 제공합니다. Kubernetes 패턴은 vLLM Kubernetes 문서에 설명되어 있으며, API 형태는 vLLM OpenAI 호환 서버 문서에 설명되어 있습니다.

qwen-vllm.yaml 파일을 생성합니다:

apiVersion: apps/v1
kind: Deployment
metadata:
...

몇 가지 세부 사항이 중요합니다.

포드 (pod)는 nvidia.com/gpu: 1을 통해 하나의 GPU를 요청합니다. 이것이 이 워크로드를 GPU 워크로드로 스케줄링 (schedulable) 가능하게 만드는 요소입니다. 토큰은 HF_TOKENHUGGING_FACE_HUB_TOKEN 두 가지로 모두 나타나는데, 이는 서로 다른 라이브러리 (libraries)와 예제들이 서로 다른 이름을 사용하기 때문입니다. 두 이름 모두 동일한 Secret 값을 가리킵니다.

/dev/shm 마운트가 있는 이유는 모델 서버가 공유 메모리 (shared memory)를 많이 사용하기 때문입니다. 컨테이너 내부의 아주 작은 기본 공유 메모리 제한은 기이한 오류를 발생시킬 수 있습니다. 메모리 기반의 emptyDir을 사용하면 첫 번째 배포를 문제없이 진행할 수 있습니다.

이 포드 (pod)가 시작될 때, vLLM은 대략 다섯 가지 작업을 수행합니다. 명령에서 모델 이름을 읽고, Hugging Face 토큰을 사용하여 저장소에 접근하며, 모델 파일을 다운로드하거나 재사용하고, 토크나이저 (tokenizer) 및 모델 런타임 (model runtime)을 초기화한 다음, 8000 포트에서 API 서버를 시작합니다. 이 과정이 완료된 후에야 API를 사용할 수 있습니다.

운영 환경 (production)에서는 latest를 사용하는 대신 vllm/vllm-openai 이미지 버전을 고정(pin)하십시오. 이번 실습에서는 예제를 읽기 쉽게 유지하기 위해 latest를 사용합니다.

적용하기:

kubectl apply -f qwen-vllm.yaml

예상 출력:

deployment.apps/qwen-vllm created
service/qwen-vllm created

시작 과정을 제대로 관찰하기

포드를 관찰하십시오:

kubectl get pods -n llm-demo -w

다음과 같이 보일 수 있습니다:

NAME                         READY   STATUS              RESTARTS   AGE
qwen-vllm-6c9f7d8c9d-x9v2m   0/1     Pending             0          3s
qwen-vllm-6c9f7d8c9d-x9v2m   0/1     ContainerCreating   0          15s
...

너무 일찍 축하하지 마십시오.

Running 상태가 준비(ready) 상태와 같은 것은 아닙니다. 이미지가 아직 자리 잡는 중이거나, 모델을 다운로드 중이거나, CUDA를 초기화 중이거나, 가중치 (weights)를 로드 중이거나, 또는 vLLM이 서빙 엔진 (serving engine)을 준비하는 동안에도 컨테이너는 running 상태일 수 있습니다. 첫 시작은 모델을 가져와야(pull) 하기 때문에 보통 더 느립니다.

로그를 따라가십시오:

kubectl logs -n llm-demo -f deployment/qwen-vllm

서버가 모델 로딩을 마치고 8000 포트에서 대기하는 것을 확인해야 합니다. 정확한 로그 라인은 vLLM 버전에 따라 다릅니다. 로그가 여전히 바쁘게 움직인다면 기다리십시오. 만약 명확한 에러가 나타난다면 아래의 문제 해결 (troubleshooting) 표로 이동하십시오.

서비스 포트 포워딩 (Port-forward)

첫 번째 테스트를 위해 공개 인그레스 (public ingress)를 생성하지 마십시오. DNS를 추가하지 마십시오. 인터넷에 노출된 로드 밸런서 (load balancer) 뒤에 두지 마십시오.

포트 포워딩을 사용하십시오:

kubectl port-forward -n llm-demo svc/qwen-vllm 8000:8000

해당 명령어를 계속 실행 상태로 유지하십시오. 다음과 같은 메시지가 보여야 합니다:

Forwarding from 127.0.0.1:8000 -> 8000
Forwarding from [::1]:8000 -> 8000

이제 로컬 포트 8000이 Kubernetes 서비스 (Service)로 전달되며, 이 서비스는 다시 vLLM 포드 (pod)로 전달됩니다.

첫 번째 curl 요청 보내기

다른 터미널에서 OpenAI 호환 채팅 엔드포인트 (chat endpoint)를 호출하십시오:

curl http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
...

왜 curl 요청에 모델 이름을 다시 포함하나요?

이 부분은 처음 보면 중복되어 보일 수 있습니다:

"model": "Qwen/Qwen2.5-1.5B-Instruct"

우리는 이미 Deployment에서 vllm serve에 모델 이름을 전달했습니다. 그것은 서버에 어떤 모델을 메모리에 로드할지 알려주는 역할을 합니다. curl 요청의 model 필드는 OpenAI 호환 API 규약 (API contract)의 일부입니다. 클라이언트는 서버가 어떤 서비스 중인 모델을 대상으로 하는 요청인지 알 수 있도록 이 값을 보냅니다.

이 글에서는 서버에 모델이 하나뿐이므로 값이 반복되는 것처럼 느껴집니다. 실제 시스템에서는 동일한 API 스타일이 라우터 (router), 게이트웨이 (gateway), 별칭 (alias), 여러 개의 Deployment, 또는 모델 간 전환이 가능한 클라이언트 뒤에 위치할 수 있습니다. 이 필드를 유지함으로써 curl, OpenAI SDK 코드, 그리고 추후 게이트웨이 설정이 모두 동일한 형태를 따르게 됩니다.

첫 실행 시에는 vllm serve에 전달한 모델 값과 동일하게 유지하십시오. 나중에 vLLM은 서비스 중인 모델 이름의 별칭을 사용하여 클라이언트에 노출되는 다른 이름을 제공할 수 있지만, 이는 아직 필요하지 않은 추가적인 복잡성입니다.

성공적인 응답은 JSON 형태일 것입니다. 정확한 문구는 다를 수 있지만, 구조는 익숙할 것입니다:

{
  "object": "chat.completion",
  "model": "Qwen/Qwen2.5-1.5B-Instruct",
...

그 순간 배포 (deployment)가 실체화됩니다. 요청이 모델 서버에 도달했고, vLLM이 OpenAI 호환 경로를 처리했으며, 모델이 텍스트를 생성했고, 응답이 Kubernetes를 통해 돌아왔습니다. 다이어그램도 아니고 약속도 아닙니다. 클러스터 내부에서 실행 중인 API를 통해 모델이 답변을 한 것입니다.

모델 교체하기

더 작은 모델을 시도하려면, 서빙되는 모델을 변경하세요:

command:
  - vllm
  - serve
...

그 다음 curl 본문(body)도 변경하세요:

"model": "Qwen/Qwen2.5-0.5B-Instruct"

더 큰 모델로 테스트하려면, 두 곳 모두에 Qwen/Qwen2.5-7B-Instruct를 사용하세요.

첫 실행 시에는 요청(request)에 포함된 모델 이름을 vLLM이 서빙하는 모델 이름과 동일하게 유지하세요. 별칭(alias) 설정은 나중에 할 수 있습니다. 오늘은 불필요한 디버깅을 피합시다.

무슨 일이 일어났는가

Kubernetes는 nvidia.com/gpu를 광고하는 노드에 Pod를 스케줄링했습니다. NVIDIA 디바이스 플러그인(device plugin)이 컨테이너에서 GPU를 사용할 수 있도록 만들었습니다. Hugging Face 토큰은 컨테이너가 모델을 풀(pull)할 수 있게 해주었습니다. vLLM은 모델을 GPU에 로드하고 8000 포트에서 HTTP 서버를 시작했습니다. 서비스(Service)는 Pod에 클러스터 내부의 안정적인 엔드포인트(endpoint)를 제공했습니다. 포트 포워딩(Port-forward)은 안전한 로컬 경로를 제공했습니다. Curl은 /v1/chat/completions를 통해 API가 응답할 수 있음을 증명했습니다.

이것이 모든 LLM 플랫폼이 화려해지기 전에 갖춰야 할 기본적인 루프(loop)입니다:

  1. Kubernetes가 워크로드(workload)를 GPU에 스케줄링할 수 있는가?
  2. 컨테이너가 GPU를 볼 수 있는가?
  3. 모델 서버가 모델을 다운로드하고 로드할 수 있는가?
  4. API 라우트(route)가 요청을 수락할 수 있는가?
  5. 모델이 응답을 생성할 수 있는가?
  6. 이 단계 중 하나라도 깨졌을 때 실패를 관찰할 수 있는가?

만약 이 루프가 신뢰할 수 없다면, 오토스케일링(autoscaling)과 게이트웨이(gateways)도 당신을 구원하지 못할 것입니다. 그것들은 단지 문제를 잠시 동안 숨겨줄 뿐입니다.

트러블슈팅 (Troubleshooting)

증상일반적인 의미확인 사항
Pod가 Pending 상태로 멈춤Kubernetes가 일치하는 노드를 찾을 수 없음kubectl describe pod -n llm-demo <pod-name>을 실행하고 스케줄러 이벤트를 읽으세요. GPU 용량이 존재하는지 확인하세요.
...

가장 흔한 실수는 Running 상태를 결승선으로 생각하는 것입니다. 그렇지 않습니다. 모델 서빙(model serving)의 경우, 준비 상태(readiness)는 다운로드, GPU 초기화, 모델 로드 및 서버 시작과 연결되어 있습니다. Pod의 단계(phase)뿐만 아니라 로그를 확인하세요.

정리 (Clean up)

이것이 단순한 테스트였다면, 네임스페이스(namespace)를 삭제하세요:

kubectl delete namespace llm-demo

그 명령은 Deployment, Service, 그리고 Secret을 삭제합니다. 만약 계속해서 실험을 이어간다면, GPU pod는 아무도 요청을 보내지 않을 때도 값비싼 용량(capacity)을 점유할 수 있다는 점을 기억하세요.

아직 다루지 않는 내용

이 글은 첫 번째로 작동하는 API 호출 단계에서 마무리됩니다. 공개 인그레스 (public ingress), 인증 (authentication), 오토스케일링 (autoscaling), 멀티 GPU 서빙 (multi-GPU serving), 양자화 (quantization), 프로덕션 모니터링 (production monitoring), 또는 비용 최적화 (cost optimization)는 아직 다루지 않습니다.

이것들은 사소한 디테일이 아닙니다. 공개 인그레스 (public ingress)는 TLS, 라우팅 (routing), 제한 (limits), 그리고 남용 제어 (abuse controls)를 가져옵니다. 인증 (authentication)은 누가 모델을 호출할 수 있는지를 결정합니다. 오토스케일링 (autoscaling)은 CPU뿐만 아니라 LLM 특화 시그널 (LLM-specific signals)이 필요합니다. 멀티 GPU 서빙 (multi-GPU serving)은 스케줄링 (scheduling)과 장애 동작 (failure behavior)을 변화시킵니다. 양자화 (quantization)는 메모리와 품질 사이의 트레이드오프 (tradeoffs)를 변화시킵니다. 모니터링 (monitoring)에는 토큰 (token), 지연 시간 (latency), GPU, 큐 (queue), 그리고 모델 서버 (model-server) 메트릭 (metrics)이 필요합니다.

하지만 이 모든 것은 이 기본적인 경로가 성공적으로 작동한 이후의 단계입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0