研究室サーバーでKubernetesクラスタ構築(#14)

はじめに

以前から、研究室でノードを跨いだNaaS(Notebook as a Service)を作ってみたいという野望がありました。そのためにはまず、研究室サーバーでKubernetesクラスタを構築するのが最も手堅いと言えるでしょう。

ただしNaaSの提供は、コンテナ(DockerやPodman)で研究していたユーザーを完全に移行させるものではありません。ユーザーが選択可能かつメンテナーが居なくなった場合は従来のコンテナのみの運用に戻せるように細心の注意が必要です。

本記事では、既にDockerがインストールされているノードを活用してkubeadmでオンプレミスのKubernetesクラスタを構築するためのノウハウを共有します。

1. クラスタのノード構成

kubernetes.io

元々は、余っていたRaspberry Pi 4BかIntel NUCをコントロールプレーンノードとする予定でした。しかし、過去のおうちKubernetesの経験から、クラスタを安定稼働させるには高性能なコントロールプレーンノードが必要不可欠だと感じていました。

研究室の先生にダメ元で購入をお願いしたところ、既に素晴らしい性能のCPUラックサーバーを注文されていたとのことで、ありがたくコントロールプレーンノードとして利用させていただくことになりました。

人生初のラックサーバー取り付け

最終的には以下のようなノード構成となりました。

ホスト名 役割 OS CPU メモリ GPU
polaris コントロールプレーンノード Ubuntu 24.04 AMD EPYC 7543P
(32C/64T, 2.8GHz)
128GB N/A
aries ワーカーノード① Ubuntu 24.04 AMD EPYC 7542
(32C/64T, 2.9GHz)
256GB NVIDIA Quadro RTX 6000 × 4
taurus ワーカーノード② Ubuntu 24.04 Intel Core i9-10940X
(14C/28T, 3.3GHz)
64GB NVIDIA RTX 6000 Ada Generation × 3
gemini ワーカーノード③ Ubuntu 24.04 Intel Xeon Gold 5218R
(20C/40T, 2.1GHz)
192GB NVIDIA RTX A6000 × 5

先生のおかげで、非常に恵まれた環境でKubernetesクラスタを構築できることになりました。

2. コンテナランタイムのセットアップ

kubernetes.io

コンテナランタイムの候補としてcontainerdCRI-Oの間でかなり悩みましたが、以下の2つの理由からcontainerdを採用しました。

1つ目は、全てのワーカーノードにDockerがインストールされており、containerdも一緒にインストールされていることです。Kubernetesクラスタのアップグレードに合わせてDocker Engineのアップグレードが必要になる可能性がありますが、Docker Engineはメンテナンスで年に数回アップグレードしているため、そこまで大きな問題にはならないと考えました。

2つ目は、CRI-OとPodmanでconmonが競合する可能性があることです。今年からデフォルトでrootlessなコンテナエンジンであるPodmanの利用を推奨しており、全てのワーカーノードにインストールされています。コンテナ監視ツールであるconmonはCRI-OとPodmanの両方で使用されているため、バージョン統一が大変になると考えました。

ワーカーノードでcontainerdのバージョンを確認します。

$ containerd -v
containerd containerd.io 1.7.25 bcc810d6b9066471b0b6fa75f557a15a1cbf31bb

基本的にはDocker Engineのアップグレード時点で最新バージョンのcontainerdをインストールしているため、執筆時点で最新のKubernetes v1.32もサポートしているcontainerdのバージョンでした(参考)。

それから注意点として公式ドキュメントにも記載がありますが、今回はパッケージからcontainerdをインストールした場合に相当するので、設定ファイルである/etc/containerd/config.tomlに変更が必要でした。デフォルトではdisabled_plugins = ["cri"]とCRIが無効化されているので、Kubernetesからcontainerdを利用するためにはこれを有効化する必要があります。公式ドキュメントで紹介されていたように以下のコマンドで設定をリセットしてcontainerdを再起動しました。

$ containerd config default > /etc/containerd/config.toml
$ sudo systemctl restart containerd

ここで、containerdを再起動する必要があったため、ワーカーノードとしては偶然ユーザーが誰も利用していなかった1台のノードのみを利用することにしました。残りのノードはメンテナンスを周知してからクラスタに参加させる予定です。

3. kubeadmによるクラスタ作成

Kubernetesのバージョンは最新のv1.32を採用しました。

基本的には以下の公式ドキュメントの通りセットアップを行いました。

kubernetes.io

ただし、手順に沿ってkubeadm initコマンドとkubeadm joinコマンドを実行しても、コントロールプレーンノードがNotReadyという状態のままでした。

root@polaris:~# kubectl get nodes
NAME      STATUS     ROLES           AGE     VERSION
polaris   NotReady   control-plane   7m27s   v1.32.3
taurus    Ready      <none>          4m1s    v1.32.3

公式ドキュメントにも

You must deploy a Container Network Interface (CNI) based Pod network add-on so that your Pods can communicate with each other. Cluster DNS (CoreDNS) will not start up before a network is installed.

と記載がありますが、以下のようなコマンドでもノードの状態がなぜNotReadyなのかを確認することができます。

root@polaris:~# kubectl describe node polaris
Name:               polaris
Roles:              control-plane
# 省略
Conditions:
  Type             Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----             ------  -----------------                 ------------------                ------                       -------
  MemoryPressure   False   Mon, 07 Apr 2025 00:52:13 +0900   Mon, 07 Apr 2025 00:47:04 +0900   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure     False   Mon, 07 Apr 2025 00:52:13 +0900   Mon, 07 Apr 2025 00:47:04 +0900   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure      False   Mon, 07 Apr 2025 00:52:13 +0900   Mon, 07 Apr 2025 00:47:04 +0900   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready            False   Mon, 07 Apr 2025 00:52:13 +0900   Mon, 07 Apr 2025 00:47:04 +0900   KubeletNotReady              container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
# 省略

原因はCNIプラグインがインストールされていなかったことでした。

CNIプラグインとしては、公式ドキュメントで紹介されている中でもCalicoCiliumFlannelなどが有名ですが、今回はCiliumを採用しました。Ciliumの機能については別の記事にまとめようと思います。

docs.cilium.io

Ciliumは上記の公式ドキュメントを参考にHelmでデプロイしました。

ただし、今後Istioを導入する場合は公式ドキュメントに従って--set socketLB.hostNamespaceOnly=true --set cni.exclusive=falseを指定する必要があるので注意が必要です。

これによって、無事に全てのノードがReady状態となりました。

root@polaris:~# kubectl get nodes
NAME      STATUS   ROLES           AGE   VERSION
polaris   Ready    control-plane   21m   v1.32.3
taurus    Ready    <none>          18m   v1.32.3

4. Argo CDの導入

Kubernetesにおけるデプロイ方法のデファクトスタンダードであるGitOpsを実現するためのツールとして、Argo CDを導入します。

argo-cd.readthedocs.io

上記の公式ドキュメントに従ってArgo CDのインストールから始めました。

ステップ3でArgo CDのAPIサーバーを公開するためには、Service Type Load Balancer / Ingress / Port Forwardingの中から1つを選択しなければいけません。MetalLBはインストールしておらず、デバッグ用途のkubectl port-forwardも不適切であることから、消去法でIngressを選択しました。Ingressの設定にもたくさんの選択肢がありますが、今回はIstioを採用しました。

事前準備として以下の2ステップが必要でした。

  1. Istioのインストール:Istio / Install with Helm
  2. Ingress Gatewayのインストール:Istio / Installing Gateways

2.では「Kubernetes YAML」でインストールしましたが、LoadBalancerではなくNodePortを利用したかったので以下のように変更してapplyしました。

apiVersion: v1
kind: Service
metadata:
  name: istio-ingressgateway
  namespace: istio-ingress
spec:
-  type: LoadBalancer
+  type: NodePort
  selector:
    istio: ingressgateway
  ports:
  - port: 80
    name: http
+    targetPort: 8080
+    nodePort: 30080
-  - port: 443
-    name: https
---
# 省略
[root@polaris argocd ]# kubectl apply -f ingress.yaml
# 省略
[root@polaris argocd ]# kubectl get all -n istio-ingress
NAME                                        READY   STATUS    RESTARTS   AGE
pod/istio-ingressgateway-5b6548c6d6-wvt7d   1/1     Running   0          6s

NAME                           TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/istio-ingressgateway   NodePort   10.111.154.125   <none>        80:30080/TCP   6s

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/istio-ingressgateway   1/1     1            1           6s

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/istio-ingressgateway-5b6548c6d6   1         1         1       6s

残りは下記の公式ドキュメントに従ってIstioの設定を行います。ただし、今回はHTTPSは使用しないので、設定手順の中のTLSの設定はカットしました。

argo-cd.readthedocs.io

ブラウザでhttp://{{ IP }}/argocdにアクセスするとWeb UIにアクセスできます。

お馴染みのArgo CDのログイン画面

ステップ4でadminユーザーの初期パスワードを取得してログインしてPrivate Repositoriesの設定を行う予定でしたが、ログイン後のリダイレクトURLが/argocd/argocd/applications(正しくは/argocd/applications)になってしまいました。調査したところ、既にIssueでバグとして報告されており、執筆時点でv2.13では修正が完了したもののv2.14では未完了でした。

github.com

今回利用していたのはv2.14.9であったため、バージョンを下げることにしました。

# kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# 以下を変更
- https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.6/manifests/install.yaml

patches:
- path: ./patch.yaml

この修正で無事に/argocd/applicationsにリダイレクトされるようになりました。

正しくリダイレクトされた画面

さてPrivate Repositoriesの設定ですが、PATやSSH Private Key Credentialなどの属人化した設定をしたくなかったので、公式ドキュメントに従ってGitHub App Credentialの設定を行いました。

インストールしたGitHub Appの概要

最後にWeb UI上でアプリケーションの作成を行い、Getting StartedのサンプルマニフェストGitHubリポジトリにpushすることによって動作確認を行いました。

guestbookアプリケーションがSyncされている様子

Getting Startedの内容はここまででほとんどカバーできました。

公式ドキュメントで紹介されていたApp of Apps Patternは既に導入しましたが、今後はSSOSync Optionsなども設定する予定です。

5. GPU device pluginの導入

主にコンテナからGPUを利用するため、NVIDIA公式のdevice pluginを導入します。

github.com

READMEに記載されている必須要件の中のnvidia-docker >= 2.0 || nvidia-container-toolkit >= 1.7.0は、ワーカーノードが既に要件を満たしていました。

root@taurus:/home/azuma# nvidia-container-toolkit --version
NVIDIA Container Runtime Hook version 1.17.4
commit: 9b69590c7428470a72f2ae05f826412976af1395

一方で、必須要件のうち「nvidia-container-runtimeがデフォルトの低レベルランタイムとして設定されている」については、公式ドキュメントに従ってnvidia-ctkコマンドで設定した上で以下のような変更を/etc/containerd/config.tomlに加える必要がありました。

# 省略
    [plugins."io.containerd.grpc.v1.cri".containerd]
-      default_runtime_name = "runc"
+      default_runtime_name = "nvidia"
# 省略

containerdをrestartしたら、いよいよDaemonSetをデプロイするだけでGPUを有効化できます。staticなYAMLファイルも提供されていましたが、今回は本番環境で推奨されていたHelmを利用しました*1

[root@polaris ~]# helm upgrade -i nvdp nvdp/nvidia-device-plugin \
  --version=0.17.1 \
  --namespace nvidia-device-plugin \
  --create-namespace \
  --set gfd.enabled=true
# 省略
[root@polaris ~ ]# kubectl get daemonset -n nvidia-device-plugin
NAME                                              DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
nvdp-node-feature-discovery-worker                1         1         1       1            1           <none>                        5m17s
nvdp-nvidia-device-plugin                         1         1         1       1            1           <none>                        5m17s
nvdp-nvidia-device-plugin-gpu-feature-discovery   1         1         1       1            1           <none>                        5m17s
nvdp-nvidia-device-plugin-mps-control-daemon      0         0         0       0            0           nvidia.com/mps.capable=true   5m17s
[root@polaris ~ ]# kubectl describe node taurus
# 省略
Capacity:  # 省略
  nvidia.com/gpu:     3
Allocatable:  # 省略
  nvidia.com/gpu:     3
# 省略

このHelmの設定もGitOpsとして宣言的に管理するため、GitHubリポジトリに追加しました(参考)。

最後に、READMEで紹介されている、GPUを利用するサンプルジョブをデプロイして成功すれば完成です。

Time-slicingやMPSのようなshared GPUのための設定も必要になりそうですが、NaaS構築と同じタイミングでニーズに合わせて設定する予定です。

まとめ

本記事では、Dockerがインストールされたノードを活用してkubeadmでオンプレミスのKubernetesクラスタを構築する方法を紹介しました。つまずきポイントがいくつかあったので、オンプレミスクラスタを構築してみたい方にとって少しでも参考になれば幸いです。

Kubernetes周りでデファクトスタンダードなツールは他にもたくさんあるのですが、のめり込んでしまうとキリがないので、まずはNaaS構築に向けて着実にマイルストーンを達成してこうと思います。

*1:READMEに記載されていた最も基本的なインストールコマンドであるhelm upgrade -i nvdp nvdp/nvidia-device-plugin --namespace nvidia-device-plugin --create-namespace --version 0.17.1は期待通りにデプロイされませんでした。原因はデフォルトで無効化されているgpu-feature-discoveryが、自動的に生成するラベルがDaemonSetのnodeAffinityとして設定されているためでした。GFDを有効にするか、手動でノードにnvidia.com/gpu.present=trueラベルを付与しましょう。