Kubeflow環境を構築してみた

# はじめに
この記事はラクスパートナーズ Advent Calendar 2024の21日目の記事です。

Kubernetesにまともに触れてこなかったエンジニアが、アドベントカレンダー参加を機にKubeflowの環境構築を行ってみました。

簡単にですが、その手順をまとめていこうと思います。

内容について誤りがございましたら、優しい言葉でご指摘いただけますと幸いです。

# 本記事のゴール
VMインスタンス上にシンプルなKubeflowの環境を構築すること。

# Kubeflowってなに？
Kubernetes上で機械学習（ML）ワークフローを管理するためのオープンソースプラットフォームです。
モデルの開発、学習、デプロイといった MLライフサイクルのプロセスを自動化し、スケーラブルで再現性のある環境を実現することができます。
Kubeflowにおける各コンポーネントが、どのプロセスに対応するかは[公式サイト](https://www.kubeflow.org/docs/started/architecture/#kubeflow-components-in-the-ml-lifecycle)の説明が理解しやすいです。

また、前提となる「MLOps」の考え方については[こちら](https://qiita.com/nokoxxx1212/items/d2c36b0b22ab0da6c759)で非常に分かりやすくまとめてくださっています。


# 環境情報

## マシン情報
今回はアカウント作成時にもらったクレジットが余っていたので、GCP上にVMインスタンスを構築しました。
| 項目　| 値 |
| ---- | ---- |
| OS |  Ubuntu, 24.04 LTS |
|CPUコア数 | 8コア |
|メモリサイズ | 32GB |
|ディスクサイズ | 64GB |
|プロセッサアーキテクチャ | AMD64 | 

## バージョン情報
| 項目　| 値 |
| ---- | ---- |
| Kubernetes | v1.31 |
| Kubeflow | v1.90 |

# 前提
Kubeflow環境を構築するにあたり、個人利用の範疇ということを加味してKubeflow Manifestsを利用することにしました。

https://www.kubeflow.org/docs/started/installing-kubeflow/

また、GitHubの手順では Kind (Kubernetes in Docker) を利用してクラスターを構築していますが、今回はマシンスペックの都合によりパブクラ上のVMに構築するため、外部からの通信設定が比較的行いやすい kubeadm で構築することにしました。

https://github.com/kubeflow/manifests


# 手順

## Swapの無効化
Kubeadmでクラスターを作成する際、Swapメモリが検出されると起動に失敗してしまうため、Swapの無効化を行います。
- https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#swap-configuration
```shell
swapoff -a

# Swap領域の確認
free
```

## IPv4 パケットのフォワーディング(転送)を有効にする
この設定により、Pod間通信を実現できるようになります。

PodはLinuxのNetwork Namespaceで分離されており、それぞれが独立した仮想的なネットワーク空間を持っています。
これらの仮想インターフェースは同一ノード上の他Podや、ホスト側の実ネットワークインターフェースとは直接的に一体化されていないため、パケットを正しいインターフェースへ「フォワーディング」する機能が必要となります。

- https://kubernetes.io/docs/setup/production-environment/container-runtimes/#network-configuration
```shell
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF

# 設定の適用
sudo sysctl --system

# 設定値の確認
sysctl net.ipv4.ip_forward
```
## br_netfilterモジュールをロード

<dt>br_netfilterモジュールとは？</dt>
<dd>
Linuxカーネルモジュールの1つで、Linuxブリッジ経由のトラフィックにNetfilterのルールを適用する機能を提供します。
</dd>

このモジュールをロードしておくことで、Pod間通信にパケットフィルタリングルールを適用できるようになります。
この辺りの話は[こちら](https://qiita.com/TK_Yudai/items/87c92d579ac04752e557)の記事が大変参考になりました。

また、後述するflannelではbr_netfilterをロードしておかないと以下のようなエラーが発生してしまいます。
```
Failed to check br_netfilter: stat /proc/sys/net/bridge/bridge-nf-call-iptables: no such file or directory
```
```shell
# モジュールがシステム起動時に自動的にロードされるようにしておく
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
br_netfilter
EOF

# モジュールのロード
sudo modprobe br_netfilter

# モジュールがロードされていることを確認
lsmod | grep br_netfilter
```

## CRI (Container Runtime Interface) のインストール
今回はDockerを利用する必要がないため、CRIにcontainerdを採用しました。
- https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd
```shell
sudo apt-get update
sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd

# デフォルトのconfigファイルを作成
containerd config default | sudo tee /etc/containerd/config.toml
```

## containerdコンフィグファイルの修正
Ubuntu, 24.04 LTS の initシステムが`systemd`のため、cgroupドライバーとしてsystemd ドライバーを利用するようにcontainerdコンフィグファイルの一部を修正します。
- https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd-systemd  

cgroupsの話は[こちら](https://gihyo.jp/admin/serial/01/linux_containers/0003)の記事が分かりやすかったです。

また、このままではkubeadmによってクラスターを作成する際、下記のような警告メッセージが表示されるため、こちらも該当箇所を修正していきます。
```
W1219  detected that the sandbox image "registry.k8s.io/pause:3.8" of the container runtime is inconsistent with that used by kubeadm.It is recommended to use "registry.k8s.io/pause:3.10" as the CRI sandbox image.
```
**修正箇所**
```diff_shell
[plugins."io.containerd.grpc.v1.cri"]

- sandbox_image = "registry.k8s.io/pause:3.8"
+ sandbox_image = "registry.k8s.io/pause:3.10"
...
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  ...
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]

-     SystemdCgroup = false
+     SystemdCgroup = true
```
```shell
# initシステムの確認
systemctl --version

# コンフィグファイルの修正
sudo vi /etc/containerd/config.toml

# 設定の反映
sudo systemctl restart containerd
```

## inotifyの上限値を変更
<dt>inotifyとは？</dt>
<dd>
Linux上で「ファイルやフォルダが変更されたかどうか」を監視するツールです。これにより、アプリケーションがファイルやディレクトリの変更を自動で検知できるようになります。
</dd>

Kubeflowの環境を構築する上で、下記のように上限を変更しておかないと`Too many open files`のエラーが発生し、立ち上がらないPodが出てきてしまいます。
- https://github.com/kubeflow/manifests/tree/master?tab=readme-ov-file#prerequisites-1
```shell
# ユーザID毎に作成可能なinotifyインスタンスの上限を指定する
sudo sysctl fs.inotify.max_user_watches=1255360
# ユーザID毎に同時に監視できるファイル/ディレクトリの上限を指定する
sudo sysctl fs.inotify.max_user_instances=2280
```

## kubectl, kubeadm, kubeletのインストール
ざっくりとですが、それぞれ以下のような役割を担います。

<dl>
<dt>kubectl</dt>
<dd>Kubernetesクラスターを管理するためのコマンドラインツール。マニフェストの適用やログ収集を行うことができます。</dd>

<dt>kubelet</dt>
<dd>各ノードで動作し、コンテナの状態管理やPodのヘルスチェック・リソース管理などを担います。</dd>

<dt>kubeadm</dt>
<dd>Kubernetesクラスターを作成するためのツール。</dd>
</dl>

- https://v1-31.docs.kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl

```shell
sudo apt update
sudo apt-get install -y apt-transport-https ca-certificates curl

# Kubernetesリポジトリの追加
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

# kubectlの自動補完を有効にする（必須でない）
source /usr/share/bash-completion/bash_completion
echo 'source <(kubectl completion bash)' >>~/.bashrc
source ~/.bashrc
```

## Kustomizeのインストール
<dt>Kustomizeとは</dt>
<dd>Kubernetesのマニフェストを管理するためのツール。主なユースケースとして、「環境共通となる設定と環境毎に差分となる設定を分けて管理する」などが挙げられます。</dd>

Kubeflow Manifestsは、Kustomizeによりマニフェストが管理されているためインストールしていきます。

- https://kubectl.docs.kubernetes.io/guides/introduction/kustomize/

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

## クラスターの初期化
ネットワークアドオンとしてflannelを利用するため、PodネットワークのCIDR範囲を`10.244.0.0/16`としてクラスターを作成します。
```shell
sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --kubernetes-version=1.31.0

# kubectl コマンドを使用してクラスターを管理できるようにする
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
```

## ネットワークアドオンのインストール
以下にも記載がある通り、Pod間の通信を実現するためにCNI (Container Network Interface) をベースとするネットワークアドオンをインストールする必要があります。
今回はメジャーな CNI プラグインであるflannelを使用します。
- https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network
```shell
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
```

## コントロールプレーンノードの隔離
デフォルトではコントロールプレーンノードに、Podがスケジューリングされることはありません。
今回はシングルノードで構築を行うため、設定を変更します。
- https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#control-plane-node-isolation
```shell
# 設定を変更しない場合、下記のようなエラーが発生する。
# 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }. 

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

kubectl describe nodes | grep -i taints
```

## DVP (Dynamic Volume Provisoner) を有効にする
Kubeflow環境を立ち上げると複数のPodが作成されますが、その中にはPVCを必要とするPodも含まれます。
そのため、あらかじめ DVPを有効にしておくことで、動的にPVオブジェクトが作成されるようにしおきます。
今回は、DVPを有効にするためにストレージプロビジョナーとしてLocal Path Provisionerを利用していきます。

- https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/
- https://github.com/rancher/local-path-provisioner
```shell
# DVPを有効にしておかないと以下のようなエラーが出る
# 0/1 nodes are available: pod has unbound immediate PersistentVolumeClaims. 

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml

# デフォルトのStorageClassに設定
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

kubectl get storageclass
```

## Kubeflow Manifetsのクローン
```shell
git clone https://github.com/kubeflow/manifests.git
cd manifests
```

## Kubeflow Platformの構築
全てのオブジェクトが構築されるまでに数分かかります。
```shell
while ! kustomize build example | kubectl apply --server-side --force-conflicts -f -; do echo "Retrying to apply resources"; sleep 20; done

kubectl get pods -n kubeflow
```
全てのPodのステータスがRunningとなっていれば完了です。


# 最後に
## 今回ハマったポイント
**プロセッサアーキテクチャの違いによるエラー**
当初ARM64のVMインスタンス上で環境構築を行っていたのですが、いくつかのPodがCrashLoopBackOFFの状態で再起動を繰り返していました。
出力されたエラーログから、どうやらアーキテクチャが原因そうということが分かったので、AMD64に変更してみたところ、無事すべてのPodを起動することができました。

ただ、ARM64だとダメだな理由が見つけられなかったので、この辺りに知見のある方がいらっしゃいましたらご教示いただきたいです。

## 今後やりたいこと
今回構築した環境を利用して、データの前処理、モデル学習、デプロイなど一連のMLライフサイクル管理を体験してみようと考えています。

また、Kubernetesについて色々調べていくうちに、「コンテナがLinuxカーネル上で動く仕組み」の一端を知れたことが個人的には大きな収穫でした。
とはいえ、まだまだ分かっていないことが多いためこの辺りも引き続き勉強していきたいです。