Skip to content

필사 모드: GPU Operator 설치 중 만난 "no runtime for nvidia is configured" — 컨테이너 런타임과 Helm 업그레이드가 실제로 하는 일

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — GPU 4장이 놀고 있다

앞선 글에서 홈랩 클러스터를 5노드로 확장하고 NAS 스토리지를 붙였습니다. 워커 네 대에는 각각 GPU가 있습니다.

노드GPUVRAM
gpu-aRTX 309024GB
gpu-bRTX 509032GB
gpu-cRTX 4070 Laptop8GB
gpu-dRTX 4070 Laptop8GB

그런데 kubectl describe node 를 보면 GPU가 자원 목록에 없습니다. 쿠버네티스는 CPU와 메모리는 알아서 인식하지만 GPU는 모릅니다. 파드에 nvidia.com/gpu: 1 을 요청해도 스케줄러가 "그런 자원 없다"며 Pending에 둡니다.

이 간극을 메우는 것이 NVIDIA GPU Operator 입니다.

GPU Operator가 하는 일

Operator는 여러 구성 요소를 묶어 배포합니다. 각각의 역할이 다릅니다.

Node Feature Discovery (NFD) — 노드의 하드웨어를 조사해 라벨을 붙입니다. PCI 장치 ID를 보고 "이 노드에 NVIDIA GPU가 있다"는 라벨을 답니다. 다른 구성 요소들은 이 라벨을 보고 자신을 배치할 노드를 정합니다.

Container Toolkit — 컨테이너 런타임이 GPU를 컨테이너 안으로 넣어줄 수 있게 만듭니다. 이 글의 주인공입니다.

Device Plugin — kubelet에게 "이 노드에 할당 가능한 GPU가 1개 있다"고 보고합니다. 그래야 nvidia.com/gpu 라는 자원이 스케줄러에 보입니다.

DCGM Exporter — GPU 사용률, 온도, 전력을 Prometheus 형식으로 노출합니다.

Operator Validator — 위의 것들이 실제로 동작하는지 단계별로 검증합니다. 검증에 실패하면 다른 파드를 기동시키지 않습니다.

첫 설치 — 그리고 판단 착오

호스트를 먼저 확인했습니다.

$ for h in gpu-a gpu-b gpu-c gpu-d; do ssh $h 'nvidia-smi --query-gpu=name,driver_version --format=csv,noheader; which nvidia-ctk'; done
NVIDIA GeForce RTX 3090, 570.195.03
/usr/bin/nvidia-ctk
NVIDIA GeForce RTX 5090, 570.195.03
/usr/bin/nvidia-ctk
...

드라이버 570.195.03이 설치돼 있고 nvidia-ctk 바이너리도 있습니다. 그래서 드라이버와 toolkit 설치를 모두 끄고 설치했습니다.

helm install gpu-operator nvidia/gpu-operator --version v26.3.3 \
  -n gpu-operator \
  --set driver.enabled=false \
  --set toolkit.enabled=false \
  --set devicePlugin.enabled=true \
  --set dcgmExporter.enabled=true \
  --set nfd.enabled=true

드라이버를 끈 것은 옳은 판단이었습니다. Operator가 드라이버를 다시 깔면 기존 드라이버와 충돌하고, 실패하면 노드가 GPU를 잃습니다.

toolkit을 끈 것이 틀렸습니다.

증상 — 전부 Init에서 멈춘다

gpu-feature-discovery-fpw5l              0/1   Init:0/1     gpu-c
nvidia-dcgm-exporter-gmlvl               0/1   Init:0/1     gpu-a
nvidia-device-plugin-daemonset-k4ggb     0/1   Init:0/1     gpu-a
nvidia-operator-validator-krj69          0/1   Init:0/4     gpu-a
gpu-operator-node-feature-discovery-...  1/1   Running      cp-1

NFD만 Running이고 GPU 관련 파드는 전부 Init입니다. 이벤트를 보니 원인이 명확했습니다.

Warning  FailedCreatePodSandBox  7s (x7 over 87s)  kubelet
  Failed to create pod sandbox: rpc error: code = Unknown
  desc = failed to get sandbox runtime: no runtime for "nvidia" is configured

파드가 시작조차 못 했습니다. 컨테이너 안에서 뭔가 실패한 게 아니라, 파드를 담을 샌드박스를 만드는 단계에서 막혔습니다.

진단 — RuntimeClass는 있는데 런타임이 없다

두 곳을 확인했습니다. 먼저 쿠버네티스 쪽입니다.

$ kubectl get runtimeclass
NAME            HANDLER         AGE
nvidia          nvidia          3m48s
nvidia-cdi      nvidia-cdi      3m48s
nvidia-legacy   nvidia-legacy   3m48s

nvidia 라는 RuntimeClass가 등록돼 있습니다. Operator가 만든 것입니다. 이제 노드 쪽입니다.

$ ssh gpu-a 'grep -c nvidia /etc/containerd/config.toml'
0

containerd 설정에 nvidia가 한 글자도 없습니다.

여기서 구조가 보입니다. RuntimeClass는 쿠버네티스 오브젝트이고, handler: nvidia 라는 이름표일 뿐입니다. 실체는 노드의 컨테이너 런타임이 갖고 있어야 합니다.

[파드] runtimeClassName: nvidia
[RuntimeClass] handler = "nvidia"          ← 쿠버네티스에 존재 ✅
[containerd] plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia
                                            ← 노드에 없음 ❌

이름표는 붙였는데 그 이름으로 찾아갈 곳이 없었던 것입니다.

왜 없었나

k8s를 재구축하면서 런타임을 cri-dockerd에서 containerd로 바꿨습니다. containerd를 새로 설치하면 config.toml 이 기본값으로 생성되는데, 여기에는 당연히 nvidia 런타임이 없습니다.

호스트의 nvidia-ctk 바이너리는 과거에 설치된 잔재였습니다. 바이너리가 있다는 것과 containerd가 그것을 쓰도록 설정돼 있다는 것은 전혀 다른 이야기인데, 저는 전자를 보고 후자를 추정했습니다.

왜 쿠버네티스에는 toolkit이 필수인가

여기서 한 걸음 물러나 볼 필요가 있습니다. 호스트에서 nvidia-smi 가 잘 되는데 왜 컨테이너에는 별도 장치가 필요할까요.

컨테이너는 격리된 네임스페이스에서 돕니다. 기본 상태에서는 /dev/nvidia0 같은 장치 파일도, libcuda.so 같은 드라이버 라이브러리도 컨테이너 안에 없습니다. 호스트에 있어도 컨테이너는 볼 수 없습니다.

GPU를 쓰려면 컨테이너를 만드는 바로 그 순간에 다음이 주입돼야 합니다.

  • 장치 파일 — /dev/nvidiactl, /dev/nvidia-uvm, /dev/nvidia0
  • 드라이버 라이브러리 — 호스트 드라이버와 버전이 정확히 일치해야 합니다
  • 유틸리티 — nvidia-smi

버전 일치가 중요합니다. 컨테이너 이미지에 CUDA 툴킷은 들어 있어도 드라이버 라이브러리는 넣을 수 없습니다. 호스트 드라이버가 570.195.03이면 컨테이너도 정확히 그 버전의 라이브러리를 써야 합니다. 이미지에 박아 넣으면 드라이버를 올릴 때마다 이미지를 다시 만들어야 합니다.

그래서 런타임이 컨테이너 생성 시점에 호스트의 것을 주입하는 방식을 씁니다. NVIDIA Container Toolkit이 그 일을 하는 훅(hook)을 런타임에 등록합니다.

정리하면 이렇습니다.

계층역할없으면
드라이버커널이 GPU를 다루게 함호스트에서도 GPU 못 씀
Container Toolkit컨테이너에 장치·라이브러리 주입컨테이너가 GPU 못 봄
Device Pluginkubelet에 GPU 개수 보고스케줄러가 GPU 자원을 모름

이 셋은 대체 관계가 아니라 직렬로 쌓이는 층입니다. 하나라도 빠지면 그 위가 동작하지 않습니다.

해결 — helm upgrade

고치는 방법은 둘입니다.

방법 1 — 노드에서 직접 설정

sudo nvidia-ctk runtime configure --runtime=containerd
sudo systemctl restart containerd

네 노드 모두에서 실행하고 containerd를 재시작해야 합니다. 재시작은 그 노드의 모든 컨테이너에 영향을 줍니다.

방법 2 — Operator가 하게 한다

helm upgrade gpu-operator nvidia/gpu-operator --version v26.3.3 \
  -n gpu-operator \
  --reuse-values \
  --set toolkit.enabled=true

이쪽을 택했습니다. sudo가 필요 없고, 노드가 늘어나도 자동으로 적용되며, Operator가 설정의 생애주기를 관리합니다.

helm upgrade는 내부적으로 무엇을 하는가

Helm은 상태를 클러스터 안에 저장합니다. 릴리스마다 Secret에 리비전을 쌓습니다.

$ helm history gpu-operator -n gpu-operator
REVISION  UPDATED                   STATUS      CHART                 DESCRIPTION
1         Wed Aug 19 10:29:43 2026  superseded  gpu-operator-v26.3.3  Install complete
2         Wed Aug 19 10:34:05 2026  deployed    gpu-operator-v26.3.3  Upgrade complete

리비전 1은 superseded(대체됨), 2가 deployed입니다. 1이 지워지지 않고 남아 있는 점이 중요합니다. helm rollback gpu-operator 1 로 되돌릴 수 있습니다.

업그레이드 절차는 이렇습니다.

1. 값 병합--reuse-values 는 이전 리비전의 값을 그대로 가져온 뒤 새로 준 것만 덮어씁니다. 이게 없으면 차트 기본값으로 초기화되어 driver.enabled=false 가 날아가고, Operator가 드라이버를 다시 설치하려 들었을 것입니다. 이 플래그 하나가 사고를 막았습니다.

2. 템플릿 렌더링 — 병합된 값으로 차트를 다시 렌더링해 최종 매니페스트를 만듭니다.

3. 3-way 병합 — Helm은 세 가지를 비교합니다. 이전 리비전의 매니페스트, 새로 렌더링한 매니페스트, 그리고 클러스터의 현재 실제 상태입니다. 셋을 견줘 필요한 변경만 적용하므로, 다른 도구가 손댄 부분을 함부로 되돌리지 않습니다.

4. 적용 — 이번 경우 ClusterPolicy 커스텀 리소스의 toolkit.enabledfalse에서 true로 바뀝니다.

여기서 Operator 패턴이 작동합니다. Helm은 CR을 고쳤을 뿐이고, 실제 일은 Operator 컨트롤러가 합니다. CR 변경을 감지해 toolkit DaemonSet을 생성합니다.

helm upgrade → ClusterPolicy CR 수정 → Operator가 감지 → DaemonSet 생성 → 각 노드에서 containerd 설정

Helm이 DaemonSet을 직접 만들지 않는다는 점이 핵심입니다. 선언(CR)만 바꾸고 조립은 Operator가 합니다.

결과 — 설정은 어디에 심겼나

nvidia-container-toolkit-daemonset-bw9v6   1/1   Running
nvidia-container-toolkit-daemonset-n26r2   1/1   Running
nvidia-container-toolkit-daemonset-wvn4t   1/1   Running
nvidia-container-toolkit-daemonset-zw9mz   1/1   Running

GPU 노드 4대에 하나씩 떴습니다. 그런데 다시 확인해 보니,

$ ssh gpu-a 'grep -c nvidia /etc/containerd/config.toml'
0

여전히 0입니다. 그런데 파드는 정상 기동했습니다. 어디에 설정된 걸까요.

$ ssh gpu-a 'grep -rl nvidia /etc/containerd/'
/etc/containerd/conf.d/99-nvidia.toml

drop-in 파일이었습니다. Operator는 config.toml 을 직접 고치지 않고 conf.d/ 에 별도 파일을 떨어뜨립니다.

$ ssh gpu-a 'ls -la /usr/local/nvidia/toolkit/nvidia-container-runtime'
-rwxr-xr-x 1 root root 395 Aug 19 10:35 /usr/local/nvidia/toolkit/nvidia-container-runtime

런타임 바이너리도 /usr/local/nvidia/toolkit/ 에 따로 설치했습니다. 호스트의 /usr/bin/nvidia-ctk 를 건드리지 않습니다.

이 설계는 의도적입니다. 호스트가 원래 갖고 있던 설정과 섞이지 않으므로, Operator를 제거하면 drop-in 파일만 지우면 되고 호스트는 원래 상태로 돌아갑니다. 제 grepconfig.toml 만 본 탓에 잠깐 혼란스러웠지만, 실제로는 더 안전한 방식입니다.

검증 — 진짜로 GPU를 쓸 수 있는가

파드가 Running이라고 끝이 아닙니다. 자원 등록부터 확인했습니다.

$ kubectl get nodes -o custom-columns="NODE:.metadata.name,GPU:.status.capacity.nvidia\.com/gpu,MODEL:.metadata.labels.nvidia\.com/gpu\.product"
NODE     GPU      MODEL
cp-1   <none>   <none>
gpu-c     1        NVIDIA-GeForce-RTX-4070-Laptop-GPU
gpu-d     1        NVIDIA-GeForce-RTX-4070-Laptop-GPU
gpu-a     1        NVIDIA-GeForce-RTX-3090
gpu-b    1        NVIDIA-GeForce-RTX-5090

GPU가 없는 cp-1은 none이고 나머지 넷은 각자 모델명까지 라벨로 붙었습니다. NFD가 하드웨어를 조사해 붙인 것입니다. 총 4장입니다.

이제 실제 파드에서 써 봅니다.

apiVersion: v1
kind: Pod
metadata: { name: gpu-test }
spec:
  restartPolicy: Never
  nodeSelector: { kubernetes.io/hostname: gpu-b }
  containers:
    - name: cuda
      image: nvidia/cuda:12.8.0-base-ubuntu24.04
      command: ["sh","-c","nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader"]
      resources:
        limits: { nvidia.com/gpu: 1 }
=== 파드 상태: Succeeded ===
NVIDIA GeForce RTX 5090, 32607 MiB, 570.195.03

컨테이너 안에서 RTX 5090이 보입니다. 드라이버 버전도 호스트와 같은 570.195.03입니다. 이미지에는 CUDA 베이스만 들어 있고 드라이버는 없는데, 런타임이 호스트 것을 주입해 준 결과입니다.

resources.limitsnvidia.com/gpu: 1 을 적은 것만으로 스케줄러가 GPU 있는 노드를 고르고, 런타임이 장치를 넣어 줬습니다.

최종 구성 — 32개 파드

  5  gpu-operator-node-feature-discovery-worker    ← 전 노드 (GPU 없는 노드 포함)
  4  nvidia-container-toolkit                      ← GPU 노드만
  4  nvidia-device-plugin
  4  nvidia-dcgm
  4  nvidia-cuda-validator
  4  nvidia-operator-validator
  4  gpu-feature-discovery
  1  gpu-operator                                  ← 컨트롤러
  1  gpu-operator-node-feature-discovery-master
  1  gpu-operator-node-feature-discovery-gc

NFD worker만 5개인 점이 눈에 띕니다. GPU가 없는 cp-1에도 떠서 하드웨어를 조사합니다 — 나중에 GPU를 꽂으면 자동으로 감지하기 위해서입니다. 나머지 GPU 관련 구성 요소는 NFD가 붙인 라벨을 보고 GPU 노드 4대에만 배치됐습니다.

정리

항목
GPU Operatorv26.3.3
관리 GPU4장 (3090 / 5090 / 4070 ×2)
드라이버570.195.03 (호스트 기존 것 사용)
파드32개 전부 Running
Helm 리비전2 (rollback 가능)
설정 위치/etc/containerd/conf.d/99-nvidia.toml

되돌아보면 이 사고의 교훈은 하나로 요약됩니다. "바이너리가 있다"와 "설정이 되어 있다"는 다른 이야기입니다. which nvidia-ctk 가 경로를 뱉었다고 containerd가 그것을 쓰도록 구성돼 있다는 뜻이 아닙니다. 확인해야 할 것은 바이너리의 존재가 아니라 런타임 설정 자체였습니다.

그리고 --reuse-values 는 습관으로 붙일 만합니다. 이번에 이 플래그가 없었다면 driver.enabled=false 가 기본값으로 돌아가 Operator가 드라이버를 다시 설치하려 들었을 것이고, 훨씬 큰 사고가 됐을 것입니다.

🧠 이해도 체크 퀴즈

1. RuntimeClass는 등록돼 있는데 "no runtime for nvidia is configured" 오류가 나는 이유는 무엇인가요?

RuntimeClass는 쿠버네티스 오브젝트로, handler: nvidia 라는 이름표를 정의할 뿐입니다. 실제 런타임 구현은 각 노드의 containerd 설정에 있어야 합니다. 이름표는 붙였지만 그 이름으로 찾아갈 런타임이 노드에 없으면, 파드를 담을 샌드박스를 만드는 단계에서 실패합니다.

2. 호스트에서 nvidia-smi가 잘 되는데 컨테이너에는 왜 별도의 toolkit이 필요한가요?

컨테이너는 격리된 네임스페이스라 /dev/nvidia* 장치 파일과 드라이버 라이브러리가 기본적으로 보이지 않습니다. 특히 드라이버 라이브러리는 호스트 드라이버와 버전이 정확히 일치해야 해서 이미지에 넣을 수 없습니다. 그래서 컨테이너 생성 시점에 런타임이 호스트의 것을 주입하는 방식을 쓰고, 그 훅을 등록하는 것이 Container Toolkit입니다.

3. --reuse-values 가 없었다면 어떤 일이 벌어졌을까요?

이전 리비전의 값이 버려지고 차트 기본값으로 초기화됩니다. driver.enabled=false 가 기본값인 true 로 돌아가 Operator가 드라이버를 다시 설치하려 시도했을 것이고, 이미 570.195.03이 설치된 호스트와 충돌해 노드가 GPU를 잃을 수 있었습니다.

4. Helm upgrade가 DaemonSet을 직접 만들지 않는데 어떻게 toolkit이 배포되나요?

Helm은 ClusterPolicy 커스텀 리소스의 toolkit.enabled 값을 false에서 true로 바꿀 뿐입니다. 그 변경을 GPU Operator 컨트롤러가 감지해 DaemonSet을 생성하고, DaemonSet의 파드가 각 노드에서 containerd 설정을 심습니다. 선언은 Helm이, 조립은 Operator가 하는 분업입니다.

5. Operator가 config.toml을 직접 수정하지 않고 conf.d/99-nvidia.toml 을 쓰는 이유는 무엇인가요?

호스트가 원래 갖고 있던 설정과 섞이지 않게 하기 위해서입니다. drop-in 파일로 분리하면 Operator를 제거할 때 그 파일만 지우면 되고 호스트는 원래 상태로 돌아갑니다. 런타임 바이너리도 /usr/local/nvidia/toolkit/ 에 따로 설치해 호스트의 /usr/bin/nvidia-ctk 를 건드리지 않습니다.

참고 자료

현재 단락 (1/175)

[앞선 글](/blog/kubernetes/synology-nfs-dynamic-pvc-csi-driver)에서 홈랩 클러스터를 5노드로 확장하고 NAS 스토리지를 붙였습니다. 워...

작성 글자: 0원문 글자: 9,031작성 단락: 0/175