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

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — GPU 4장이 놀고 있다
- GPU Operator가 하는 일
- 첫 설치 — 그리고 판단 착오
- 증상 — 전부 Init에서 멈춘다
- 진단 — RuntimeClass는 있는데 런타임이 없다
- 왜 쿠버네티스에는 toolkit이 필수인가
- 해결 — helm upgrade
- 결과 — 설정은 어디에 심겼나
- 검증 — 진짜로 GPU를 쓸 수 있는가
- 최종 구성 — 32개 파드
- 정리
- 🧠 이해도 체크 퀴즈
- 참고 자료
들어가며 — GPU 4장이 놀고 있다
앞선 글에서 홈랩 클러스터를 5노드로 확장하고 NAS 스토리지를 붙였습니다. 워커 네 대에는 각각 GPU가 있습니다.
| 노드 | GPU | VRAM |
|---|---|---|
| gpu-a | RTX 3090 | 24GB |
| gpu-b | RTX 5090 | 32GB |
| gpu-c | RTX 4070 Laptop | 8GB |
| gpu-d | RTX 4070 Laptop | 8GB |
그런데 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 Plugin | kubelet에 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.enabled 가 false에서 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 파일만 지우면 되고 호스트는 원래 상태로 돌아갑니다. 제 grep 이 config.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.limits 에 nvidia.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 Operator | v26.3.3 |
| 관리 GPU | 4장 (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 를 건드리지 않습니다.