Data Center / Cloud

공유 GPU 인프라에서 격리된 테넌트 쿠버네티스 클러스터 운영하기

Reading Time: 7 minutes

팀별로 전용 쿠버네티스 클러스터를 각각 운영하는 방식은 조직에 필요한 것보다 과도한 수준의 격리를 초래하는 경우가 많습니다. 하나의 클러스터를 여러 팀이 공유하도록 구현할 수 있지만, 팀 수가 늘어날수록 관리 조율 비용 또한 급증합니다. CRD 버전 충돌, RBAC 영역의 중첩, GPU 용량을 팀별 예산에 맞게 깔끔하게 분할 관리하기 어렵다는 점 등이 주요 과제로 꼽힙니다. 특정 규모에 도달하면 팀들은 개발 자율성을 확보하기 위해 개별 클러스터 할당을 요구하기 시작합니다.

본 게시글에서는 하드웨어를 분할하지 않으면서도 팀의 자율성을 보존할 수 있는 패턴을 제공합니다. 이 솔루션은 GPU 풀을 갖춘 단일 컨트롤 플레인 클러스터, 팀별 쿼터가 적용된 GPU 공유 기능, 그리고 API 서버, 컨트롤러, 데이터 스토어, 싱커(syncer), 스케줄러를 포함하여 팀별로 완전히 격리된 쿠버네티스 컨트롤 플레인 구조로 구성됩니다. 이는 두 가지 오픈소스 도구인 KAI SchedulervCluster를 활용하여 구현할 수 있습니다.

튜토리얼 단계를 차례로 따라 하면 3개 팀이 하나의 물리 GPU를 공유하면서 각자의 테넌트 클러스터 내에서 실제 GPU 기반 쿠버네티스 파드(Pod)를 실행하는 환경을 구축할 수 있습니다. 또한 각 팀이 오직 자신들의 워크로드만 볼 수 있도록 격리되었는지 직접 검증할 수 있습니다.

튜토리얼 전제 조건 및 주의 사항

사용자가 최소한의 리소스만으로도 직접 재현해 볼 수 있도록, 본 튜토리얼에서는 1대의 NVIDIA L40S GPU를 탑재한 클러스터에서 3개 팀이 해당 GPU의 일부 자원을 나누어 사용하는 구성을 채택했습니다. 이를 통해 전체 동작 방식을 명확히 파악하고 쉽게 실습해 볼 수 있습니다. 이 방식은 수백 개의 GPU 노드와 수십 개의 팀으로 구성된 대규모 클러스터에서도 동일하게 적용됩니다. 노드 풀, 큐 계층 구조, 테넌트 클러스터 수 역시 동일한 패턴으로 확장할 수 있습니다.

KAI Scheduler는 AI 워크로드의 GPU 리소스 할당 최적화를 목표로 설계된 강력하고 효율적이며 확장 가능한 토폴로지 인식 Kubernetes 스케줄러입니다. 수천 개의 노드와 높은 처리량의 워크로드를 포함한 대규모 GPU 클러스터를 관리하도록 설계되었습니다. KAI Scheduler를 사용하면 워크로드에 GPU 리소스를 동적으로 할당할 수 있습니다. 기본 kube-scheduler와 함께 실행할 수 있으며, schedulerName: kai-scheduler가 지정된 파드는 KAI Scheduler가 처리하고 나머지는 일반 kube-scheduler 프로세스를 거칩니다.

vCluster Kubernetes 플랫폼은 인프라 또는 베어 메탈에서 직접 완전히 격리된 테넌트 클러스터를 프로비저닝합니다. 각 테넌트 클러스터는 자체 API 서버, CRD(Custom Resource Definitions), RBAC(Role-Based Access Control)를 가지며, 전용 Kubernetes 클러스터와 구분할 수 없는 환경을 제공하면서 기저 노드와 하드웨어를 공유합니다. 가상화된 컨트롤 플레인은 테넌트에게 보이지 않습니다. 공유 컨트롤 플레인 노드도, 클러스터 내 에이전트 파드도, 환경 간 측면 경로도 없습니다. 이는 팀들이 하드웨어를 분리하지 않고도 깔끔한 자체 클러스터 경험이 필요한 GPU 인프라에 vCluster가 자연스럽게 적합한 이유입니다.

이 튜토리얼에서는 vCluster 공유 노드 모델을 사용하므로, 팀들은 GPU 노드를 공유하면서도 각자 격리된 컨트롤 플레인을 갖습니다. 이는 신뢰할 수 있는 내부 팀에 적합한 구성입니다. 노드·네트워크·스토리지 수준의 분리가 필요한 신뢰할 수 없는 테넌트의 경우, 동일한 패턴을 vCluster 프라이빗 노드로 확장할 수 있습니다.

이 예시에서는 NLP 팀, Vision 팀, Recommender System 팀 세 팀을 사용합니다. NLP 팀은 자체 CRD를 설치하고 싶어합니다. Vision 팀은 스케줄링 디버깅을 위해 cluster-admin이 필요합니다. Recommender 팀은 다른 버전의 Kubeflow를 사용합니다. 아무도 kubectl 컨텍스트를 공유하다가 서로의 환경을 실수로 망가뜨리고 싶지 않습니다.

vCluster를 사용하면 각 팀이 격리된 컨트롤 플레인, RBAC, 네임스페이스, CRD를 갖게 됩니다. 모두 cluster-admin 접근 권한을 가질 수 있습니다. 내부적으로는 모든 테넌트 클러스터가 동일한 노드와 GPU를 공유합니다.

데모 환경

이 데모는 Nebius의 NVIDIA Brev GPU 인스턴스에서 실행됩니다:

  • NVIDIA L40S 1개, vCPU 40개, RAM 160 GiB, 디스크 256 GiB (VRAM 48 GB)
  • Ubuntu 24.04.4 LTS
  • MicroK8s v1.36.2 – Kubernetes는 Brev에서 사전 구성되었으며, MicroK8s gpu 애드온이 포함되어 gpu-operator-resources 네임스페이스에 NVIDIA GPU Operator가 사전 설치됨
  • KAI Scheduler v0.16.4
  • vCluster CLI 0.35.1

참고: 다른 설정의 경우 GKE/EKS/AKS/바닐라 k8s/k3s에서 클러스터 생성 및 GPU Operator 설치 단계가 다를 수 있습니다. Step 3(KAI Scheduler)부터는 CDI(Container Device Interface)가 활성화된 상태로 NVIDIA GPU Operator가 실행 중인 모든 Kubernetes에서 동일합니다.

Step 1: 로컬 툴링

모든 명령 앞에 microk8s를 붙이지 않아도 되도록 독립형 kubectlhelm을 설치합니다. 그런 다음 kubeconfig를 연결하고, 데모 중 snap이 컨트롤 플레인을 자동 업그레이드하지 않도록 MicroK8s를 고정합니다.

sudo snap refresh --hold microk8s

sudo snap install kubectl --classic --channel=1.35/stable
sudo snap install helm --classic

mkdir -p ~/.kube
sudo microk8s config > ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
chmod 600 ~/.kube/config

다음으로 필요한 MicroK8s 애드온이 활성화되어 있는지 확인합니다:

microk8s enable dns
microk8s enable hostpath-storage    # vCluster에는 PVC가 필요합니다

그런 다음 검증합니다:

kubectl get nodes -o wide
kubectl get storageclass
NAME             STATUS   ROLES    AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION                CONTAINER-RUNTIME
brev-8dq0cch1j   Ready    <none>   57d   v1.36.2   10.0.0.20     <none>        Ubuntu 24.04.4 LTS   6.11.0-1016-nvidia (amd64)   containerd://2.2.3

NAME                          PROVISIONER            RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
microk8s-hostpath (default)   microk8s.io/hostpath   Delete          WaitForFirstConsumer   false                  20m

Step 2: Helm 레포 추가

NVIDIA Helm 레포를 추가합니다:

helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

Step 3: GPU Operator 확인

kubectl get pods -n gpu-operator-resources
NAME                                                         READY   STATUS      RESTARTS   AGE
gpu-feature-discovery-wpcjm                                  1/1     Running     0          8m
gpu-operator-57d75775c8-npjzz                                1/1     Running     0          9m
gpu-operator-node-feature-discovery-...                      1/1     Running     0          9m
nvidia-container-toolkit-daemonset-ft4hb                     1/1     Running     0          8m
nvidia-cuda-validator-8vqdv                                  0/1     Completed   0          8m
nvidia-device-plugin-daemonset-l6fj6                         1/1     Running     0          8m
nvidia-operator-validator-cdnmj                              1/1     Running     0          8m

구버전 GPU Operator를 사용 중이라면 26.3.x로 업그레이드합니다:

helm upgrade gpu-operator nvidia/gpu-operator \
   -n gpu-operator-resources \
   --version v26.3.3 \
   --reset-then-reuse-values

필요한 경우 NVIDIA GPU Operator를 직접 설치합니다:

helm install gpu-operator nvidia/gpu-operator \
    -n gpu-operator-resources --create-namespace \
    --version v26.3.3 \
    --set driver.enabled=false \
    --set operator.defaultRuntime=containerd \
    --set toolkit.env[0].name=CONTAINERD_CONFIG \
    --set toolkit.env[0].value=/var/snap/microk8s/current/args/containerd.toml \
    --set toolkit.env[1].name=CONTAINERD_SOCKET \
    --set toolkit.env[1].value=/var/snap/microk8s/common/run/containerd.sock \
    --set-string toolkit.env[2].name=CONTAINERD_SET_AS_DEFAULT \
    --set-string toolkit.env[2].value=1

Step 4: KAI Scheduler 설치

다음으로 KAI Scheduler를 설치합니다:

helm upgrade -i kai-scheduler \
  oci://ghcr.io/kai-scheduler/kai-scheduler/kai-scheduler \
  -n kai-scheduler --create-namespace \
  --version v0.16.4 \
  --set "global.gpuSharing=true"

그런 다음 검증합니다:

kubectl get pods -n kai-scheduler
NAME                                     READY   STATUS    RESTARTS   AGE
admission-57556f949-sp98t                1/1     Running   0          47s
binder-66785d8dd9-9frgk                  1/1     Running   0          46s
kai-operator-6fdf595c4d-292d7            1/1     Running   0          52s
kai-scheduler-default-6bb667b767-vq2q4   1/1     Running   0          46s
pod-grouper-84dfc7759b-v5qtb             1/1     Running   0          47s
podgroup-controller-5878f48dbb-v7wcn     1/1     Running   0          47s
queue-controller-7796bb8984-hdn5r        1/1     Running   0          46s

Step 5: 팀 큐 정의

KAI Scheduler는 Queue CRD를 사용하여 조직 → 팀 계층 구조를 모델링합니다. 이 단계에서는 하나의 부모 큐(ml-org, 총 예산 GPU 1개)와 세 개의 자식 큐를 생성합니다. 각 큐는 GPU의 0.33이 보장되며, 다른 팀이 유휴 상태일 때 전체 GPU까지 사용량을 늘릴 수 있습니다.

다음을 create-queues.yaml로 저장합니다:

apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: ml-org
spec:
  resources:
    gpu: { quota: 1, limit: -1, overQuotaWeight: 1 }
---
apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: team-nlp
spec:
  parentQueue: ml-org
  priority: 100
  resources:
    gpu: { quota: 0.33, limit: 1, overQuotaWeight: 1 }
---
apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: team-vision
spec:
  parentQueue: ml-org
  priority: 100
  resources:
    gpu: { quota: 0.33, limit: 1, overQuotaWeight: 1 }
---
apiVersion: scheduling.run.ai/v2
kind: Queue
metadata:
  name: team-recommender
spec:
  parentQueue: ml-org
  priority: 100
  resources:
    gpu: { quota: 0.33, limit: 1, overQuotaWeight: 1 }

여기서 quota는 보장된 최솟값, limit은 허용된 최댓값, overQuotaWeight는 잉여 리소스를 분배하는 방식을 제어합니다.

다음으로 적용하고 목록을 확인합니다:

kubectl apply -f create-queues.yaml
kubectl get queues
queue.scheduling.run.ai/ml-org created
queue.scheduling.run.ai/team-nlp created
queue.scheduling.run.ai/team-vision created
queue.scheduling.run.ai/team-recommender created

NAME                   PRIORITY   PARENT                 CHILDREN                                        DISPLAYNAME
default-parent-queue                                     ["default-queue"]
default-queue                     default-parent-queue
ml-org                                                   ["team-nlp","team-vision","team-recommender"]
team-nlp               100        ml-org
team-recommender       100        ml-org
team-vision            100        ml-org

default-parent-queuedefault-queue는 KAI Scheduler 최초 설치 시 자동으로 생성됩니다. 큐를 지정하지 않은 파드의 폴백 큐 역할을 합니다.

Step 6: 팀별 vCluster 생성

다음으로 vCluster CLI를 설치합니다:

curl -L -o vcluster "https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-linux-amd64"
sudo install -m 755 vcluster /usr/local/bin/vcluster
rm vcluster
vcluster --version
vcluster version 0.35.1

vCluster 설정을 정의합니다. 핵심 설정은 setOwner: false입니다. KAI Scheduler의 pod-grouper는 소유권 체인(Job → Pod, Deployment → ReplicaSet → Pod)을 따라가며 워크로드를 자동으로 그룹화합니다. vCluster의 소유자 재작성을 비활성화하면 KAI Scheduler가 실제 계층 구조를 볼 수 있습니다.

cat > vcluster.yaml <<'EOF'
experimental:
  syncSettings:
    setOwner: false

sync:
  fromHost:
    nodes:
      enabled: true
      selector:
        all: true
EOF

그런 다음 팀별로 vCluster를 하나씩 생성합니다:

vcluster create team-nlp          --values vcluster.yaml --connect=false
vcluster create team-vision       --values vcluster.yaml --connect=false
vcluster create team-recommender  --values vcluster.yaml --connect=false

세 개의 vCluster가 모두 실행 중인지 확인합니다:

vcluster list
        NAME       |         NAMESPACE         | STATUS  | VERSION | CONNECTED | AGE
  -------------------+---------------------------+---------+---------+-----------+-------
    team-nlp         | vcluster-team-nlp         | Running | 0.35.1  |           | 102s
    team-recommender | vcluster-team-recommender | Running | 0.35.1  |           | 88s
    team-vision      | vcluster-team-vision      | Running | 0.35.1  |           | 93s

각 팀은 GPU 노드를 포함한 실제 노드를 볼 수 있습니다:

vcluster connect team-nlp -- kubectl get nodes
19:09:22 done vCluster is up and running
NAME             STATUS   ROLES    AGE     VERSION
brev-8dq0cch1j   Ready    <none>   6m20s   v1.36.2

Step 7: 각 팀에서 워크로드 배포

각 팀은 자신의 vCluster에서 GPU 워크로드를 배포합니다. 파드 스펙은 간단하며, KAI Scheduler에 지시사항을 전달하는 세 개의 필드가 있습니다:

vcluster connect team-nlp -- kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: nlp-sentiment-model
  labels:
    kai.scheduler/queue: team-nlp # 어느 팀인지
  annotations:
    gpu-fraction: "0.33"          # GPU 사용량
spec:
  schedulerName: kai-scheduler.   # 기본 스케줄러 대신 KAI 사용
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists
      effect: NoSchedule
  containers:
    - name: nlp-inference
      image: nvidia/cuda:12.4.0-base-ubuntu22.04
      command: ["bash", "-c", "nvidia-smi; sleep infinity"]
  nodeSelector:
    nvidia.com/gpu.present: "true"
EOF
19:13:34 done vCluster is up and running
pod/nlp-sentiment-model created

큐 이름을 변경하여 각 팀에서 동일하게 배포합니다.

이제 검증합니다:

NAMESPACE                   NAME                                             READY   STATUS    RESTARTS   AGE    IP             NODE
vcluster-team-nlp           nlp-sentiment-model-x-default-x-team-nlp         1/1     Running   0          110s   10.1.171.180   brev-8dq0cch1j
vcluster-team-recommender   recommender-model-x-default-x-team-recommender   1/1     Running   0          6s     10.1.171.187   brev-8dq0cch1j
vcluster-team-vision        vision-classifier-model-x-default-x-team-vision  1/1     Running   0          14s    10.1.171.145   brev-8dq0cch1j

세 개의 파드가 세 개의 서로 다른 vcluster-team-* 네임스페이스에서 동일한 물리적 노드 brev-8dq0cch1j에서 실행 중입니다.

각 팀은 자신의 vCluster 내부에서 자신의 파드만 볼 수 있습니다:

vcluster connect team-nlp         -- kubectl get pods -o wide
vcluster connect team-vision      -- kubectl get pods -o wide
vcluster connect team-recommender -- kubectl get pods -o wide
NAME                  READY   STATUS    RESTARTS   AGE     IP             NODE             NOMINATED NODE   READINESS GATES
nlp-sentiment-model   1/1     Running   0          3m11s   10.1.171.180   brev-8dq0cch1j   <none>           <none>

NAME                      READY   STATUS    RESTARTS   AGE     IP             NODE             NOMINATED NODE   READINESS GATES
vision-classifier-model   1/1     Running   0          3m59s   10.1.171.145   brev-8dq0cch1j   <none>           <none>

NAME                READY   STATUS    RESTARTS   AGE     IP             NODE             NOMINATED NODE   READINESS GATES
recommender-model   1/1     Running   0          2m45s   10.1.171.187   brev-8dq0cch1j   <none>           <none>

마지막으로 큐를 통해 할당을 확인합니다. 세 개를 동시에 확인할 수 있습니다:

kubectl describe queue team-nlp         | grep -A4 Status
kubectl describe queue team-vision      | grep -A4 Status
kubectl describe queue team-recommender | grep -A4 Status

모두 동일한 결과가 표시됩니다:

Status:
  Allocated:
    nvidia.com/gpu:  330m
  Requested:
    nvidia.com/gpu:  330m

KAI Scheduler는 어떤 파드가 어떤 GPU에 어떤 비율로 배치될지를 스케줄링 처리합니다. 단, GPU 공유 사용 시 하드웨어 수준에서 GPU 메모리 격리를 강제하지는 않습니다. 애플리케이션이 메모리 사용량을 직접 준수해야 합니다(예: vLLM에서 --gpu-memory-utilization 설정). 내부적으로 GPU는 커널 경계에서 각 파드의 CUDA 컨텍스트 간에 타임 슬라이싱됩니다. 지원되는 하드웨어에서 하드 메모리 격리가 필요한 경우, NVIDIA Multi-Instance GPU(MIG)가 하드웨어 수준 파티셔닝을 제공하며, KAI Scheduler로도 스케줄링할 수 있습니다.

격리된 테넌트 Kubernetes 클러스터 운영 시작하기

KAI Scheduler는 GPU 공유 및 DRA Driver 지원, 보장된 할당량과 초과 할당 기능을 갖춘 계층적 큐, 갱 스케줄링, 토폴로지 인식을 활용하여 누가 GPU 슬라이스를 공정하게 할당받아 그룹으로 스케줄링될지를 결정합니다. AI 워크로드 스케줄링이 해결되면 팀들은 자체 클러스터를 원하게 됩니다. vCluster는 별도 인프라 비용 없이 각 팀에게 격리된 Kubernetes 컨트롤 플레인을 제공합니다.

KAI Scheduler와 vCluster는 함께 하나의 GPU에서 세 팀이 낭비 없이 전용 클러스터 경험을 누릴 수 있게 합니다. 답은 항상 더 많은 GPU가 아니라, 인프라를 더 잘 활용하는 것입니다.

시작할 준비가 되셨나요? GitHub에서 KAI Scheduler, vCluster, NVIDIA GPU Operator를 확인해보세요.

KAI Scheduler와 vCluster 통합에 대한 자세한 내용은 11월 9~12일 KubeCon 2026 North America에서 확인하세요.

Discuss (0)

Tags