Kubernetes(k8s) v1.16とNvidia-Docker2を用いたマルチノードDeepLearning環境の構築 Part2

この記事は近畿大学 Advent Calendar 2019 - Qiita 24日目の記事です.

はじめに

この記事は下記記事の続きですので,下記記事を読んだ後に読むことを推奨します.

tenzen.hatenablog.com

前回の構築ではVolumeのhostPathを使用してWorker物理マシン上のディスクにあるディレクトリを直接マウントしました.仮構築であればそれでも良いですが,本番環境で使用する際はデータベースサーバなどを用意して使用するのが好ましいと考えられます. そこで今回はNFSサーバを用意して,PersistentVolumeを前回構築したKubernetes Cluster上にマウントしてみます.

その後図1の左側の準備を行ってKubernetes Cluster上でのリモート開発環境を構築していきたいと思います.

final_reach
図1:最終目標図

また前回計算リソースの監視としてKubernetes Dashboard2.0を用いましたが,Kubernetesと同じくCNCFのGraduatedプロジェクトであるPrometheus と可視化ツールのGrafanaを用いてより高度な死活監視システムの構築を行って行きたいと思います.

最後に本記事はKubernetesやDockerについてある程度知識があることを前提に書いているため詳細は記述しません.私の過去記事や書籍など読んで知識を身につけてきてください.

tenzen.hatenablog.com

Kubernetes完全ガイド (impress top gear)

Kubernetes完全ガイド (impress top gear)

Strageリソース

Kubernetesには大きく分けてVolumeとPersistentVolumeと呼ばれる二種類のストレージシステムが用意されています.Part1では作業簡略化のためVolumeを使用してコンテナ内のStorageリソースを構築しましたが,今回はPersistentVolumeを使用してコンテナ内Strageリソースを構築していきたいと思います. またKubernetesではVolumeとPersistent Volumeで対応されているものは違いますが,単純なファイルシステム(FS)からオブジェクトストレージシステムや分散ストレージシステムにも対応しています.

Volume

Volumeでは以下のような種類をサポートしています.

  • awsElasticBlockStore
  • azureDisk
  • azureFile
  • cephfs
  • cinder
  • configMap
  • csi
  • downwardAPI
  • emptyDir
  • fc (fibre channel)
  • flexVolume
  • flocker
  • gcePersistentDisk
  • gitRepo (deprecated)
  • glusterfs
  • hostPath
  • iscsi
  • local
  • nfs
  • persistentVolumeClaim
  • projected
  • portworxVolume
  • quobyte
  • rbd
  • scaleIO
  • secret
  • storageos
  • vsphereVolume

一つ一つの仕様については以下の公式ガイドを参照してください.

kubernetes.io

Volumeとは主にPodなどをデプロイする際マニフェストに直接マウントするディレクトリなどを記述することで使用可能になるStrageリソースのことです.そのためKubernetes上からボリュームの新規作成や削除などのファイル操作をすることはできませんし,マニフェストでは既存のボリュームしか指定できません.

前回使用したhostPathではWorkerホストマシン上のディレクトリを直接マウントする方法になってしまい,利便性にかけてしまいます.

Persistent Volume

Persistent Volumeでは以下の種類をサポートしています.

  • GCEPersistentDisk
  • AWSElasticBlockStore
  • AzureFile
  • AzureDisk
  • CSI
  • FC (Fibre Channel)
  • FlexVolume
  • Flocker
  • NFS
  • iSCSI
  • RBD (Ceph Block Device)
  • CephFS
  • Cinder (OpenStack block storage)
  • Glusterfs
  • VsphereVolume
  • Quobyte Volumes
  • HostPath(シングルノークラスタのみで使用可能,マルチノードクラスタでは機能しない.)
  • Portworx Volumes
  • ScaleIO Volumes
  • StorageOS

それぞれの詳細な仕様は以下の公式ガイドを参照してください.

kubernetes.io

今回はこれらのうち比較的簡単に用意ができるNFSを使用していきます.Persistent VolumeでのNFSは容量制限の機能が無かったりと他のプラグインと違うところもありますので,留意しておいてください.

準備

今回はNetGear社製のReadyNASにあるNFAサーバ機能を利用するためサーバ側の構築はしませんが,各自でNFSサーバの準備をしてください.

NFSサーバの構築が終了したら,NFSクライアントの準備を行うため下記のコマンドを全てのMasterとWorkerで実行してください.

$ sudo apt-get install -y nfs-client

Persistent Volumeの作成

以下のマニフェスト「sample_persistent_volume.yaml」を作成します. 以下のマニフェストではサンプルとしてhdd・ssd・nvmeのラベルのついた三種類の容量のボリュームを作成していますが,サンプルとして表しているだけであり実際の物理ストレージとは異なります.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv01
  labels:
    type: nfs
    environment: hdd
    speed: slow 
spec:
  capacity:
    storage: 500Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nfs:
    server: xxx.xxx.xxx.xxx
    path: /data/workspace/tenzen/k8s/test_volume/sample01
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv02
  labels:
    type: nfs
    environment: ssd
    speed: high
spec:
  capacity:
    storage: 400Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nfs:
    server: xxx.xxx.xxx.xxx
    path: /data/workspace/tenzen/k8s/test_volume/sample02
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: sample-pv03
  labels:
    type: nfs
    environment: Nvme
    speed: much_high
spec:
  capacity:
    storage: 300Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nfs:
    server: xxx.xxx.xxx.xxx
    path: /data/workspace/tenzen/k8s/test_volume/sample03

マニフェストをいきなり貼られても理解不能な方もいらっしゃると思うので,作成したマニフェストのわかりにくいだろうと思う項目の解説を行います.

metadata.labels

metadata.labels.type・metadata.labels.environment・metadata.labels.speedはPersistent Volumeを作成する上でのラベル情報になります.ラベル情報なので必須項目ではないのですが,ラベルを設定しておくことで次の章のPersistent Volume Claimでラベル情報を元にPersistent Volumeを探し出してくることができます.上記例では「type」・「environment」・「speed」三種類のラベルを設定しています.

persistent_volume_labels
図2:Persistent Volumeのラベル処理

spec.capacity.storage

作成するPersistent Volumeの容量を定義します.

spec.accessModes

ボリュームへのアクセス制限を行います.アクセス制限の種類は三種類用意されています.

  • ReadWriteOnce(RWO)
    • 単一ノードからのみRead/Writeを許可する.
      • 単一ノードからのみ許可していますが,Kubernetesは要求リソースを満たすnodeにPodを自動デプロイするため単一ノードというよりは単一Podというイメージです.

example_for_rwo
図3:RWOの例

  • ReadOnlyMany(ROX)
    • 複数ノードからのReadのみ許可する.
      • ROXの場合書き込み要求があるPodが存在してしまうと,該当ノード以外のノードでマウントできなくなってしまうため,Persistent Volume ClaimにはreadOnlyを指定する必要があります.

example_for_rox
図4:ROXの例

  • ReadWriteMany(RWM)
    • 複数ノードからのRead/Writeを許可する.

example_for_RWX
図5:RWXの例

spec.persistentVolumeReclaimPolicy

Reclaim PolicyではPersistent Volume Claim破棄後にPersistent Volumeをどのように扱うかの記述を行う場所です. 制御方法は以下の3パターンが用意されています.

  • Delete
    • Persistent Volume Claimが削除されるとPersistent Volume自体も削除されます.また,ディスク自体も削除されるため新たにPersistent Volume Claimを作成しても使用することはできません.

reclaim_policy_delete
図6:Reclaim PolicyにおけるDeleteの処理

  • Recycle
    • Persistent Volume Claimが削除されてもPersistent Volumeは削除せず,ディスク上データの削除のみ行います.そのため再び他のPersistent Volume ClaimからPersistent Volumeの使用が可能になります.

reclaim_policy_recycle
図7:Reclaim PolicyにおけるRecycleの処理

  • Retain
    • Persistent Volume Claimが削除されるとPersistent Volumeは削除されますが,ディスク上データの削除は行われません.そのためこのままではPersistent Volumeがreleasedになっていて他のPersistent Volume Claimから使用することはできませんが,新たにPersistent Volumeを宣言して同領域を割り当てることによって他のPersistent Volume Claimから再使用することができます.

reclaim_policy_retain
図8:Reclaim PolicyにおけるRetainの処理

spec.nfs

ここではマウントするNFSサーバについて記述していきます. spec.nfs.serverにはNFSサーバのIPアドレスを記述してください. また,spec.nfs.pathにはマウントするNFSサーバ上のディレクトリを指定してください.

Persistent Volume Claimの作成

Persistent Volume Claimは先ほど作成したPersistent Volumeなどの永続化領域にたいして要求を行うものです. 今回は下記のような「persistent_volume_claims.yaml」を作成してください. 以下のマニフェストでは先ほど作成した3つのPersistent Volumeをそれぞれ呼び出すように記述しています.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-claim01
spec:
  selector:
    matchLabels:
      environment: hdd
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  storageClassName: manual
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-claim02
spec:
  selector:
    matchLabels:
      environment: ssd
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  storageClassName: manual
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-claim03
spec:
  selector:
    matchLabels:
      environment: nvme
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  storageClassName: manual
  resources:
    requests:
      storage: 1Gi

spec.selector.matchLabels

spec.selector.matchLabelsではPersistent Volumeでつけたラベルを記述することで,ラベルにマッチするPersistent Volumeを呼び出してくれます.上記の「test-claim01」では 「sample-pv01」を,「test-claim02」では「sample-pv02」を,「test-claim03」では「sample-pv03」を呼び出すようにPersistent Volume Claimのラベルを指定しています.

spec.resources.requests.storage

spec.resources.requests.storageでは,要求するPersistent Volumeの容量を記述します.例えばここで150GBを指定するとPersistent Volume Claimは最も近い容量である300GBを持ってる「sample-pv03」を確保します. このように指定した値の容量ではなく最も近い容量を確保するため,Persistent Volumeは様々な容量のものを用意しておきましょう.またDynamic Provisioning機能を使えば要求容量ぴったりのPersistent Volumeを確保することができますが,今回は使用しないので割愛します.

さらに今回はプラグインnfsを使用しているため容量指定が機能しませんので, 適当に1GBと設定しています.

requests_strage
図9:Persistent Volume Claimのrequests storageに対する動き

Deployment

先ほど作成したPersistent Volume Claimを使用してPod内にマウントした以下のようなDeployment「test-deployment.yaml」を作成します.Deploymentの原型は,Volumeのhostpathを使用してPart1で作成したもの使用します.(紫色の場所を削除し,水色の場所を追記してください.)

tenzen.hatenablog.com

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gpu-dlenv
  template:
    metadata:
      labels:
        app: gpu-dlenv
    spec:
      containers:
        - name: gpu-dlenv-container
          image: xxx.xxx.xxx.xxx:5000/k8s/dl_env:hoge

#デプロイするNodeにのみ存在するimageを使用する場合は下記を記述する.
#          imagePullPolicy: Never

          tty: true
          volumeMounts:
          - mountPath: /srv/sample01
-           name: host-share
+           name: pvc-volume01
          resources:
            limits:
              nvidia.com/gpu: 2 #GPUの個数を制限する.
      volumes:
-     - name: host-share  
-       hostPath:
-         path: /host_hoge
-         type: DirectoryOrCreate
+     - name: pvc-volume01
+       persistentVolumeClaim:
+         claimName: test-claim01
+     restartPolicy: Always

#nodeを指定す場合は下記のように記述する. 
#      nodeSelector:
#          type: <label名>

以上のマニフェストで「gpu-deployment」内にreplica数1で「gpu-dlenv-container」を作成し,コンテナ内のディレクトリ「/srv/sample01」に「pvc-volume01」としてPersistent Volume Claimの「test-claim01」をマウントしています. 次に,残りの2つのPersistent Volume Claimもコンテナ「gpu-dlenv-container」内にマウントしてみます. 先ほど作成したマニフェスト「test-deployment.yaml」に追加してください.(追加項目は水色になっています.)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gpu-dlenv
  template:
    metadata:
      labels:
        app: gpu-dlenv
    spec:
      containers:
        - name: gpu-dlenv-container
          image: xxx.xxx.xxx.xxx:5000/k8s/dl_env:hoge

#デプロイするNodeにのみ存在するimageを使用する場合は下記を記述する.
#          imagePullPolicy: Never

          tty: true
          volumeMounts:
          - mountPath: /srv/sample01
            name: pvc-volume01
+         - mountPath: /srv/sample02
+           name: pvc-volume02
+         - mountPath: /srv/sample03
+           name: pvc-volume03 
          resources:
            limits:
              nvidia.com/gpu: 2 #GPUの個数を制限する.
      volumes:
      - name: pvc-volume01
        persistentVolumeClaim:
          claimName: test-claim01
+     - name: pvc-volume02
+       persistentVolumeClaim:
+         claimName: test-claim02
+     - name: pvc-volume03
+       persistentVolumeClaim:
+         claimName: test-claim03
      restartPolicy: Always

#nodeを指定す場合は下記のように記述する. 
#      nodeSelector:
#          type: <label名>

デプロイ

実装

以下のコマンドを実行して作成したPersistent Volume,Persistent Volume Claim,Deploymentをデプロイします.デプロイしたPersistent Volumeを確認してみると,正常に3つともStatusがBoundになっていることがわかります.同様にPersistent Volume Claimも3つ正常に起動されていることがわかります. また作成したPodの情報を表示してみると,PVCが正常にマウントされて起動されていることがわかります.

$ kubectl apply -f sample_persistent_volume.yaml
persistentvolume/sample-pv01 created
persistentvolume/sample-pv02 created
persistentvolume/sample-pv03 created
$
$ kubectl apply -f persistent_volume_claims.yaml
persistentvolumeclaim/test-claim01 created
persistentvolumeclaim/test-claim02 created
persistentvolumeclaim/test-claim03 created
$
$ kubectl apply -f test-deployment.yaml
deployment.apps/gpu-deployment-remote created
$
$ kubectl get pv
NAME          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS   REASON   AGE
sample-pv01   500Gi      RWO            Retain           Bound    default/test-claim01   manual                  10s
sample-pv02   400Gi      RWO            Retain           Bound    default/test-claim02   manual                  10s
sample-pv03   300Gi      RWO            Retain           Bound    default/test-claim03   manual                  10s
$
$kubectl get pvc
NAME           STATUS   VOLUME        CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-claim01   Bound    sample-pv01   500Gi      RWO            manual         9s
test-claim02   Bound    sample-pv02   400Gi      RWO            manual         9s
test-claim03   Bound    sample-pv03   300Gi      RWO            manual         9s
$
$ kubectl get pods
NAME                                    READY   STATUS    RESTARTS   AGE
gpu-deployment-remote-d49ff6565-bzzzk   1/1     Running   0          93s
$
$ kubectl describe pods
Name:         gpu-deployment-d49ff6565-bzzzk
Namespace:    default
Priority:     0
Node:         utaha/xxx.xxx.xxx.xxx
Start Time:   Wed, 11 Dec 2019 17:57:49 +0900
Labels:       app=gpu-dlenv
              pod-template-hash=d49ff6565
Annotations:  <none>
Status:       Running
IP:           10.244.1.53
IPs:
  IP:           10.244.1.53
Controlled By:  ReplicaSet/gpu-deployment-d49ff6565
Containers:
  gpu-dlenv-container:
    Container ID:   docker://6ec592f048c0a50358b64df2eda7c76c1cd5a524d23add7c72790bdbc7efbcd8
    Image:          xxx.xxx.xxx.xxx:5000/k8s/dl_env:hoge
    Image ID:       docker-pullable://xxx.xxx.xxx.xxx:5000/k8s/dl_env@sha256:c7be5ffa7f6c04e3a0ace7c334bf889ddc071f318c4618298d40b5132662e1de
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Wed, 11 Dec 2019 17:57:51 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      nvidia.com/gpu:  1
    Requests:
      nvidia.com/gpu:  1
    Environment:       <none>
    Mounts:
      /srv/sample01 from pvc-volume01 (rw)
      /srv/sample02 from pvc-volume02 (rw)
      /srv/sample03 from pvc-volume03 (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-bsgjw (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  pvc-volume01:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  test-claim01
    ReadOnly:   false
  pvc-volume02:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  test-claim02
    ReadOnly:   false
  pvc-volume03:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  test-claim03
    ReadOnly:   false
  default-token-bsgjw:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-bsgjw
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age        From               Message
  ----    ------     ----       ----               -------
  Normal  Scheduled  <unknown>  default-scheduler  Successfully assigned default/gpu-deployment-d49ff6565-bzzzk to utaha
  Normal  Pulled     4m27s      kubelet, utaha     Container image "xxx.xxx.xxx.xxx:5000/k8s/dl_env_remote:hoge" already present on machine
  Normal  Created    4m27s      kubelet, utaha     Created container gpu-dlenv-container
  Normal  Started    4m26s      kubelet, utaha     Started container gpu-dlenv-container

確認

実際にコンテナへ擬似ログインしてマウントされているか確認します.

$ kubectl exec -it gpu-deployment-d49ff6565-bzzzk /bin/bash
root@gpu-deployment-d49ff6565-bzzzk:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@gpu-deployment-d49ff6565-bzzzk:/#
root@gpu-deployment-d49ff6565-bzzzk:/#cd ./srv
root@gpu-deployment-d49ff6565-bzzzk:/srv# ls
sample01  sample02  sample03

マウントできていることがわかります.次にそれぞれのボリューム内で適当なテキストファイルを作って実際NFSサーバ内に作成されるか確認します.以下でまずコンテナ内で適当なファイルを作成します.

root@gpu-deployment-d49ff6565-bzzzk:/srv# touch ./sample01/01.txt
root@gpu-deployment-d49ff6565-bzzzk:/srv# touch ./sample02/02.txt
root@gpu-deployment-d49ff6565-bzzzk:/srv# touch ./sample03/03.txt
root@gpu-deployment-d49ff6565-bzzzk:/srv# ls sample01
01.txt
root@gpu-deployment-d49ff6565-bzzzk:/srv# ls sample02
02.txt
root@gpu-deployment-d49ff6565-bzzzk:/srv# ls sample03
03.txt
root@gpu-deployment-d49ff6565-bzzzk:/srv# exit

次にnfsサーバをホストマシンにマウントして,反映されているか直接確認します.「xxx.xxx.xxx.xxx」にはnfsサーバのIPアドレスを指定してください. また「:」以下にはマウントするnfsサーバのディレクトリを記述してください.

$ cd /mnt/
$ sudo mount -t nfs xxx.xxx.xxx.xxx :/data/workspace/tenzen/k8s/test_volume/
$ 
$ ls -R
.:
sample01  sample02  sample03

./sample01:
01.txt

./sample02:
02.txt

./sample03:
03.txt

Persistent Volumeのマウントの確認とデータ同期の確認ができたので次にPodを削除して動作を見てみます.

$ kubectl delete -f test_deployment.yaml
deployment.apps "gpu-deployment" deleted
$
$ cd /mnt/
$ ls -R
.:
sample01  sample02  sample03

./sample01:
01.txt

./sample02:
02.txt

./sample03:
03.txt

次にPersistent VolumeとPersistent Volume Claimの削除も行います.

$ kubectl delete -f persistent_volume_claims.yaml
persistentvolumeclaim "test-claim01" deleted
persistentvolumeclaim "test-claim02" deleted
$
$ kubectl delete -f persistent_volume.yaml 
persistentvolume "sample-pv01" deleted
persistentvolume "sample-pv02" deleted
persistentvolume "sample-pv03" deleted
$
$ kubectl get pv
No resources found in default namespace.
$
$ kubectl get pvc
No resources found in default namespace.

nfsサーバの中にはいって確認してみます.

$ cd /mnt/
$
$ ls -R
.:
sample01  sample02  sample03

./sample01:
01.txt

./sample02:
02.txt

./sample03:
03.txt

Reclaim PolicyをRetainにセットしているのでちゃんとデータが保持されていることがわかると思います.

リモート開発環境の構築

概略

Microsoft VSCodeを使用してKubernetes内コンテナ内でリモート開発環境を構築していきます. ローカルマシンにはMacBookPro 13インチ 2018Lateモデルを使用しています.

事前にVSCodeをインストールしておいてください. VSCodeがインストールできたら以下のような「Remote Development」拡張ツールをインストールします.

remote_development
図10:Remote Development

リモート開発環境の構築では,以下の記事を参考にしました. この記事ではサーバマシンのDockerコンテナにアクセスする際にサーバホストを踏み台としてコンテナにsshログインしています. これをKubernetes Cluster上で行います.

qiita.com

今回は下図のようにクライアントマシンからKubernetes Masterホストを踏み台としてKubernetes Clusterにアクセスし,そこから目的のコンテナ内にsshログインします.

remote_access
図11:リモートアクセスの様子

リモート開発環境用DeepLearning環境DockerImageの作成

今回は前回のPart1の記事で作成したDockerFileを改変してリモートアクセスできる以下のようなDockerFileを作成します.

tenzen.hatenablog.com

また,そのままではリモートアクセスする際bashファイルなどがコピーされずパスが通らないので,強引にPythonへパスを通します.

FROM nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04

#preparation
RUN apt-get update
RUN apt-get install -y curl wget git unzip imagemagick bzip2 vim
RUN git clone https://github.com/pyenv/pyenv.git .pyenv

WORKDIR /
ENV HOME  /
ENV PYENV_ROOT /.pyenv
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH

RUN pyenv install anaconda3-4.4.0
RUN pyenv global anaconda3-4.4.0
RUN pyenv rehash

RUN pip install --upgrade pip
RUN apt-get update && apt-get install -y libsm6
RUN pip install opencv-python==3.4.7.28
RUN pip install tensorflow-gpu==1.13.1 --ignore-installed --user
RUN pip install keras  
RUN pip install torch torchvision && apt-get install -y libgl1-mesa-dev
RUN pip install tqdm
RUN pip install torchsummary
RUN pip install progressbar

RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:パスワード' | chpasswd
RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile
EXPOSE 22

RUN echo "export HOME=/"  >> /etc/environment
RUN echo "export PYENV_ROOT=/.pyenv"  >> /etc/environment
RUN echo "if [ -f ~/.bashrc ]; then  . ~/.bashrc;  fi" >>~/.bash_profile
RUN echo "PATH=${PATH}:$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> ~/.bashrc

CMD ["/usr/sbin/sshd", "-D"]

今回はパスワード認証でsshログインします.DockerFile中の「RUN echo 'root:パスワード' | chpasswd」のパスワードの部分は任意のパスワードを設定してください.

「RUN echo "export HOME=/" >> /etc/environment」から「"PATH=${PATH}:$PYENV_ROOT/shims:$PYENV_ROOT/bin" >> ~/.bashrc 」まででパスを通しています.前回のDockerFileを使用しない場合は適宜パスを通し直してください.

以下のコマンドを実行してDockerイメージをビルドし,Private Docker Registryにプッシュします.Private Docker Registryの建て方などは以下を参考にしてください.

tenzen.hatenablog.com

$ sudo docker build -t dl_env_remote .
$
$ sudo docker tag dl_env_remote:latest xxx.xxx.xxx.xxx:5000/k8s/dl_env_remote:hoge
$ sudo docker push xxx.xxx.xxx.xxx:5000/k8s/dl_env_remote:hoge

ssh設定

まずはじめに図12のようなVScode左下にある緑色のボタンを押して,図13のポップアップを出します.

f:id:tenzen_hgst:20191221073951p:plain
図12:ssh設定Step1

f:id:tenzen_hgst:20191221073955p:plain
図13:ssh設定 Step2

ポップアップが出たら上から二段目の「Remote-SSH: Open Configuration File」を選択します.

次に一番上の「Users/hoge/.ssh/config」を選択します.

f:id:tenzen_hgst:20191222012122p:plain
図14:ssh設定 Step3

以下のコマンドを実行して接続するPodのIPアドレスを探してきます.KubernetesのPod内コンテナはポート制御によりネットワークが構成されているため同一Pod内のコンテナは同一IPアドレスが割り振られています.しかしKubernetesではサブプロセスを走らせるコンテナがある場合など一部の例外をのぞいて,1Podに1コンテナが原則のため今回はその原則に従い,PodのIPアドレスの22番ポートに接続することでコンテナへのsshを可能にします.

f:id:tenzen_hgst:20191225140010p:plain
Pod内ネットワーク

$ kubectl get pods -owide
NAME                                    READY   STATUS    RESTARTS   AGE    IP            NODE    NOMINATED NODE   READINESS GATES
gpu-deployment-remote-d49ff6565-69dvm   1/1     Running   0          4d1h   10.244.1.7    utaha   <none>           <none>

例では「10.244.1.7」がデプロイしたPodのIPアドレスになるので,sshのconfigを以下のように設定します.

Host k8s_Master
# Kubernetes Cluster Master IP address.
  Hostname xxx.xxx.xxx.xxx
  User k8s-m

Host test_worker
  Hostname 10.244.1.7
  User root
  ProxyCommand ssh -W %h:%p k8s_Master

完了したら図13の「Remote-SSH: Connect to Host...」を選択して,先ほど作成した「test_worker」を選択します.

接続できたら拡張機能でコンテナ内にPython拡張機能をインストールしてください.うまくいかない場合reloadを繰り返すとうまくいくことがあります. 以上でリモート開発環境の構築は完了です.

死活監視システムの構築

今回はKubernetes Cluster上にPrometheusとGrafanaを展開して死活監視を行います.Prometheusの方ではPrometheus Operatorを使用し,NodeExporterとDCGM(Data Center GPU Manager)Exporterを使用して行きます. Prometheusの実際の導入事例などがPrometheus MeetUpで発表されているのですが,以下のサイトでその資料がまとめられているので必ず読みましょう.かなり勉強になります.

prometheus.connpass.com

Prometheus

prometheus.io

PrometheusとはCNCFでGraduatedプロジェクトに認定されているモニタリングシステムのことです.またこの章は以下の書籍を参考にしました.

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

PrometheusはPull型の監視アプリケーションです.そのためPrometheus側が監視対象サーバにメトリクスを取得しに行きます.スクレイプするターゲットはPrometheusがサービスディスカバリで見つけてきます. 取得方法は大きく分けて二種類あり,ダイレクトインストルメンテーションによってアプリケーションに埋め込まれたクライアントライブラリがPrometheusで受け取ることができる形式に合わせて生成したメトリクスを取得する方法と,ダイレクトインストルメンテーションできないアプリケーションの場合にexporterをデプロイする形式があります.exporterはPrometheusからのPull命令を受け取り,アプリケーションへメトリクスを取得しに行きます.その後受け取ったメトリクスをPrometheusが扱える形式に変換した後Prometheusにメトリクスを渡す役割をはたしいています.

Prometheusは受け取ったメトリクスをAlertManagerなどの警告ツールへ受け渡したり,Grafanaなどの可視化ツールがデータリソースとしてPromethesu側へ情報を取りに行きます.

prometheus_architechture
図15:Prometheusのアーキテクチャ

Prometheus Operator

Prometheus Operatorとは文字通りPrometheusをOperatorに使用して展開するものです. またPrometheus Operatorについては以下の書籍と記事を参考にしました.

qiita.com

Operator

Kubernetesでは前回の記事で作成した深層学習環境のDeploymentのようにステートレスなアプリケーションが推奨されてきました.しかしデータベースなどステートフルにした方が扱いやすいアプリケーションがあることも確かであるためKuberneteではStatefulSetが用意されています.Statefulの機能に運用のノウハウ自体を自動化するNoOpsという考え方をミックスしたものがOperatorです.

Prometheus Operator

Prometheus Operatorは前述したPrometheusとOperatorを融合したものであり,ステートフルにPrometheusを比較的用意にデプロイできるようにしたものです.Prometheus OperatorはPrometheusの設定情報を常に監視し,変更があった場合には最新の設定を反映させます.

github.com

DCGM

DCGM(Data Center GPU Manager)はnvidiaGitHub上で公開しているサードパーティー製のexporterです.公式exporterとしてはnode exporterなどが有名ですが,DCGMを使用することでnode exporterでは取得できないNvidiaGPU情報を取得することができるようになります.

github.com

Grafana

GrafanaはPrometheus専用のダッシュボード作成ツールではないですが,現在PrometheusではGrafanaを使用することが推奨されています.GrafanaはPrometheusをデータリソースとして使用することでPrometheusを強力に可視化することができます.今回はあまりGrafanaについては踏み込まず,簡単な表示のみを行っていきます.

grafana.com

実装

今回の死活監視の流れは図16のような構成をとります. node exporterではCPUやメモリ,ストレージIOなどの基本的なハードウェア情報を,DCGM exporterではGPUの情報を取得してきます.exporterが集めてきた情報をPrometheusが収集し,GrafanaがPrometheusにその情報を取りに行って可視化します.

figure_monitoring_flow
図16:死活監視の概略

まず準備としてGitHubのcoreosにあるkube-prometheusリポジトリから以下のコマンドを実行してPrometheus Operatorを取ってきます.

$ git clone https://github.com/coreos/kube-prometheus

次にNvidiaGPUを搭載しているNodeにのみexporterが展開されるように,NvidiaGPUを搭載している全てのNodeにラベルをつけていきます.

$ kubectl label nodes utaha hardware-type=NVIDIAGPU
$ kubectl label nodes eriri hardware-type=NVIDIAGPU
$ kubectl label nodes megumi hardware-type=NVIDIAGPU

Serviceの実装

この章ではPrometheusとGrafanaのServiceを実装していきます. この章では基本的に全作業をMasterで行います.

Prometheus OperatorにおけるServiceは「Cluster IP」・「NodePort」・「LoadBalancer」の三種類が使用可能です.

  • Cluster IP
    • Kubernetes Cluster内部でのみ通信可能な仮想IPが割り当てられます.そのためポートフォワーディングなどを行わない限りKubernete Cluster外との通信はできません.

clusterip
図17:ClusterIP

  • Node Port
    • クラスタ内に作られたNodePortが全ての「Kubernetes Node IP:Service Port」を受信し,各Podへ通信を転送します.Kubernetes Cluster外との通信ができますが,NodePortはKubernetes Cluster内のどこかのNodeに作られてそこで通信を捌くためそのノードを障害を起こしてしまうと通信ができなくなってしまい,単一障害点となってしまうデメリットもあります.

nodeport
図18:NodePort

  • LoadBalancer
    • LoadBalancerはクラスタ外に作られ,Kubernete Cluster外からの通信を全て引上け,Podに転送を行います.NodePortと動きは似ていますが,クラスタ外で通信を受け取るためNode障害に強い特徴があります.

loadbalancer
図19:LoadBalancer

今回Prometheus OperatorとGrafanaのServiceはデフォルトでCluster IPが使用されていますが,今回はNode Portを使用してKubernetes Cluster外部からでもアクセスしやすいように設計します. 以下のように取得してきたファイルの

「/kube-prometheus/manifests/prometheus-service.yaml」と

「/kube-prometheus/manifests/grafana-service.yaml」を以下のように書き換えます.

prometheus-service.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    prometheus: k8s
  name: prometheus-k8s
  namespace: monitoring
spec:
+ type: NodePort
  ports:
  - name: web
+   nodePort: 30900
    port: 9090
+   protocol: TCP
    targetPort: web
  selector:
    app: prometheus
    prometheus: k8s
-  sessionAffinity: ClientIP
+ #sessionAffinity: ClientIP

grafana-service.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: grafana
  name: grafana
  namespace: monitoring
spec:
+ type: NodePort
  ports:
  - name: http
+   nodePort: 30039
    port: 3000
+   protocol: TCP
    targetPort: http
  selector:
    app: grafana

これでNodePortを使用してKubernetes Cluster外部と通信ができるようになります.

DCGM exporterの実装

DCGM exporterをnode exporterの中に埋め込んでいきます.

DCGM exporteをnode exporterに埋め込んだマニフェストは以下のNvidia公式GitHubで公開されていますが,古すぎてKubernetes 1.16では動かず,1.16用のapi versionの書き換えても動きませんので注意してください.(ここでだいぶハマった.)

github.com

またNvidiaKubernetesのパッケージ管理ツールHelmを用いたKubernetes Cluster上へのPrometheus Operatorの導入方法を公式ドキュメントで公開しています.しかしながらこちらもドキュメントの更新は最近まで行われていますが,そもそも元に使用されているPrometheus Operater のHelmのチャートが二年前のPormetheus Operatorのためapi versionなどを書き換えてもv1.16では動きませんでした.(ここでも更にハマった.)

docs.nvidia.com

https://nvidia.github.io/gpu-monitoring-tools/CHANGELOG.md

https://docs.nvidia.com/datacenter/dcgm/1.6/pdf/dcgm-user-guide.pdf

そのため本実装では最新版のPrometheus Operatorに含まれている node exporterにdcgm exporterを組み込み,DCGM exporterが拾ってきたGPUの情報を node exporterに拾ってもらうことでPrometheusに受け渡すことで実現させます.

先ほどと同じく取得してきた「/kube-prometheus/manifests/node-exporter-daemonset.yaml」を以下のように編集します.

node-exporter-daemonset.yaml

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: node-exporter
  name: node-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
+     affinity:
+       nodeAffinity:
+         requiredDuringSchedulingIgnoredDuringExecution:
+           nodeSelectorTerms:
+           - matchExpressions:
+              - key: hardware-type
+                operator: In
+                values:
+                - NVIDIAGPU
      containers:
      - args:
        - --web.listen-address=127.0.0.1:9100
        - --path.procfs=/host/proc
        - --path.sysfs=/host/sys
        - --path.rootfs=/host/root
        - --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/)
        - --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$
+       - --collector.textfile.directory=/run/prometheus
        image: quay.io/prometheus/node-exporter:v0.18.1
        name: node-exporter
        resources:
          limits:
            cpu: 250m
            memory: 180Mi
          requests:
            cpu: 102m
            memory: 180Mi
        volumeMounts:
        - mountPath: /host/proc
          name: proc
          readOnly: false
        - mountPath: /host/sys
          name: sys
          readOnly: false
        - mountPath: /host/root
          mountPropagation: HostToContainer
          name: root
          readOnly: true
+       - mountPath: /run/prometheus
+         name: collector-textfiles
+         readOnly: true  
      - args:
        - --logtostderr
        - --secure-listen-address=[$(IP)]:9100
        - --tls-cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
        - --upstream=http://127.0.0.1:9100/
        env:
        - name: IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        image: quay.io/coreos/kube-rbac-proxy:v0.4.1
        name: kube-rbac-proxy
        ports:
        - containerPort: 9100
          hostPort: 9100
          name: https
        resources:
          limits:
            cpu: 20m
            memory: 40Mi
          requests:
            cpu: 10m
            memory: 20Mi

+     - image: nvidia/dcgm-exporter:1.0.0-beta
+       name: nvidia-dcgm-exporter
+       securityContext:
+         runAsNonRoot: false
+         runAsUser: 0
+       volumeMounts:
+       - name: collector-textfiles
+         mountPath: /run/prometheus      

      hostNetwork: true
      hostPID: true
      nodeSelector:
        kubernetes.io/os: linux
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
      serviceAccountName: node-exporter
      tolerations:
      - operator: Exists
      volumes:
      - hostPath:
          path: /proc
        name: proc
      - hostPath:
          path: /sys
        name: sys
      - hostPath:
          path: /
        name: root

+     - name: collector-textfiles
+       emptyDir:
+         medium: Memory

マニフェスト書き換えの意図を解説しておくと以下のようになります.

affinity:
   nodeAffinity:
     requiredDuringSchedulingIgnoredDuringExecution:
       nodeSelectorTerms:
       - matchExpressions:
         - key: hardware-type
           operator: In
           values:
           - NVIDIAGPU

上記の記述はnodeAffinityを用いてラベルhardware-typeがNVIDIAGPUのnodeのみにexporterを展開するように指定しています. requiredDuringSchedulingIgnoredDuringExecutionを使用しているため必須のスケジューリングポリシーとして指定することが可能です.

- --collector.textfile.directory=/run/prometheus

DCGN exporterは/run/prometheusにtextfile形式の取得してきたGPU情報を吐き出すため,node exporterにそれを回収してもらうためnode exporterに引数として記述しています.

- image: nvidia/dcgm-exporter:1.0.0-beta

docker hubからdcgm-exporterのバージョン1.0.0-betaを取得します.記事執筆時点では最新バージョンになります.

name: nvidia-dcgm-exporter

コンテナの名前を定義しています.

securityContext:
  runAsNonRoot: false
  runAsUser: 0

DCGM exporterがGPU情報を取得できるようにroot権限を与えています.

volumeMounts:
- name: collector-textfiles
  mountPath: /run/prometheus

取得してきたGPU情報を/run/prometheusに吐き出すように記述しています.

- name: collector-textfiles
  emptyDir:
    medium: Memory

GPU情報を吐き出す先のVolumeとしてhost領域をマウントしています.

デプロイ

必要なマニフェストは揃ったのでデプロイしていきます.

まず,以下を実行してPrometheus Operatorをデプロイします.

$ kubectl apply -f /kube-prometheus/manifests/setup
namespace/monitoring created
customresourcedefinition.apiextensions.k8s.io/alertmanagers.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/podmonitors.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/prometheuses.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/prometheusrules.monitoring.coreos.com created
customresourcedefinition.apiextensions.k8s.io/servicemonitors.monitoring.coreos.com created
clusterrole.rbac.authorization.k8s.io/prometheus-operator created
clusterrolebinding.rbac.authorization.k8s.io/prometheus-operator created
deployment.apps/prometheus-operator created
service/prometheus-operator created
serviceaccount/prometheus-operator created
servicemonitor.monitoring.coreos.com/alertmanager created
$
$ kubectl get all -n monitoring
NAME                                      READY   STATUS    RESTARTS   AGE
pod/prometheus-operator-99dccdc56-57pc4   1/1     Running   0          42s

NAME                          TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
service/prometheus-operator   ClusterIP   None         <none>        8080/TCP   43s

NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/prometheus-operator   1/1     1            1           43s

NAME                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/prometheus-operator-99dccdc56   1         1         1       43s

次にPrometheusやGrafanaやnode exporterなどをデプロイしていきます.

$ kubectl apply -f manifests/
alertmanager.monitoring.coreos.com/main created
secret/alertmanager-main created
service/alertmanager-main created
serviceaccount/alertmanager-main created
secret/grafana-datasources created
configmap/grafana-dashboard-apiserver created
configmap/grafana-dashboard-cluster-total created
configmap/grafana-dashboard-controller-manager created
configmap/grafana-dashboard-k8s-resources-cluster created
configmap/grafana-dashboard-k8s-resources-namespace created
configmap/grafana-dashboard-k8s-resources-node created
configmap/grafana-dashboard-k8s-resources-pod created
configmap/grafana-dashboard-k8s-resources-workload created
configmap/grafana-dashboard-k8s-resources-workloads-namespace created
configmap/grafana-dashboard-kubelet created
configmap/grafana-dashboard-namespace-by-pod created
configmap/grafana-dashboard-namespace-by-workload created
configmap/grafana-dashboard-node-cluster-rsrc-use created
configmap/grafana-dashboard-node-rsrc-use created
configmap/grafana-dashboard-nodes created
configmap/grafana-dashboard-persistentvolumesusage created
configmap/grafana-dashboard-pod-total created
configmap/grafana-dashboard-pods created
configmap/grafana-dashboard-prometheus-remote-write created
configmap/grafana-dashboard-prometheus created
configmap/grafana-dashboard-proxy created
configmap/grafana-dashboard-scheduler created
configmap/grafana-dashboard-statefulset created
configmap/grafana-dashboard-workload-total created
configmap/grafana-dashboards created
deployment.apps/grafana created
service/grafana created
serviceaccount/grafana created
servicemonitor.monitoring.coreos.com/grafana created
clusterrole.rbac.authorization.k8s.io/kube-state-metrics created
clusterrolebinding.rbac.authorization.k8s.io/kube-state-metrics created
deployment.apps/kube-state-metrics created
role.rbac.authorization.k8s.io/kube-state-metrics created
rolebinding.rbac.authorization.k8s.io/kube-state-metrics created
service/kube-state-metrics created
serviceaccount/kube-state-metrics created
servicemonitor.monitoring.coreos.com/kube-state-metrics created
clusterrole.rbac.authorization.k8s.io/node-exporter created
clusterrolebinding.rbac.authorization.k8s.io/node-exporter created
daemonset.apps/node-exporter created
service/node-exporter created
serviceaccount/node-exporter created
servicemonitor.monitoring.coreos.com/node-exporter created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
clusterrole.rbac.authorization.k8s.io/prometheus-adapter created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrolebinding.rbac.authorization.k8s.io/prometheus-adapter created
clusterrolebinding.rbac.authorization.k8s.io/resource-metrics:system:auth-delegator created
clusterrole.rbac.authorization.k8s.io/resource-metrics-server-resources created
configmap/adapter-config created
deployment.apps/prometheus-adapter created
rolebinding.rbac.authorization.k8s.io/resource-metrics-auth-reader created
service/prometheus-adapter created
serviceaccount/prometheus-adapter created
clusterrole.rbac.authorization.k8s.io/prometheus-k8s created
clusterrolebinding.rbac.authorization.k8s.io/prometheus-k8s created
servicemonitor.monitoring.coreos.com/prometheus-operator created
prometheus.monitoring.coreos.com/k8s created
rolebinding.rbac.authorization.k8s.io/prometheus-k8s-config created
rolebinding.rbac.authorization.k8s.io/prometheus-k8s created
rolebinding.rbac.authorization.k8s.io/prometheus-k8s created
rolebinding.rbac.authorization.k8s.io/prometheus-k8s created
role.rbac.authorization.k8s.io/prometheus-k8s-config created
role.rbac.authorization.k8s.io/prometheus-k8s created
role.rbac.authorization.k8s.io/prometheus-k8s created
role.rbac.authorization.k8s.io/prometheus-k8s created
prometheusrule.monitoring.coreos.com/prometheus-k8s-rules created
service/prometheus-k8s created
serviceaccount/prometheus-k8s created
servicemonitor.monitoring.coreos.com/prometheus created
servicemonitor.monitoring.coreos.com/kube-apiserver created
servicemonitor.monitoring.coreos.com/coredns created
servicemonitor.monitoring.coreos.com/kube-controller-manager created
servicemonitor.monitoring.coreos.com/kube-scheduler created
servicemonitor.monitoring.coreos.com/kubelet created
$
$ kubectl get all -n monitoring
NAME                                      READY   STATUS    RESTARTS   AGE
pod/alertmanager-main-0                   2/2     Running   0          10m
pod/alertmanager-main-1                   2/2     Running   0          10m
pod/alertmanager-main-2                   2/2     Running   0          10m
pod/grafana-58dc7468d7-p7csw              1/1     Running   0          10m
pod/kube-state-metrics-78b46c84d8-xj9ss   3/3     Running   0          10m
pod/node-exporter-hxvvh                   3/3     Running   0          10m
pod/node-exporter-xlptj                   3/3     Running   0          10m
pod/node-exporter-xlpz5                   3/3     Running   0          10m
pod/prometheus-adapter-5cd5798d96-d9l76   1/1     Running   0          10m
pod/prometheus-k8s-0                      3/3     Running   1          10m
pod/prometheus-k8s-1                      3/3     Running   1          10m
pod/prometheus-operator-99dccdc56-57pc4   1/1     Running   0          36m

NAME                            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
service/alertmanager-main       ClusterIP   10.102.232.189   <none>        9093/TCP                     10m
service/alertmanager-operated   ClusterIP   None             <none>        9093/TCP,9094/TCP,9094/UDP   10m
service/grafana                 NodePort    10.110.75.246    <none>        3000:30039/TCP               10m
service/kube-state-metrics      ClusterIP   None             <none>        8443/TCP,9443/TCP            10m
service/node-exporter           ClusterIP   None             <none>        9100/TCP                     10m
service/prometheus-adapter      ClusterIP   10.104.83.95     <none>        443/TCP                      10m
service/prometheus-k8s          NodePort    10.108.101.37    <none>        9090:30900/TCP               10m
service/prometheus-operated     ClusterIP   None             <none>        9090/TCP                     10m
service/prometheus-operator     ClusterIP   None             <none>        8080/TCP                     36m

NAME                           DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/node-exporter   3         3         3       3            3           kubernetes.io/os=linux   10m

NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/grafana               1/1     1            1           10m
deployment.apps/kube-state-metrics    1/1     1            1           10m
deployment.apps/prometheus-adapter    1/1     1            1           10m
deployment.apps/prometheus-operator   1/1     1            1           36m

NAME                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/grafana-58dc7468d7              1         1         1       10m
replicaset.apps/kube-state-metrics-78b46c84d8   1         1         1       10m
replicaset.apps/prometheus-adapter-5cd5798d96   1         1         1       10m
replicaset.apps/prometheus-operator-99dccdc56   1         1         1       36m

NAME                                 READY   AGE
statefulset.apps/alertmanager-main   3/3     10m
statefulset.apps/prometheus-k8s      2/2     10m

以上でデプロイは完了しました.node exporterがNVIDIAGPU搭載nodeのみに展開されているかどうか確認してください.

ダッシュボードへのアクセス

実際にWEBページからPrometheusとGrafanaにアクセスして起動が成功しているかどうか確認していきます.

Prometheus

まずPrometheusを見てみます. ブラウザで「http://Master NodeのIPアドレス:30900」を入力してアクセスします. すると以下の図20のような画面が表示されます.

top_page_prometheus
図20:Prometheusのトップページ

次に上部バーのStatus->Targetsを選択します.図21のように下の方へスクロールするとnode-exporterの表示があると思うのでエラーが出ずにステータスがUPになっていることを確認してください.

prometheus_node-exporter
図21:Prometheusのnode-exporter起動確認

さらにGPUの温度グラフを表示してみます. 上部バーのGraphを選択して「Expression (pess Shift+Enter for newlines)」の部分に「dcgm_gpu_temp」と入力してExecuteボタンを押してください.その後Graphタブを選択すると図22のようなページが表示されるはずです.

dcgm_gpu_temp_graph
図22:dcgm_gpu_tempグラフの様子

Grafana

Grafanaにブラウザから「http://MasterIPアドレス:30039」でアクセスします. 最初図23のようなログイン画面が出てきますが,今回は事前にアカウントなどを作っていないのでデフォルトアカウントであるusername:admin・password:adminで入ります.

top_page_grafana
図23:Grafanaのトップページ

ログインしたら図24のようなページが表示されると思います. 左端バーの歯車マークを選択してData Sourcesをクリックしてください.

grafana_logined
図24:Grafanaのログイン後ページ

図25のようにGrafanaのData Sourcesはすでに登録されていると思います.

grafana_data_sources
図25:GrafanaのData Sources

登録されていない場合は図26のように設定してください.設定されている場合でも一番下のTestボタンをクリックしてチェックを必ず行ってください.

grafana_data_sources_setting
図26:GrafanaのData Sources設定

左端バー上のGrafanaのロゴをクリックして図24のページに戻った後Homeと書かれたタブをクリックして図27のように「Nodes」を選択します.

grafana_nodes_informattion_move
図27:GrafanaのNodes情報取得画面への遷移方法

すると図28のようにデフォルトのグラフが表示されると思います.今回は簡易的にこのダッシュボードへGPUの温度グラフを追加してみたいと思います.

grafana_default_nodes_dashboard
図28:GrafanaのデフォルトNodesダッシュボード

まず,右上の歯車マークのdashboard settingから図29のようなページにアクセスして「Make Editable」をクリックし,編集可能にします.

nodes_dashboard_setting_page
図29:Nodesダッシュボードの設定画面

ダッシュボードのページに戻ると右上のバーに図30のようなボタンが増えていると思うので選択します.

add_panel_button
図30:Add Panel ボタン

選択すると図31のようにNew Panelが表示されると思うのでAdd Queryを選択します.

new_panel
図31:New Panel

次に図32のページでQueryのタブを選択し,$datasourceを選択してください.

new_panel_setting_top_page
図32:New Panelの設定トップページ

さらに図33のようにMetricsのところに「dcgm_gpu_temp{instance="eriri"}」と入力してエンターを押します.eririには表示したいノードを入力してください.

dcgm_gpu_temp_graph_setting
図33:dcgm_gpu_temp設定の様子

後は保存すれば図34のようになります.

final_dashboard
図34:完成系のダッシュボード

最後に

GrafanaはPromQLを使用できるのでさらにダッシュボードを充実させることができます.今回はAlertの設定を行いませんでしたが,設定することで例えばGPUの温度が規定値以上になったらSlackやメールに通知するなどの応用も考えられます. また,今後CI/CD環境の構築も時間をみて実装していくつもりです.