KubernetesのPodからDAOSを触れるようにした

DAOS用のCSI driverを書いて、KubernetesのPodからアクセス出来るようにした話。
コードは[ここ](https://github.com/tkokamo/csi-driver-daos)

## はじめに

私は元々スパコンのファイルシステム開発をしていたので、その辺りの知見は若干あった。一方で、最近気が向いた時にKubernetesを触るようになって、[CSI(Container Storage Interface)](https://github.com/container-storage-interface/spec/blob/master/spec.md)というものの存在を知った。スパコンのファイルシステムのCSI driverはあまり実装されてなさそうだし、勉強がてら試しに作ってみるかというのがスタートだったのだが、調べてみたらスパコン界隈でデファクトの[LustreのCSI driver](https://github.com/kubernetes-sigs/aws-fsx-csi-driver)は既に存在していたし、[BeeGFSのそれ](https://github.com/NetApp/beegfs-csi-driver)も既に実装されていた。それでまだ誰も手をつけていなさそうだったのがDAOSだったので、DAOSのCSI driverを書いてみることにした。

## DAOSとは

[DAOS (Distributed Asynchronous Object Storage)](https://docs.daos.io/v2.0/)とは、Intelが主導で開発している不揮発性メモリ向けのオブジェクトストレージである。国内での導入例は未だ聞かないが、海外では使われ始めている（今年の[Users Group](https://daosio.atlassian.net/wiki/spaces/DC/pages/4866835353/DUG20?preview=/4866835353/4866837360/7_DUG20_Storing_High-Energy_Physics_data_in_DAOS.pdf)のアジェンダを見る限りロシアのRSJ GroupやCERNなどで導入されている様子）。特に、2022年稼働予定の米国[Aurora](https://www.alcf.anl.gov/support-center/aurora/aurora-introduction)では230PBという大規模な導入が決まっており、Lustre(こちらは150PB)とどのように組み合わせて運用されるか個人的にとても楽しみなところである。Auroraの運用実績次第では、現在スパコンのファイルシステムとしてデファクトのLustreを置き換えるものになると思われる。

### DAOSの基本

DAOSのノード種別はStorage NodeとClient Nodeの二種類に分かれる。前者がいわゆるサーバで後者がクライアントである。Storage Nodeのコンポーネントは、ストレージの管理操作を処理するControl Planeとデータアクセスを処理するData Planeの二つのコンポーネントで構成され、Client Nodeで実行されるアプリケーションやコマンドなどはこれらのコンポーネントにリクエストを送ることでストレージ領域の予約やファイルアクセスを行う。（下図参照）

![](https://i.imgur.com/vqiyzLh.png)

引用元: https://github.com/daos-stack/daos/tree/master/src#daos-components

Client NodeからControl Planeにアクセスするコンポーネントが`dmg`と`daos_agent`の2種類存在するが、前者は管理者向け、後者はユーザ向けのコンポーネントであり、それぞれで出来ることが若干異なる。DAOSを利用する際には、最初にプールというストレージ領域を作成(予約)し、その中に各種APIに沿ったコンテナ(S3のバケットのようなもの)を作成し、利用する。プールの管理（作成やACLの設定など）は管理者の仕事で`dmg`コマンドで可能になっており、コンテナの管理は[一部](https://docs.daos.io/v1.2/admin/pool_operations/#recovering-ownership-of-a-pools-container)を除いてユーザの仕事であり`daos_agent`を通じて行われる（実際に使うのは[`daos`コマンド](https://docs.daos.io/v1.2/user/container/#container-management)）。後ほど実際のコマンドを示しながら再度簡単に説明する。

Data Planeも気になるところだが、今回の記事ではあまりここを知っていても意味がない（+私がまともに説明できない）ので、説明は省略する。[公式ドキュメント](https://docs.daos.io/v1.2/overview/terminology/)や[GitHubのページ](https://github.com/daos-stack/daos/tree/master/src)、[Communityのページ](https://daosio.atlassian.net/wiki/spaces/DC/pages/4806836263/Resources)など資料はたくさんあるので興味がある方はそちらを参照いただきたい。一つだけ補足しておくと、DAOSはオブジェクトストレージと先に述べたが、オブジェクトストレージのIFはどちらかというと開発者向けのもので、ユーザはその上に実装されたPOSIXやMPI-IOなど各種APIを使ってデータにアクセスする。今回はDAOS v1.2を使っているが、ドキュメント化されている最新バージョンのv2.2だと、[Hadoop、Sparkなどもサポート](https://docs.daos.io/v2.2/user/workflow/)しているようである。

DAOSが登場した背景については[過去に記事を書いた](https://qiita.com/tkokamo/items/7e24db7d10dd5e9a9ec7)ので興味があれば読んでいただきたい。

### プール作成～ファイルアクセスまで

上で述べたプール作成～ファイルアクセスまでをコマンドを合わせて紹介する。今回作ったCSI driverの中ではこれから説明する処理の内、コンテナの作成、マウントのコマンドを直にたたいている（DAOSのコマンドはGoで実装されているが、とりあえず動くことを優先してサボっている）。

以下で紹介するコマンドは[ここ](https://github.com/tkokamo/daos_docker/tree/main/multi)に則ってDAOSサーバ/クライアントを起動した後に、クライアントにログインすると使える。ストレージとしてメモリを不揮発性メモリ代わりに使っているので、ホストメモリは64GBほど必要になっている。
起動した時点でファイルシステムのマウントまで終わっているのだが、説明の都合上元々マウントされているファイルシステムはアンマウントし、コンテナやプールは削除した後の実行結果を紹介する。

#### プールの作成

不揮発性メモリとSSDのサイズを個別に指定することも出来るが、ここでは一括で指定している。
今回は不揮発性メモリ(実体はメモリ)のみの環境のため全てSCM割当になっている。

```shell=
# dmg pool create -i --size=8G
Creating DAOS pool with automatic storage allocation: 8.0 GB NVMe + 6.00% SCM
Pool created with 100.00% SCM/NVMe ratio
-----------------------------------------
  UUID          : e4b1427c-092c-48bd-9761-462eea30e306
  Service Ranks : 0                                   
  Storage Ranks : 0                                   
  Total Size    : 8.0 GB                              
  SCM           : 8.0 GB (8.0 GB / rank)              
  NVMe          : 0 B (0 B / rank)                    
```

作成されたpoolのownerは`dmg`を実行したユーザ/グループになっている。

```shell=
# dmg pool get-acl --pool=e4b1427c-092c-48bd-9761-462eea30e306
# Owner: daos_server@
# Owner Group: daos_server@
# Entries:
A::OWNER@:rw
A:G:GROUP@:rw
```

#### コンテナの作成

先に作成したプールとコンテナのタイプを指定することでコンテナを作成することが出来る。

```shell=
# daos cont create --pool=e4b1427c-092c-48bd-9761-462eea30e306 --type=POSIX
Successfully created container 3f9687fc-de8f-49e9-b1c2-7eb681464e2e
```

#### マウント、ファイルアクセス

DAOSをファイルシステムとしてマウントする場合は`dfuse`コマンドを使う。DAOSではFUSEを用いているためアンマウントには`fusermount3`コマンドを使う。

```shell=
# dfuse --mountpoint dfuse --pool=e4b1427c-092c-48bd-9761-462eea30e306 --cont=3f9687fc-de8f-49e9-b1c2-7eb681464e2e
# df dfuse
Filesystem     1K-blocks  Used Available Use% Mounted on
dfuse            7812500   144   7812357   1% /home/daos/dfuse

# ls dfuse
# echo hoge > dfuse/file
# fusermount3 -u dfuse
# dfuse --mountpoint dfuse --pool=e4b1427c-092c-48bd-9761-462eea30e306 --cont=3f9687fc-de8f-49e9-b1c2-7eb681464e2e
# cat dfuse/file 
hoge
```

#### 後片付け

作成の逆をやるだけ。

```shell=
# fusermount3 -u dfuse
# daos cont destroy --pool=e4b1427c-092c-48bd-9761-462eea30e306 --cont=3f9687fc-de8f-49e9-b1c2-7eb681464e2e
Successfully destroyed container 3f9687fc-de8f-49e9-b1c2-7eb681464e2e
# dmg pool destroy --pool=e4b1427c-092c-48bd-9761-462eea30e306
Pool-destroy command succeeded
# dmg pool list
No pools in system
```

## CSIとは

Container Storage Interface (CSI)はKubernetesなどのコンテナオーケストレータ上で実行されるコンテナに対して、ファイルシステムなどのストレージを公開するための標準仕様である。サードパーティのストレージプロバイダはCSI driverを実装し、クラスタにデプロイすることでストレージをコンテナに公開することが出来る。


![](https://i.imgur.com/MFpsORo.png)
引用元: https://github.com/kubernetes/design-proposals-archive/blob/main/storage/container-storage-interface.md#recommended-mechanism-for-deploying-csi-drivers-on-kubernetes

CSI driverはController PluginとNode Pluginというコンポーネントに別れてデプロイする(上図参照)。

* Controller Plugin: ボリュームの管理(作成やノードへのアタッチなど)を行うPodで、StatefulSetまたはDeploymentとしてデプロイされる。Controller Pluginは、Kubernetes APIを監視し必要な処理を行う。例えば、StorageClassのprovisionerとしてCSI Driverが指定されていて、そのStorageClassを指定してPVCが定義された場合、Controller Pluginはこれを検知しボリュームの作成を行う。
* Node Plugin: ノード上でボリュームのフォーマットやマウントを行う。ノード毎に必要なPodのためDaemonSetとしてデプロイされる。Node Pluginはホスト上のパス（図では/var/lib/kubeletと書かれている）にファイルシステムをマウントし、[MountPropagation](https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation)という仕組みを使ってアプリケーションのPodにもマウントを伝搬する。

CSI driverの実体はIdentityサービス、Nodeサービス、Controllerサービスの3種類のgRPCサービスである。Identityサービスはdriverのバージョンなどを返すサービス、NodeサービスはNode Pluginの処理をするサービス、ControllerサービスはController Pluginの処理をするサービスである。Controller Pluginを実行するにはControllerサービスとIdentityサービス、Node Pluginを実行するにはNodeサービスとIdentityサービスが必要になる。

CSI driverをKubernetes環境にデプロイし使えるようにするためは、driverの登録やKubernetes APIなどの監視も行わないと行けない。Kubernetesコミュニティはこれらの処理を行うコンテナ(図の赤で塗りつぶされたコンテナ)を準備しておいてくれていて、これらのコンテナをCSI driverのコンテナ(図の緑で塗りつぶされたコンテナ)のサイドカーとして用いることで、CSI driverの可搬性を損なわずにKubernetes環境にデプロイすることが出来る。サイドカーコンテナとCSI driver間の通信はUnix Domain Socketで行われる。

より詳細な情報は、[参考リンク](https://qiita.com/tkokamo/items/44324307cf6fa1ee2d83#csi関連の参考資料)を参照していただきたい。

## 簡単な作りの説明

今回は既にDAOSのプールは事前に管理者が作成済みという想定で、CSI driverはコンテナの作成、マウントのみを行うため、Node Pluginのみデプロイしていている（コンテナの作成はNode Pluginの仕事ではない気がしているが）。Contoller PluginはデプロイしていないしControllerサービスも一切実装していない。

また、現状はEphemeral Volumesのみ使える。以下のようなマニフェストでPodをデプロイすると、Node PluginのCSI driverコンテナで動いているNodeサービスに対して`NodePublishVolume`リクエストが発行される。リクエストにはマニフェストにかかれている`uid`, `poolid`に加えてホスト上のマウントパスが含まれているで、これらの情報を使って先に書いたDAOSコンテナ作成、ファイルシステムのマウントをコマンド直書きでやっている。実装は[ここ](https://github.com/tkokamo/csi-driver-daos/blob/main/pkg/dfuse/nodeserver.go#L43)（汚い）。

```yaml=
kind: Pod
apiVersion: v1
metadata:
  name: test-app
spec:
...
  volumes:
    - name: dfuse
      csi:
        driver: dfuse.csi.k8s.io
        volumeAttributes:
            uid: "0"
            poolid: "7170ec68-52d5-4f39-98fb-27494cabb47c"
```

Pod終了時には`NodeUnpublishVolume`リクエストが発行されるのでそこでアンマウント、コンテナの削除を行っている。

## 動作例

動かし方については、[README](https://github.com/tkokamo/csi-driver-daos)に書いた以上のことはないのだが、Podからアクセスできている出力を貼っておく。

```shell=
# cat app.yaml
kind: Pod
apiVersion: v1
metadata:
  name: test-app
spec:
  containers:
    - name: test
      image: busybox
      volumeMounts:
      - mountPath: "/data"
        name: dfuse
      command: [ "sleep", "1000000" ]
  volumes:
    - name: dfuse
      csi:
        driver: dfuse.csi.k8s.io
        volumeAttributes:
            uid: "0"
            poolid: "7170ec68-52d5-4f39-98fb-27494cabb47c"
# kubectl create -f app.yaml
pod/test-app created
# kubectl get po
NAME       READY   STATUS    RESTARTS   AGE
test-app   1/1     Running   0          6s
# kubectl exec -it test-app -- /bin/sh
/ # df
Filesystem           1K-blocks      Used Available Use% Mounted on
overlay              959863856 174735984 736299664  19% /
tmpfs                    65536         0     65536   0% /dev
tmpfs                 49402308         0  49402308   0% /sys/fs/cgroup
dfuse                  7812500       281   7812219   0% /data ★
/dev/nvme0n1p2       959863856 174735984 736299664  19% /dev/termination-log
/dev/nvme0n1p2       959863856 174735984 736299664  19% /etc/resolv.conf
/dev/nvme0n1p2       959863856 174735984 736299664  19% /etc/hostname
/dev/nvme0n1p2       959863856 174735984 736299664  19% /etc/hosts
shm                      65536         0     65536   0% /dev/shm
tmpfs                 49402308        12  49402296   0% /var/run/secrets/kubernetes.io/serviceaccount
tmpfs                 49402308         0  49402308   0% /proc/acpi
tmpfs                    65536         0     65536   0% /proc/kcore
tmpfs                    65536         0     65536   0% /proc/keys
tmpfs                    65536         0     65536   0% /proc/timer_list
tmpfs                    65536         0     65536   0% /proc/sched_debug
tmpfs                 49402308         0  49402308   0% /proc/scsi
tmpfs                 49402308         0  49402308   0% /sys/firmware
/ # dd if=/dev/zero of=/data/file bs=1M count=1024
1024+0 records in
1024+0 records out
1073741824 bytes (1.0GB) copied, 0.867793 seconds, 1.2GB/s
```

## おわりに

とりあえず無理やり動くようにしただけなので、色々ひどい状況になっているが、作る中で仕組みが何となくわかってきたので、今後アップデートしていきたい。


## CSI関連の参考資料

1. [KubernetesにおけるContainer Storage Interface (CSI)の概要と検証](https://qiita.com/ysakashita/items/4b56c2577f67f1b141e5)
日本語で書かれているページ。全体像を把握するのに良い。ちなみに検証しているドライバはEOLでサイドカーコンテナのバージョンが古いとかでそのままでは動かない（はず）。
1. [Storage | Kubernetes](https://kubernetes.io/docs/concepts/storage/)
CSIに限った話ではないが、KubernetesのVolumeについての公式ドキュメント。わからないことがあればまずはこれを読む。
1. [Production Kubernetes](https://tanzu.vmware.com/content/ebooks/production-kubernetes)
VMwareから無料で配布されている今年オライリーから出た本。4章のCSIの説明は比較的わかりやすい。というかそれ以外も良さそう。
1. [How to write a Container Storage Interface (CSI) plugin](https://www.velotio.com/engineering-blog/kubernetes-csi-in-action-explained-with-features-and-use-cases)
作り終わった後に見つけたのだが、実装について簡潔にまとめられている。記事の中で出ている[実装](https://github.com/digitalocean/csi-digitalocean/tree/v0.1.1)も参考になりそう。CSI driverを作りたくなったらまずここを読んでみて、後は↓で紹介するspecや実装例を参考にしてがちゃがちゃやるのが良さそう。
1. [Kubernetes CSI in Action: Explained with Features and Use Cases](https://www.velotio.com/engineering-blog/kubernetes-csi-in-action-explained-with-features-and-use-cases)
`How to write...`と同じブログ。他の情報で補完出来る気もするがとりあえず目を通しておくと良さそう。
1. [Introduction - Kubernetes CSI Developer Documentation](https://kubernetes-csi.github.io/docs/introduction.html)
こちらも他の情報で保管できる気がするが、公式ドキュメントなので困ったら参照すると良い。
1. [Container Storage Interface (CSI)](https://github.com/container-storage-interface/spec/blob/master/spec.md)
CSIのspec。RPCの説明やエラーコードの定義など書かれている。
1. [CSI Volume Plugins in Kubernetes Design Doc](https://github.com/kubernetes/design-proposals-archive/blob/main/storage/container-storage-interface.md)
1. [CSIドライバサンプル(EOL)](https://github.com/kubernetes-retired/drivers)
EOLだがNFSのCSI driverの実装（`pkg/nfs`）は小さいので最初に読むのに良い
1. [csi-driver-nfs](https://github.com/kubernetes-csi/csi-driver-nfs)
今回はこちらのコードを一部流用改造して作った。
1. [csi-driver-hostpath]()
ワーカーノードのパスをコンテナに見せるためのCSI driver。簡単に試せるのでとりあえず触りたければこれを触ってみると良い。
1. [csi-test](https://github.com/kubernetes-csi/csi-test)
Kuberenetes-CSIから出されてるテスト実装。記事を書いてる中で見つけたのでほとんど目を通していないが、これをベースに作れば色々はかどりそうな気がする。（もっと早く知りたかった）