RKE2 GPU Cluster

이 문서는 tirosh-home site에서 GPU node를 RKE2 기반 workload cluster로 설치하고, NVIDIA GPU Operator로 GPU resource를 Kubernetes에 노출하는 흐름을 설명합니다.

RKE2를 선택한 배경은 ADR 0001을 참고합니다. GPU Operator를 별도 계층으로 두는 배경은 ADR 0002를 참고합니다.

1. 전체 흐름

1-1. 설치와 GPU enablement 분리

GPU cluster 구성은 RKE2 설치와 GPU enablement를 분리해서 봅니다.

1-2. 구성 흐름

host static IP 적용
  |
  v
inventory 렌더링
  |
  v
RKE2 server 설치
  |
  v
RKE2 agent join
  |
  v
kubeconfig merge
  |
  v
NVIDIA GPU Operator 적용
  |
  v
GPU runtime 검증

1-3. 기본 실행 순서

기본 실행 순서는 아래와 같습니다.

make site/inventory/render SITE=tirosh-home
make validate SITE=tirosh-home

make rke2/server/install SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true
make rke2/agent/install SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true
make rke2/status SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

make rke2/kubeconfig/merge SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true
make rke2/gpu/operator/apply SITE=tirosh-home
make rke2/gpu/benchmark SITE=tirosh-home

1-4. Ready와 GPU resource의 차이

RKE2 node가 Ready가 되는 것과 GPU가 nvidia.com/gpu resource로 보이는 것은 다른 단계입니다. node가 Ready인데 GPU가 n/a라면 RKE2 join은 성공했고 GPU driver/runtime/device plugin 계층을 확인해야 합니다.

2. site 설정과 inventory

RKE2 관련 설정도 실행 기본값, host catalog, cluster 변수로 나눕니다.

sites/tirosh-home/
  profile.toml             Make target이 사용할 RKE2 cluster profile
  inventory.toml           사람이 관리하는 host catalog source
  inventory.yml            inventory.toml에서 렌더링한 Ansible inventory
  rke2/
    vars.yml               RKE2 cluster 공통 설정
    gpu-operator.helmchart.yaml
    nbody-gpu-benchmark.yaml

2-1. profile 기본값

profile.tomlrke2.default_cluster는 기본 cluster profile 이름입니다. 현재 기본값은 gpu입니다.

[rke2]
default_cluster = "gpu"

[rke2.gpu]
vars = "sites/tirosh-home/rke2/vars.yml"
gpu_operator_manifest = "sites/tirosh-home/rke2/gpu-operator.helmchart.yaml"
gpu_benchmark_manifest = "sites/tirosh-home/rke2/nbody-gpu-benchmark.yaml"
targets = ["rke2"]
server_targets = ["rke2_servers"]
agent_targets = ["rke2_agents"]
context_name = "tirosh-home/rke2/gpu"
kubeconfig_path = "~/.kube/tirosh-home-rke2.yml"

Make target은 이 값을 읽어서 Ansible --limit, kubeconfig path, Kubernetes context, GPU Operator manifest 경로로 변환합니다.

다른 cluster profile을 쓰려면 실행 시 지정합니다.

make rke2/status SITE=tirosh-home RKE2_CLUSTER=gpu

2-2. inventory 역할

host 자체의 SSH 정보와 RKE2 node 역할은 inventory.toml에 둡니다.

[baremetals.gpu-01]
ansible_host = "172.31.0.20"
ansible_user = "tirosh"
ansible_become_user = "root"
rke2_node_labels = [
  "node.tirosh.dev/role=gpu",
  "node.tirosh.dev/site=tirosh-home",
  "node.tirosh.dev/model=dgx-spark",
]
workloads = ["gpu"]
rke2 = { cluster = "gpu", role = "server" }

[baremetals.gpu-02]
ansible_host = "172.31.0.21"
ansible_user = "tirosh"
ansible_become_user = "root"
rke2_node_labels = [
  "node.tirosh.dev/role=gpu",
  "node.tirosh.dev/site=tirosh-home",
  "node.tirosh.dev/model=dgx-spark",
]
workloads = ["gpu"]
rke2 = { cluster = "gpu", role = "agent" }

workloads = ["gpu"]는 host를 gpu_nodes group에 넣습니다. rke2.role = "server"rke2_servers, rke2.role = "agent"rke2_agents group에 넣습니다.

수정 후 Ansible inventory를 다시 렌더링합니다.

make site/inventory/render SITE=tirosh-home

렌더링 후 inventory.yml에는 아래 group이 보여야 합니다.

rke2_servers:
  hosts:
    gpu-01: {}
rke2_agents:
  hosts:
    gpu-02: {}
gpu_nodes:
  hosts:
    gpu-01: {}
    gpu-02: {}
rke2:
  children:
    rke2_servers: {}
    rke2_agents: {}

2-3. static IP 전제

RKE2 server/agent join은 node IP가 바뀌면 운영이 불안정해집니다. 각 host의 static IP를 먼저 적용하고, inventory.tomlansible_host가 그 IP를 가리키게 맞춥니다.

static IP 적용 방법은 Host Network를 참고합니다.

3. RKE2 cluster 설정

RKE2 cluster 공통 설정은 sites/<site_id>/rke2/vars.yml에서 관리합니다.

rke2_cluster_config:
  write-kubeconfig-mode: "0600"
  cni: cilium
  tls-san:
    - gpu-01
    - 172.31.0.20

3-1. CNI

현재 초기 설정은 Cilium을 bundled CNI로 선택합니다. Hubble, kube-proxy replacement, WireGuard 같은 고급 기능은 cluster bootstrap 이후 별도 단계에서 켭니다.

RKE2 설치 전에 CNI 방향을 정해야 합니다. 운영 중 CNI 변경은 cluster networking 전체에 영향을 주므로 별도 migration 작업으로 다룹니다.

3-2. TLS SAN

tls-san에는 Kubernetes API에 접근할 주소를 넣습니다. 내부망과 WireGuard로 172.31.0.0/24 대역에 접근할 수 있다면 내부 IP 중심으로 두는 편이 단순합니다.

외부 DNS나 VIP로 API에 접근할 계획이 있으면 해당 이름을 tls-san에 추가합니다. kubeconfig의 server 주소가 인증서 SAN에 없으면 TLS 검증 오류가 발생합니다.

3-3. node label

node별 label은 inventory host 변수로 둡니다.

rke2_node_labels = [
  "node.tirosh.dev/role=gpu",
  "node.tirosh.dev/site=tirosh-home",
  "node.tirosh.dev/model=dgx-spark",
]

장비 모델명은 Kubernetes node label로 관리하고, 운영 alias는 gpu-01처럼 제조사에 덜 묶인 이름을 사용합니다.

4. RKE2 설치

RKE2 server와 agent는 같은 install playbook을 사용하지만, Make target에서 Ansible limit을 다르게 넘깁니다.

4-1. server 설치

첫 server node 또는 server group을 설치합니다.

make rke2/server/install SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

특정 server만 설치할 수도 있습니다.

make rke2/server/install SITE=tirosh-home RKE2_SERVER_TARGETS=gpu-01 ASK_PASS=true ASK_BECOME_PASS=true

server 설치 후 상태를 확인합니다.

make rke2/status SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

4-2. agent 설치

agent node가 준비되면 agent group을 join시킵니다.

make rke2/agent/install SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

특정 agent 몇 대만 추가할 때는 target을 좁힙니다.

make rke2/agent/install SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-02,gpu-03 ASK_PASS=true ASK_BECOME_PASS=true

agent 설치는 기존 cluster에 join해야 하므로 server 쪽의 node token을 먼저 가져와 join config를 만듭니다.

make rke2/agent/install
  |
  v
inventory.yml에서 rke2_servers / rke2_agents group 확인
  |
  v
rke2_servers의 첫 번째 host를 bootstrap server로 선택
  |
  v
bootstrap server에서 node token 읽기
  /var/lib/rancher/rke2/server/node-token
  |
  v
agent host에 /etc/rancher/rke2/config.yaml 작성
  server: https://<bootstrap-server-ip>:9345
  token: <node-token>
  node-name: <inventory host name>
  node-ip: <ansible_host>
  |
  v
rke2-agent 설치 및 service start

gpu-02, gpu-03을 agent로 설치하려면 gpu-01 같은 RKE2 server가 먼저 정상 설치되어 있어야 합니다.

4-3. 설치 후 상태 확인

Ansible 기준 상태를 확인합니다.

make rke2/status SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

로컬 kubeconfig merge 후에는 Kubernetes API 기준으로 확인합니다.

kubectl --context tirosh-home/rke2/gpu get nodes -o wide
kubectl --context tirosh-home/rke2/gpu get pods -A -o wide

5. kubeconfig와 context

RKE2 설치 직후 kubeconfig는 remote host에만 있습니다. 로컬 kubectl, k9s, Argo CD CLI가 이 cluster를 보려면 kubeconfig를 가져와 ~/.kube/config에 merge해야 합니다.

RKE2 server 설치
  |
  v
remote kubeconfig 생성
  /etc/rancher/rke2/rke2.yaml
  |
  v
local kubeconfig로 fetch
  ~/.kube/tirosh-home-rke2.yml
  |
  v
main kubeconfig에 merge
  ~/.kube/config
  |
  v
context 이름으로 접근
  tirosh-home/rke2/gpu

5-1. kubeconfig merge

설치 후 아래 명령을 실행합니다.

make rke2/kubeconfig/merge SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

merge가 끝나면 context가 보이는지 확인합니다.

kubectl config get-contexts
kubectl --context tirosh-home/rke2/gpu get nodes

5-2. context를 사용하는 명령

아래 target과 도구는 모두 profile.tomlrke2.gpu.context_name을 사용합니다.

  • make rke2/gpu/operator/apply
  • make rke2/gpu/benchmark
  • make argocd/cluster/add
  • k9s --context tirosh-home/rke2/gpu

kubeconfig와 context의 자세한 배경은 k9s를 참고합니다.

6. agent 추가 시나리오

이미 gpu-01 server가 동작 중인 RKE2 cluster에 gpu-02, gpu-03을 agent로 추가하는 상황을 예로 듭니다.

6-1. host catalog 수정

inventory.toml에 새 host가 agent 역할로 선언되어 있어야 합니다.

[baremetals.gpu-02]
ansible_host = "172.31.0.21"
ansible_user = "tirosh"
ansible_become_user = "root"
workloads = ["gpu"]
rke2 = { cluster = "gpu", role = "agent" }

[baremetals.gpu-03]
ansible_host = "172.31.0.22"
ansible_user = "tirosh"
ansible_become_user = "root"
workloads = ["gpu"]
rke2 = { cluster = "gpu", role = "agent" }

6-2. inventory 렌더링과 검증

inventory를 렌더링하고 site 설정을 검증합니다.

make site/inventory/render SITE=tirosh-home
make validate SITE=tirosh-home

server가 먼저 정상인지 확인합니다. agent join은 bootstrap server에서 token을 읽어오므로, server가 준비되지 않았으면 여기서 먼저 해결해야 합니다.

make rke2/status SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

6-3. agent만 설치

gpu-02, gpu-03만 agent로 설치합니다.

make rke2/agent/install SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-02,gpu-03 ASK_PASS=true ASK_BECOME_PASS=true

설치가 끝나면 Kubernetes node 목록에서 agent가 Ready 상태인지 확인합니다.

kubectl --context tirosh-home/rke2/gpu get nodes -o wide

일부 agent만 실패하면 성공한 host는 그대로 두고 실패한 host만 다시 실행합니다.

make rke2/agent/install SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-03 ASK_PASS=true ASK_BECOME_PASS=true

7. RKE2 제거와 역할 전환

server와 agent 제거는 설치와 마찬가지로 target을 좁혀서 실행합니다.

7-1. agent 제거

agent node를 제거할 때는 대상만 좁혀서 실행합니다. 기본 동작은 Kubernetes node drain, RKE2 service 중지, RKE2 uninstall script 실행, Kubernetes node object 삭제입니다.

make rke2/agent/uninstall SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-02 ASK_PASS=true ASK_BECOME_PASS=true

7-2. server 제거

server node 제거도 같은 방식으로 실행합니다. 다만 bootstrap server와 마지막 server 제거는 기본적으로 차단됩니다.

make rke2/server/uninstall SITE=tirosh-home RKE2_SERVER_TARGETS=gpu-03 ASK_PASS=true ASK_BECOME_PASS=true

운영 중인 server 제거는 quorum과 API endpoint를 먼저 확인해야 합니다. 의도적으로 bootstrap server나 마지막 server를 제거해야 하는 경우에만 guard를 해제합니다.

make rke2/server/uninstall SITE=tirosh-home RKE2_SERVER_TARGETS=gpu-01 \
  ASK_PASS=true ASK_BECOME_PASS=true \
  ANSIBLE_ARGS='--extra-vars rke2_uninstall_force_bootstrap_server=true'

drain이나 Kubernetes node object 삭제를 건너뛰어야 하는 복구 상황에서는 아래처럼 override할 수 있습니다.

make rke2/agent/uninstall SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-02 \
  ASK_PASS=true ASK_BECOME_PASS=true \
  ANSIBLE_ARGS='--extra-vars rke2_uninstall_drain_node=false --extra-vars rke2_uninstall_delete_node=false'

7-3. agent를 server로 전환

agent를 server로 전환할 때는 agent를 제거한 뒤 inventory.toml에서 rke2.roleserver로 바꾸고 inventory를 다시 렌더링한 다음 server install을 실행합니다.

make rke2/agent/uninstall SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-02 ASK_PASS=true ASK_BECOME_PASS=true
make site/inventory/render SITE=tirosh-home
make rke2/server/install SITE=tirosh-home RKE2_SERVER_TARGETS=gpu-02 ASK_PASS=true ASK_BECOME_PASS=true

8. GPU 구성

Kubernetes node가 Ready여도 GPU가 바로 보이는 것은 아닙니다. GPU/A, GPU/Cn/a로 보이면 Kubernetes가 아직 nvidia.com/gpu resource를 알지 못한다는 뜻입니다.

RKE2 bootstrap과 GPU stack은 서로 다른 계층입니다. RKE2는 Kubernetes control plane, kubelet, containerd, CNI를 준비하고, GPU stack은 host driver와 Kubernetes device plugin을 통해 GPU를 allocatable resource로 노출합니다.

host GPU 장착
  |
  v
NVIDIA kernel driver / libnvidia-ml
  |
  v
NVIDIA Container Toolkit / CDI / NRI
  |
  v
NVIDIA GPU Operator / device plugin
  |
  v
node.status.capacity["nvidia.com/gpu"]
  |
  v
workload resources.limits["nvidia.com/gpu"]

공식 RKE2 문서도 GPU Operator를 별도 add-on으로 다룹니다. NVIDIA 문서 기준으로 GPU Operator는 driver, container toolkit, device plugin, GPU Feature Discovery, DCGM exporter 같은 구성요소를 함께 관리합니다.

8-1. 현재 증상 해석

아래처럼 node는 Ready인데 GPU column이 n/a로 보이면 RKE2 node join은 성공했지만 GPU 계층은 아직 구성되지 않은 상태입니다.

NAME     STATUS   ROLE                  GPU/A   GPU/C
gpu-01   Ready    control-plane,etcd     n/a     n/a
gpu-02   Ready    <none>                 n/a     n/a
gpu-03   Ready    <none>                 n/a     n/a

이 상태에서 먼저 host 레벨을 확인합니다.

ssh tirosh@172.31.0.20 'lspci | grep -i nvidia || true'
ssh tirosh@172.31.0.20 'lsmod | grep nvidia || true'
ssh tirosh@172.31.0.20 'cat /proc/driver/nvidia/version || true'
ssh tirosh@172.31.0.20 'find /usr -iname libnvidia-ml.so 2>/dev/null | head'
ssh tirosh@172.31.0.20 'nvidia-smi || true'

lspci에는 GPU가 보이는데 lsmod, /proc/driver/nvidia/version, nvidia-smi가 실패하면 host driver가 아직 준비되지 않은 것입니다. 이 경우 Kubernetes 안에서 아무리 확인해도 nvidia.com/gpu가 나오지 않습니다.

8-2. GPU Operator 적용

이 repo에서는 RKE2 설치 뒤 NVIDIA GPU Operator를 cluster add-on으로 적용합니다.

make rke2/gpu/operator/apply SITE=tirosh-home

이 target은 profile.tomlrke2.<cluster>.gpu_operator_manifest, rke2.<cluster>.context_name, kube.kubeconfig를 사용합니다. 기본값 기준으로는 ~/.kube/configtirosh-home/rke2/gpu context에 GPU Operator manifest를 적용합니다.

따라서 먼저 kubeconfig merge가 완료되어 있어야 합니다.

make rke2/kubeconfig/merge SITE=tirosh-home

현재 manifest는 RKE2의 HelmChart resource를 사용합니다.

apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
  name: gpu-operator
  namespace: kube-system
spec:
  repo: https://helm.ngc.nvidia.com/nvidia
  chart: gpu-operator
  version: v26.3.2
  targetNamespace: gpu-operator
  createNamespace: true

초기값은 driver.enabled=true로 둡니다. 즉 host에 호환 가능한 NVIDIA driver가 없으면 GPU Operator가 driver 설치를 시도하는 모델입니다. 이미 검증된 host driver를 image나 수동 설치로 관리하는 운영 모델이라면 driver.enabled=false로 바꾸고 적용합니다.

RKE2 v1.35 계열에서는 최신 containerd를 사용하므로, manifest는 CDI/NRI 경로를 우선 사용합니다.

cdi:
  nriPluginEnabled: true

GPU Operator는 container runtime이나 driver 계층을 건드릴 수 있습니다. RKE2 문서도 NVIDIA Operator가 containerd에 hangup을 보내 RKE2가 재시작될 수 있다고 안내합니다. 병원 현장에서는 업무 시간, workload drain, rollback 계획을 잡고 적용합니다.

8-3. GPU resource 검증

Operator pod 상태를 확인합니다.

kubectl --context tirosh-home/rke2/gpu get pods -n gpu-operator -o wide

node에 NVIDIA label이 붙었는지 확인합니다.

kubectl --context tirosh-home/rke2/gpu get node gpu-01 -o jsonpath='{.metadata.labels}' | grep nvidia.com

GPU가 allocatable resource로 올라왔는지 확인합니다.

kubectl --context tirosh-home/rke2/gpu get node gpu-01 -o jsonpath='{.status.allocatable}' | grep nvidia.com/gpu
kubectl --context tirosh-home/rke2/gpu describe node gpu-01 | grep -A8 -E 'Capacity|Allocatable|nvidia.com/gpu'

정상이라면 nvidia.com/gpu가 capacity와 allocatable에 나타납니다. 이 값이 보여야 GPU workload가 아래처럼 resource limit을 요청할 수 있습니다.

resources:
  limits:
    nvidia.com/gpu: 1

8-4. runtime validation

마지막으로 CUDA container를 실행해 실제 container에서 GPU가 잡히는지 확인합니다.

make rke2/gpu/benchmark SITE=tirosh-home

현재 검증 pod는 ARM64 node에서 CUDA base image의 nvidia-smi를 실행하고, 실행 후 자동으로 삭제됩니다. DGX Spark는 ARM64 기반이므로 x86_64로 빌드된 CUDA sample image를 사용하면 exec format error가 발생할 수 있습니다.

정상 출력 예시는 아래와 같습니다.

NVIDIA-SMI 580.159.03
Driver Version: 580.159.03
CUDA Version: 13.3
GPU  Name
0    NVIDIA GB10

9. 문제 확인

문제가 생기면 RKE2 계층과 GPU 계층을 분리해서 봅니다.

9-1. agent join 실패

아래 단계에서 실패하면 playbook은 그 지점에서 중단되고 이후 작업으로 넘어가지 않습니다.

실패 지점 대표 원인 결과
inventory 검증 rke2_servers group이 없거나 비어 있음 bootstrap server를 고르지 못해 중단
bootstrap server SSH 접속 server host IP, SSH 계정, password, key 문제 token을 읽기 전에 중단
node token 읽기 server가 아직 설치되지 않았거나 /var/lib/rancher/rke2/server/node-token이 없음 agent config를 만들기 전에 중단
agent config 작성 sudo 권한, filesystem 문제 service 시작 전에 중단
rke2-agent service 시작 container runtime, CNI, kernel module, network 문제 설치 후 readiness 확인 전후로 실패
readiness 확인 agent가 cluster에 join하지 못함 kubectl wait 단계에서 실패

token을 못 받아오는 경우에는 agent host에 join config를 쓰기 전에 멈추는 것이 정상입니다. 이 상태에서는 먼저 server 상태를 확인합니다.

make rke2/status SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

실패 지점에 따라 agent host에 일부 파일이나 service가 남을 수 있습니다. 상태가 애매하면 agent를 제거한 뒤 다시 설치합니다.

make rke2/agent/uninstall SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-02 ASK_PASS=true ASK_BECOME_PASS=true
make rke2/agent/install SITE=tirosh-home RKE2_AGENT_TARGETS=gpu-02 ASK_PASS=true ASK_BECOME_PASS=true

9-2. kubeconfig 문제

kubectl --context tirosh-home/rke2/gpu get nodes가 실패하면 먼저 context가 ~/.kube/config에 있는지 확인합니다.

kubectl config get-contexts
make rke2/kubeconfig/merge SITE=tirosh-home ASK_PASS=true ASK_BECOME_PASS=true

kubeconfig의 server 주소가 접근 가능한 IP/DNS인지, 그리고 그 주소가 tls-san에 포함되어 있는지도 확인합니다.

9-3. GPU가 n/a로 보이는 경우

node가 Ready인데 nvidia.com/gpu가 없으면 아래 순서로 확인합니다.

kubectl --context tirosh-home/rke2/gpu get pods -n gpu-operator -o wide
kubectl --context tirosh-home/rke2/gpu describe node gpu-01 | grep -A8 -E 'Capacity|Allocatable|nvidia.com/gpu'
make rke2/gpu/benchmark SITE=tirosh-home

host driver가 없거나 GPU Operator pod가 실패하면 RKE2 재설치보다 GPU Operator manifest와 host driver 상태를 먼저 확인합니다.

10. 다음 단계

10-1. Argo CD 등록

GPU resource가 Kubernetes에 정상 노출되면 Argo CD에 workload cluster를 등록하고 application 배포를 시작합니다.

make argocd/cluster/add SITE=tirosh-home

10-2. 운영 확인

운영 중 cluster를 볼 때는 k9s를 사용합니다.

k9s --context tirosh-home/rke2/gpu