---
title: "온프레미스 GPU 서버에 Kubeflow를 설치하여 ML 워크플로를 자동화하는 방법"
description: "온프레미스 GPU 서버에 Kind 기반 싱글 노드 클러스터를 만들고, NVIDIA Container Toolkit과 kustomize 등을 사전 설치한 뒤, 인otify 제한을 늘려 Kubeflow를 설치하고 사용자 계정과 프로필을 설정하는 전체 절차와 주요 트러블슈팅 팁을 안내한다."
date: "2024-11-22"
last_modified: "2026-05-15T08:36:00.000Z"
type: "Post"
tags:
  - "docker"
  - "gpu"
  - "troubleshooting"
categories:
  - "🤖 Computer Science"
series:
  - "k8s"
canonical_url: "https://blog.pieroot.xyz/kubeflow-onprem-gpu"
markdown_url: "https://blog.pieroot.xyz/kubeflow-onprem-gpu.md"
---

# 온프레미스 GPU 서버에 Kubeflow를 설치하여 ML 워크플로를 자동화하는 방법

온프레미스 GPU 서버에 Kind 기반 싱글 노드 클러스터를 만들고, NVIDIA Container Toolkit과 kustomize 등을 사전 설치한 뒤, 인otify 제한을 늘려 Kubeflow를 설치하고 사용자 계정과 프로필을 설정하는 전체 절차와 주요 트러블슈팅 팁을 안내한다.

ML 워크플로를 자동화하고 싶은데, 클라우드는 비싸고… 그래서 연구실에 굴러다니는 GPU 서버에 **Kubeflow**를 올리기로 했다.

Kubeflow는 Kubernetes 위에서 돌아가는 **ML 워크플로 플랫폼**이다. Jupyter Notebook, Pipeline, 모델 서빙(KServe) 등 ML에 필요한 거의 모든 도구를 한 번에 제공한다. 근데 이걸 온프레미스에 올리는 건 솔직히 말해서 ~~지옥의 난이도~~다. 공식 문서가 친절하지 않고, 의존성도 복잡하고, 에러 메시지도 불친절하다.

이 글에서는 **Kind(Kubernetes in Docker)** 기반으로 싱글 노드 GPU 서버에 Kubeflow를 설치하는 전체 과정을 정리한다. 삽질한 내용도 같이 담았으니 참고하길 바란다.

> **이 글에서 다루는 내용**
> 
> - 왜 Kubeflow인가, 왜 Kind인가
> 
> - 사전 설치 (NVIDIA Container Toolkit, kustomize)
> 
> - Docker, containerd, Kind, kubectl은 이전 글 참조
> 
> - Kubeflow 설치 및 사용자 생성
> 
> - 트러블슈팅 (포트 충돌, 리소스 제한, 쿠키 문제 등)

---

## 🖥️ 서버 스펙

> CPU : Ryzen 9 5950X - 16core 32thread
> RAM : 64GB
> GPU : RTX 3060 12GB

16코어에 램 64GB면 Kubeflow를 싱글 노드로 돌리기에 나쁘지 않은 스펙이다. GPU는 3060 12GB인데, Jupyter Notebook에서 간단한 학습 돌리기엔 충분하다.

> **Kubeflow는 리소스를 상당히 많이 먹는다.** 모든 컴포넌트를 올리면 아이들 상태에서도 메모리 20GB 이상을 사용한다. RAM이 32GB 이하라면 일부 컴포넌트(Katib, KServe 등)를 비활성화하는 것을 권장한다.

---

## 🤔 왜 Kind인가?

> Kind(Kubernetes in Docker)는 Docker 컨테이너를 노드로 사용하여 로컬 Kubernetes 클러스터를 생성하는 도구다.

Kubernetes를 로컬에 올리는 방법은 여러 가지가 있다:

Kind를 선택한 이유는 간단하다:

- Docker만 있으면 바로 클러스터를 만들 수 있다

- 클러스터 생성/삭제가 빠르다 (망하면 밀고 다시 만들면 됨)

- Kubeflow 공식 매니페스트에서 Kind를 지원한다

~~솔직히 kubeadm으로 하다가 3번 정도 포맷하고 Kind로 갈아탔다~~

---

## 🔧 사전 설치

---

### 1. Docker, containerd, Kind, kubectl 설치

Docker, containerd, Kind, kubeadm, kubectl 설치는 이전 글에서 이미 다뤘으므로 여기서는 생략한다. 아직 설치하지 않았다면 아래 글을 먼저 참고하자.

> **이전 글 참조**: [Kubernetes 클러스터 구축: kubeadm으로 쉽게 시작하는 가이드](https://app.notion.com/p/30d067c015d0801cae2dd2676f60a1c7)
> 
> 위 글에서 **Docker 설치**, **containerd 설정** (cgroup을 systemd로 변경), **Kind 설치**, **kubeadm/kubectl 설치**까지 전부 다루고 있다. 이 글에서는 해당 과정이 완료된 상태를 전제로 진행한다.

> **Kubeflow에서 특히 중요한 포인트**
> 
> - `daemon.json`에서 `native.cgroupdriver=systemd` 설정이 되어 있어야 한다
> 
> - containerd의 `SystemdCgroup = true` 설정도 빠짐없이 확인하자
> 
> - Kind와 kubectl의 Kubernetes 버전이 크게 차이나지 않는지 확인 (`v1.29.x` ↔ `v1.30.x` 정도는 호환)
> 
> 이 세 가지 중 하나라도 빠지면 나중에 파드가 `CrashLoopBackOff`에 빠지거나 노드가 `NotReady`로 표시되는 등 ~~원인을 찾기 어려운~~ 문제가 발생할 수 있다.

---

### 2. NVIDIA Container Toolkit 설치

Docker에서 GPU를 사용하려면 **NVIDIA Container Toolkit**이 필수다. 이게 없으면 컨테이너 안에서 GPU를 인식하지 못한다.

> **전제 조건**: NVIDIA GPU 드라이버가 이미 설치되어 있어야 한다. `nvidia-smi` 명령어로 드라이버가 정상 동작하는지 먼저 확인하자.

```shell
# NVIDIA Container Toolkit 저장소 추가
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
```

#### Docker daemon 설정

NVIDIA runtime을 Docker의 기본 런타임으로 설정해야 한다. `/etc/docker/daemon.json` 파일을 아래와 같이 수정한다:

```json
{
    "exec-opts": ["native.cgroupdriver=systemd"],
    "default-runtime": "nvidia",
    "runtimes": {
        "nvidia": {
            "args": [],
            "path": "nvidia-container-runtime"
        }
    }
}
```

> **설정 해설**
> 
> - `native.cgroupdriver=systemd`: Kubernetes와 cgroup 드라이버를 일치시킨다. 이게 다르면 kubelet이 제대로 동작하지 않는다
> 
> - `default-runtime: nvidia`: 모든 컨테이너에서 기본적으로 NVIDIA GPU에 접근 가능하게 한다
> 
> - `runtimes.nvidia`: NVIDIA Container Runtime의 경로를 명시적으로 지정한다

설정 적용 후 Docker를 재시작한다:

```shell
sudo systemctl restart docker
```

#### GPU 인식 확인

```shell
docker run --rm --gpus all nvidia/cuda:12.0.0-base-ubuntu22.04 nvidia-smi
```

컨테이너 안에서 `nvidia-smi` 출력이 나오면 성공이다. 여기서 GPU 정보가 안 보인다면 드라이버 설치부터 다시 확인해야 한다.

---

### 3. kustomize 설치

Kubeflow 매니페스트 빌드에 필요한 패키지다. 빼먹으면 설치가 진행되지 않으니 꼭 설치해준다.

```shell
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash

sudo mv kustomize /usr/local/bin/
```

설치 확인:

```shell
kustomize version
```

---

## 🚀 Kubeflow 설치

---

### 시스템 리소스 제한 해제

Kubeflow는 수십 개의 파드를 동시에 올리기 때문에, 기본 시스템 리소스 제한으로는 부족하다. 설치 전에 반드시 올려주자.

```shell
# 즉시 적용
sudo sysctl fs.inotify.max_user_instances=2280
sudo sysctl fs.inotify.max_user_watches=1255360
```

> **inotify란?** 리눅스에서 파일 시스템 이벤트를 감시하는 메커니즘이다. Kubernetes는 ConfigMap, Secret 등의 변경을 감지하기 위해 inotify를 사용하는데, Kubeflow처럼 리소스가 많은 환경에서는 기본값으로는 턱없이 부족하다.
> 
> - `max_user_instances`: 사용자당 inotify 인스턴스 최대 수 (기본 128 → 2280)
> 
> - `max_user_watches`: 사용자당 감시할 수 있는 파일 최대 수 (기본 8192 → 1255360)

재부팅 후에도 유지하려면:

```shell
echo fs.inotify.max_user_watches=1255360 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
echo fs.inotify.max_user_instances=2280 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
```

---

### Kind 클러스터 생성

작업 디렉토리를 만들고 Kubeflow 매니페스트를 클론한다.

```shell
cd ~/
mkdir kubeflow && cd ~/kubeflow

git clone https://github.com/kubeflow/manifests.git && cd manifests
```

Kind 클러스터를 생성한다. 여기서 중요한 건 **`service-account-issuer`****와 ****`service-account-signing-key-file`** 설정이다. Kubeflow의 Istio가 이 설정을 요구하기 때문에 반드시 포함해야 한다.

```yaml
cat <<EOF | kind create cluster --name=kubeflow --kubeconfig mycluster.yaml --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  image: kindest/node:v1.29.4
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        "service-account-issuer": "kubernetes.default.svc"
        "service-account-signing-key-file": "/etc/kubernetes/pki/sa.key"
EOF
```

> **노드 이미지 버전에 주의!** Kind 노드 이미지(`kindest/node`)의 Kubernetes 버전과 설치된 `kubectl` 버전이 크게 차이나면 호환성 문제가 발생할 수 있다. `v1.29.x`와 `v1.30.x`는 대체로 호환되지만, 가급적 맞추는 것을 권장한다.

kubeconfig를 설정한다:

```shell
mkdir -p ~/.kube
kind get kubeconfig --name kubeflow > ~/.kube/config
```

---

### Docker 레지스트리 인증

Kubeflow 이미지 중 일부는 Docker Hub에서 pull하는데, 인증 없이는 rate limit에 걸릴 수 있다. Docker 로그인 후 Secret을 생성해둔다.

```shell
docker login
# Docker Hub 계정으로 로그인

kubectl create secret generic regcred \
    --from-file=.dockerconfigjson=$(echo $HOME)/.docker/config.json \
    --type=kubernetes.io/dockerconfigjson
```

---

### Kubeflow 매니페스트 빌드 & 배포

이제 진짜 설치를 시작한다. 아래 명령어는 **모든 컴포넌트를 한 번에 설치**하는 방법이다.

```shell
while ! kustomize build example | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 20; done
```

> **이 명령어가 하는 일**
> 
> 1. `kustomize build example`: Kubeflow의 모든 매니페스트를 하나의 YAML로 빌드
> 
> 1. `kubectl apply -f -`: 빌드된 YAML을 클러스터에 적용
> 
> 1. `while ! ... done`: 실패하면 20초 후 재시도 (의존성 순서 문제로 첫 시도에서 일부 실패가 정상)
> 
> 처음 실행하면 **CRD(Custom Resource Definition)**가 아직 생성되지 않아서 에러가 나는 게 정상이다. 반복 실행하면 의존성이 순서대로 해결되면서 결국 전부 올라간다. ~~인내심 테스트 같은 느낌이지만 참자~~

> **설치 시간**: 서버 스펙과 네트워크 환경에 따라 다르지만, 보통 **15~30분** 정도 걸린다. 이미지 pull 시간이 대부분이다. 설치 중간에 kserve webhook 관련 에러가 발생할 수 있는데, 이건 IP 접근 문제로 반복 시도하면 다른 IP로 접근해서 해결되는 경우가 많다.

#### 개별 설치 (선택사항)

한 번에 설치하는 게 불편하다면, 컴포넌트별로 따로 설치할 수도 있다. 특히 **kserve**와 **katib**은 webhook 이슈가 잦아서 분리 설치를 추천한다.

```shell
# 예: Istio 먼저 설치
kustomize build common/istio-1-22/istio-crds/base | kubectl apply -f -
kustomize build common/istio-1-22/istio-namespace/base | kubectl apply -f -
kustomize build common/istio-1-22/istio-install/overlays/oauth2-proxy | kubectl apply -f -

# Dex (인증)
kustomize build common/dex/overlays/oauth2-proxy | kubectl apply -f -

# Kubeflow Namespace & Roles
kustomize build common/kubeflow-namespace/base | kubectl apply -f -
kustomize build common/kubeflow-roles/base | kubectl apply -f -

# 이후 필요한 컴포넌트만 선택적으로 설치
```

#### 설치 완료 확인

모든 파드가 정상적으로 올라왔는지 확인한다:

```shell
kubectl get pods -A | grep -v Running | grep -v Completed
```

위 명령어에 아무것도 안 나오면 (헤더 제외) 모든 파드가 정상 동작 중이다. 🎉

---

### 대시보드 접속

Kubeflow 대시보드에 접근하려면 포트 포워딩이 필요하다:

```shell
kubectl port-forward svc/istio-ingressgateway -n istio-system 8080:80
```

브라우저에서 `http://localhost:8080`으로 접속하면 로그인 화면이 나온다.

기본 계정 정보:

- **Email**: `user@example.com`

- **Password**: `12341234`

---

## 👤 사용자 생성

기본 계정 말고 별도의 계정을 만들어서 사용하고 싶다면 **Dex** 설정을 수정해야 한다.

### ConfigMap 수정

`manifests/common/dex/overlays/oauth2-proxy/config-map.yaml` 파일을 열어서 `staticPasswords` 섹션에 계정을 추가한다:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dex
data:
  config.yaml: |
    issuer: http://dex.auth.svc.cluster.local:5556/dex
    storage:
      type: kubernetes
      config:
        inCluster: true
    web:
      http: 0.0.0.0:5556
    logger:
      level: "debug"
      format: text
    oauth2:
      skipApprovalScreen: true
    enablePasswordDB: true
    staticPasswords:
    - email: user@example.com
      hashFromEnv: DEX_USER_PASSWORD
      username: user
      userID: "15841185641784"
    - email: dmslab@email.com
      hash: $2y$12$.RUSNnqk3G.2pUF;:RVkDC9.HM7ZXwR8n.n423Yo.OAzXIYTRCp9Jpm
      username: dmslab
      userID: "dmslab"
    staticClients:
    - idEnv: OIDC_CLIENT_ID
      redirectURIs: ["/oauth2/callback"]
      name: 'Dex Login Application'
      secretEnv: OIDC_CLIENT_SECRET
```

> **hash vs hashFromEnv**
> 
> - `hashFromEnv`: 환경 변수에서 해시값을 가져온다. 보안이 중요한 환경에서 권장
> 
> - `hash`: YAML에 직접 bcrypt 해시값을 넣는다. 보안을 신경 쓰지 않는 개발/테스트 환경에서 편리
> 
> bcrypt 해시 생성은 아래 명령어로 가능하다:
> 
> `python3 -c "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"`

파일 수정 후 적용한다:

```shell
# 방법 1: kustomize로 전체 재빌드
kustomize build common/dex/overlays/oauth2-proxy | kubectl apply -f -

# 방법 2: ConfigMap만 직접 적용
kubectl apply -f common/dex/overlays/oauth2-proxy/config-map.yaml
```

---

### 네임스페이스 생성

계정을 만들었다고 끝이 아니다. **네임스페이스(Profile)**를 생성해줘야 로그인 후 실제로 뭔가를 할 수 있다. 네임스페이스가 없으면 로그인은 되지만 빈 화면만 보게 된다.

`my-profile.yaml` 파일을 생성한다:

```yaml
apiVersion: kubeflow.org/v1beta1
kind: Profile
metadata:
  name: dmslab
spec:
  owner:
    kind: User
    name: dmslab@email.com
```
*my-profile.yaml*

> `metadata.name`은 **Kubernetes 네임스페이스 이름 규칙**을 따라야 한다. 소문자, 숫자, 하이픈만 사용 가능하고, 63자를 넘을 수 없다. `spec.owner.name`은 Dex에 등록한 이메일과 **정확히 일치**해야 한다.

적용:

```shell
kubectl apply -f my-profile.yaml
```

---

### Dex 재시작

설정이 끝났으면 Dex를 재시작하여 변경사항을 반영한다:

```shell
kubectl rollout restart deploy dex -n auth
```

재시작 후 새 계정으로 로그인이 되는지 확인하자.

---

## 🔥 트러블슈팅

---

#### 1. MySQL 포트 충돌

온프레미스에 MySQL이 이미 올라간 상태에서 Kubeflow를 설치하면, Kubeflow 내부의 MySQL과 **3306 포트가 충돌**한다.

> Kubeflow 내부 MySQL의 포트를 바꾸는 건 생각보다 더럽게 복잡하다. ConfigMap, Deployment, 그리고 의존하는 서비스들까지 전부 수정해야 한다.

**해결 방법**: 온프레미스 MySQL의 포트를 다른 포트(예: 3307)로 변경하는 게 훨씬 간단하다.

```shell
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
port = 3307

sudo systemctl restart mysql
```

---

#### 2. 리소스 사용량 최대치

Kubeflow의 모든 노드와 리소스를 싱글 서버에 올리면 inotify 리소스가 부족해서 일부 파드(특히 Pipeline, Katib)가 올라오지 못하는 문제가 발생한다.

```shell
# 즉시 적용
sudo sysctl fs.inotify.max_user_instances=2280
sudo sysctl fs.inotify.max_user_watches=1255360

# 영구 적용 (재부팅 후에도 유지)
echo fs.inotify.max_user_watches=1255360 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
echo fs.inotify.max_user_instances=2280 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
```

> `max_user_watches` 값은 메모리에 비례하여 설정하는 게 좋다. 64GB 기준 1255360이 권장값이다. 각 watch는 약 1KB의 메모리를 사용하므로, 이 설정은 약 1.2GB의 메모리를 사용할 수 있다.

---

#### 3. HTTP 환경에서의 Cookie 문제

HTTPS가 아닌 HTTP 환경에서 Kubeflow를 사용하면 **Cookie 보안 정책 때문에 로그인 세션이 유지되지 않는** 문제가 발생한다. 로그인은 되는데 페이지를 이동하면 다시 로그인 화면으로 돌아가는 현상이다. ~~이거 때문에 한 시간 삽질했다~~

**해결 방법**: `jupyter-web-app`의 환경 변수에서 쿠키 보안 설정을 비활성화한다.

```shell
# 환경 변수 수정
JWA_APP_SECURE_COOKIES=true
# ↓ 변경
JWA_APP_SECURE_COOKIES=false
```

> **보안 주의**: 이 설정은 HTTP 환경에서만 사용하자. 프로덕션에서는 반드시 HTTPS를 적용하고 `JWA_APP_SECURE_COOKIES=true`를 유지해야 한다. Secure Cookie가 비활성화되면 세션 하이재킹 등의 보안 위협에 노출될 수 있다.

---

#### 4. Pipeline 사용법

Kubeflow Pipeline은 ML 워크플로를 **DAG(Directed Acyclic Graph)** 형태로 정의하고 실행하는 도구다. Python SDK(`kfp`)를 사용하여 파이프라인을 작성하고, UI에서 실행/모니터링할 수 있다.

간단한 예제를 통해 기본 사용법을 살펴보자:

```python
# pip install kfp
from kfp import dsl, compiler

@dsl.component
def say_hello(name: str) -> str:
    hello_text = f'Hello, {name}!'
    print(hello_text)
    return hello_text

@dsl.component
def print_message(message: str):
    print(message)

@dsl.pipeline(name='hello-pipeline')
def hello_pipeline(recipient: str = 'World'):
    hello_task = say_hello(name=recipient)
    print_message(message=hello_task.output)

# 파이프라인을 YAML로 컴파일
compiler.Compiler().compile(hello_pipeline, 'pipeline.yaml')
```

컴파일된 `pipeline.yaml`을 Kubeflow UI의 **Pipelines → Upload Pipeline**에서 업로드하고, **Create Run**으로 실행할 수 있다.

> **Pipeline 핵심 개념**
> 
> - **Component**: 파이프라인의 각 단계(step). Docker 컨테이너로 실행된다
> 
> - **Pipeline**: Component들을 연결한 DAG(워크플로)
> 
> - **Run**: 파이프라인의 한 번 실행 인스턴스
> 
> - **Experiment**: Run들을 그룹화하는 논리적 단위
> 
> `@dsl.component` 데코레이터를 사용하면 Python 함수를 자동으로 컨테이너화해준다. 별도의 Dockerfile 없이도 컴포넌트를 만들 수 있어서 편리하다.

---

## ✅ 핵심 정리

✅ **Kind**: Docker 컨테이너 기반 로컬 Kubernetes. 빠른 클러스터 생성/삭제가 장점

✅ **NVIDIA Container Toolkit**: Docker에서 GPU를 사용하기 위한 필수 런타임. `daemon.json`에서 기본 런타임으로 설정

✅ **cgroup 드라이버 일치**: Docker, containerd, kubelet 모두 `systemd`로 통일해야 한다

✅ **inotify 제한 해제**: Kubeflow처럼 리소스가 많은 환경에서는 반드시 `max_user_instances`와 `max_user_watches`를 올려야 한다

✅ **kustomize build 반복 실행**: CRD 의존성 때문에 첫 시도에서 실패하는 건 정상. 반복하면 해결된다

✅ **사용자 생성**: Dex ConfigMap에 계정 추가 + Profile(네임스페이스) 생성이 한 세트

✅ **HTTP 환경**: Cookie 보안 설정을 비활성화해야 세션이 유지된다 (개발 환경 한정)

---

## ⚠️ 주의사항

⚠️ **리소스 요구량**: Kubeflow 전체 설치 시 아이들 메모리 20GB 이상 필요. RAM이 부족하면 컴포넌트를 선택적으로 설치하자

⚠️ **Kind 노드 이미지 버전**: kubectl 버전과 크게 차이나면 호환성 문제 발생 가능

⚠️ **Docker Hub Rate Limit**: 인증 없이 이미지를 pull하면 rate limit에 걸릴 수 있다. `docker login` 필수

⚠️ **HTTPS 권장**: 프로덕션에서는 반드시 HTTPS를 적용하고, Cookie 보안 설정을 활성화해야 한다

⚠️ **클러스터 재생성 시**: `kind delete cluster --name kubeflow`로 깔끔하게 삭제 후 다시 시작하는 게 가장 확실한 방법이다

---

## 📚 참고 자료

- [Kubeflow 공식 문서 - Installing Kubeflow](https://www.kubeflow.org/docs/started/installing-kubeflow/)

- [Kubeflow Manifests GitHub](https://github.com/kubeflow/manifests)

- [Kind 공식 문서](https://kind.sigs.k8s.io/)

- [NVIDIA Container Toolkit 설치 가이드](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)

- [Kubeflow Pipelines - Getting Started](https://www.kubeflow.org/docs/components/pipelines/getting-started/)

- [Kubernetes 공식 문서 - kubeadm 설치](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/)
