Use Container Storage Interface (CSI) with Nomad
今まで Nomad で DB 等のステートフルなワークロードを実行したい場合、以下のような選択肢がありました。
- host_volume スタンザを使用し、ホストのボリュームを使用する。
- Docker を使用したタスクの場合は volumes パラメータを使用し同様にホストのボリュームをマウントして使用する。
しかし、上記の方法はホスト間でのデータを同期しないため、例えばタスクが終了後他のノードでタスクが再起動された場合にデータを保持することができません。
また、ephemeral_disk スタンザというものがあります。こちらの migrate パラメータを有効にすると、タスクを元のノードに配置できない場合にデータをホストを越えて移行することができます。しかし、以下の制約があります。
- データの移行はベストエフォートで行われるため、データは失われる可能性がある。
- 大きいサイズのデータには適していない。
- そもそも想定された使い方はホスト間でのデータの共有ではなく、その名前の通りグループ内で一時的な共有スペースを持つことである。
従って永続的なデータをホストに依存せず持ちたい場合は Portworx のようなサードパーティ製ツールを使用して用意する必要がありました。なお Nomad と Portworx を使用するチュートリアルがあります。
私の以前使用していた環境では Portworx を動かすための要件を満たしておらず、苦肉の策で Docker イメージに全部詰め込む方法を使用していました。
Nomad 0.11 のアップデートで CSI が使用できるようになった 🎉
そんな状況の中、最近ついに Nomad の標準機能としてこの問題に対する答えが発表されました!
CSI とは Container Storage Interface の略で、CO (コンテナオーケストレーションシステム: Kubernetes や Nomad 等) 側でストレージを管理するための仕様です。これまでも Kubernetes からストレージを管理する機能はありましたが、以下の問題があり、CSI 誕生のモチベーションになったようです。
- Kubernetes が動作する環境は様々であり、そのためストレージを管理するための API も様々になる。これらを扱うためのボリュームプラグインは Kubernetes のコアリポジトリ内にあった。
- そのためボリュームプラグインのリリースは Kubernetes 本体のリリースに合わせる必要があり、多くのプラグイン開発者にとって苦痛だった。
- 既存の Flex ボリュームプラグインはこの問題に対処しようとしたが、完全な方法ではなかった。
CSI が誕生したことでボリュームプラグインは Kubernetes 本体の外で開発できるようになり、プラグイン自体がコンテナ化されることで標準の Kubernetes へデプロイすることも用意になりました。
上記の話は Kubernetes の公式ブログで書かれており、ナルホデュウスだなあと思いました。
話を Nomad に戻すと、この CSI は Kubernetes 用ではなく仕様に則っていればどの CO でも使えるので、Nomad でも採用されています。
また CSI 対応のストレージは以下にまとめられていました。
CSI の仕様は以下。
DigitalOcean 上で動いている Nomad で CSI を使う
このブログは DigitalOcean 上の Nomad クラスター内で動いているので、ここに DigitalOcean のブロックストレージを CSI を通じて接続させてみます。
ボリュームの作成
Terraform で作成。使用中の Droplet のファイルシステムが xfs なのでそれに合わせて xfs のボリュームを作成します。
resource "digitalocean_volume" "nomad_volume" {
region = "sgp1"
name = "nomad-volume"
size = 20
initial_filesystem_type = "xfs"
description = "volume for nomad persistent storage"
}
なお、ボリューム ID が後の設定で必要になるので、 output しておくと楽です。
output "nomad_volume_id" {
value = digitalocean_volume.nomad_volume.id
}
Nomad にボリュームを登録
作成したボリュームの情報を Nomad に登録します。今回の場合、以下の volume.hcl を作成します。mount_options で xfs を指定することを忘れないようにします (デフォルトで ext4 が使われるのでマウントに失敗する)。また、access_mode ではボリュームを同時に使用可能にするかを指定します。single-node-writer は 1 つのノードからの読み書きのみを許可します。ほとんどの CSI プラグインはシングルモードのみを許可しているようです。対応状況は各ストレージプロバイダーの情報を確認する必要があります。DigitalOcean の場合はシングルノードのみです。
type = "csi"
id = "nomad-volume"
name = "nomad-volume"
external_id = "<作成したボリュームの ID>"
access_mode = "single-node-writer"
attachment_mode = "file-system"
plugin_id = "do-volume0"
mount_options {
fs_type = "xfs"
mount_flags = ["rw"]
}
その他設定値の詳細は以下にまとまっています。
作成した設定を使用してボリュームを登録します。
$ nomad volume register volume.hcl
以下のように登録したボリュームを確認できます。既に Controllers Healthy や Nodes Healthy が記録されていますが、後述のジョブを起動するまでは 0 になっているはず。
$ nomad volume status
Container Storage Interface
ID Name Plugin ID Schedulable Access Mode
nomad-vo nomad-volume do-volume0 true single-node-writer
$ nomad volume status nomad-volume
ID = nomad-volume
Name = nomad-volume
External ID = <作成したボリュームの ID>
Plugin ID = do-volume0
Provider = dobs.csi.digitalocean.com
Version = v1.2.0
Schedulable = true
Controllers Healthy = 3
Controllers Expected = 3
Nodes Healthy = 3
Nodes Expected = 3
Access Mode = single-node-writer
Attachment Mode = file-system
Mount Options = fs_type: xfs flags: rw
Namespace = default
Allocations
No allocations placed
改めて注意すべきなのはシングルモードのみ対応している CSI プラグインでは 同時に 2 つ以上のノードからアクセスすることはできない ということです。複数のノードから同時にボリュームを扱う必要がある場合は解決策にならないので気を付けましょう。例えば、複数のノードで起動しているジョブから 1 つのボリュームに対しログや画像などを書き込む、みたいなことは出来ない。
CSI プラグインをデプロイする
CSI プラグインをジョブとして動かします。CSI プラグインを使用することで登録したボリュームへ各 Nomad ジョブがアクセス出来るようになります。プラグインとして DigitalOcean が公開している Docker イメージを使用します。最新は 1.2.0 です。プラグインは GitHub で公開されています。
- digitalocean/do-csi-plugin:v1.2.0
- GitHub - digitalocean/csi-digitalocean: A Container Storage Interface (CSI) Driver for DigitalOcean Block Storage
job "plugin-do-volume" {
datacenters = ["sgp1"]
type = "system"
group "monolith" {
task "plugin" {
driver = "docker"
config {
image = "digitalocean/do-csi-plugin:v1.2.0"
args = [
"--endpoint=unix://csi/csi.sock",
"--token=<DigitalOcean のアクセストークン>",
"--url=https://api.digitalocean.com/",
]
privileged = true
}
csi_plugin {
id = "do-volume0"
type = "monolith"
mount_dir = "/csi"
}
resources {
cpu = 500
memory = 512
}
}
}
}
- 全てのノード上のタスクでボリュームを扱えるようにしておきたいのでジョブのタイプに system を指定しています。
- DigitalOcean の CSI Docker イメージの使い方がどこにも書いていないみたいなので必要な args はこの辺から発掘しました…。
- csi_plugin スタンザで先ほど登録したボリュームの情報を指定します。
- type は monolith を選択します。type はボリュームをマウントするクライアントノードであることを示す node タイプと、ストレージプロバイダーへ API 通信する controller タイプがあります。monolith は node 兼 controller なタイプです。DigitalOcean の CSI プラグインの場合、monolith しか使えませんでした。この辺の node やら controller やらについては仕様書に詳細が書いてあります。
上記 CSI プラグインをデプロイします。
$ nomad job plan plugin-do-volume.nomad
$ nomad job run plugin-do-volume.nomad
すると nomad volume status <ボリューム名>
で Nodes Healthy と Controllers Healthy がノードの数だけ Healthy になるはずです。これで Nomad タスクからボリュームを使う準備ができました!
Nomad タスクからボリュームを使う
ファイルを作成するバッチジョブと、ファイルを ls するバッチジョブを作成して動作確認してみます。普通であれば、ジョブが終了したらリソースは破棄され別のジョブから参照することはできません。CSI を通じて接続したボリュームにファイルを書き込むことで、別のジョブから作成されたファイルを読めることが期待されます。また、他のノードからジョブが実行された場合でも、ファイルを確認することができるはずです。
ファイルを作成するジョブは以下。外部ボリュームを /nomad-volume へマウントしそこにファイルを作成します。
job "put-file" {
datacenters = ["sgp1"]
type = "batch"
group "put-file" {
count = 1
volume "nomad-volume" {
type = "csi"
read_only = false
source = "nomad-volume"
}
task "put-file" {
driver = "docker"
volume_mount {
volume = "nomad-volume"
destination = "/nomad-volume"
read_only = false
}
config {
image = "centos"
security_opt = ["label=disable"]
args = ["touch", "/nomad-volume/hello-${NOMAD_ALLOC_ID}"]
}
}
}
}
ファイルを ls するジョブは以下。同じく /nomad-volume をマウントし、内容を表示します。
job "get-file" {
datacenters = ["sgp1"]
type = "batch"
group "get-file" {
count = 1
volume "nomad-volume" {
type = "csi"
read_only = false
source = "nomad-volume"
}
task "get-file" {
driver = "docker"
volume_mount {
volume = "nomad-volume"
destination = "/nomad-volume"
read_only = false
}
config {
image = "centos"
security_opt = ["label=disable"]
args = ["ls", "-l", "/nomad-volume/"]
}
}
}
}
security_opt = ["label=disable"]
の部分ですがこれが無いとボリュームのマウントまではできますがディレクトリへアクセスしても Permission denied となってしまいます。SELinux 周りが関わっているようなのですがまだ調べきれておらず…。Nomad クライアントの設定で selinuxlabel = "z"
と設定しているんだけど…。
とりあえずそれぞれ実行してみます!まずファイルの作成ジョブを動かします。
$ nomad job run put-test.hcl
ジョブが成功しました。
$ nomad job status put-file
...
Summary
Task Group Queued Starting Running Failed Complete Lost
put-file 0 0 0 0 1 0
Allocations
ID Node ID Task Group Version Desired Status Created Modified
00379e67 fa85b1a6 put-file 0 run complete 3m6s ago 2m48s ago
ノードは fa85b1a6 が使われていますね。以下の 3 台のうちの 1 つです。
$ nomad node status
ID DC Name Class Drain Eligibility Status
fa85b1a6 sgp1 nomad-server-0 <none> false eligible ready
40f26a1d sgp1 nomad-server-1 <none> false eligible ready
90ef5f33 sgp1 nomad-server-2 <none> false eligible ready
次にファイルを表示するジョブを動かします。
$ nomad job run get-test.hcl
成功しました。同じノードで動いていました。ログを見ると別のジョブで作成したファイルが存在していることがわかります!hello- の後ろの alloc ID が put-file ジョブの alloc ID になっているので、put-file ジョブで作成したファイルが見れていることがわかります。
$ nomad job status get-file
...
Summary
Task Group Queued Starting Running Failed Complete Lost
get-file 0 0 0 0 1 0
Allocations
ID Node ID Task Group Version Desired Status Created Modified
bc6fcb18 fa85b1a6 get-file 0 run complete 39s ago 18s ago
$ nomad alloc logs bc6fcb18
...
hello-00379e67-a7a9-f2d1-796e-426ebf7ba027
何度か get-file ジョブを実行すると、別のノード (90ef5f33) にタスクが割り当てられ、実行されました。
Allocations
ID Node ID Task Group Version Desired Status Created Modified
9104bac0 90ef5f33 get-file 4 run complete 1m8s ago 49s ago
4fbc8997 fa85b1a6 get-file 2 run complete 1m36s ago 1m17s ago
bc6fcb18 fa85b1a6 get-file 0 run complete 6m38s ago 6m17s ago
ログを見ると、ちゃんと put-file ジョブで作成されたファイルが見れています!
$ nomad alloc logs 9104bac0
hello-00379e67-a7a9-f2d1-796e-426ebf7ba027
これで以下が確認できました。✨
- ジョブが終了してもデータが保持されること。
- 異なるノードで同じデータを参照できること。
課題
DigitalOcean の CSI プラグインはシングルモードのみ対応しているので、複数のノードから同時に接続が行われるような操作 (count を 2 以上にするとか) をするとジョブが失敗します。
Allocations
ID Node ID Task Group Version Desired Status Created Modified
bc7ff658 40f26a1d put-file 0 run complete 1m33s ago 1m7s ago
41fcf137 fa85b1a6 put-file 0 run failed 1m33s ago 28s ago
b545d10a 90ef5f33 put-file 0 run failed 1m33s ago 28s ago
が、なぜかそれ以降 1 つのノードから接続を行う場合も失敗するようになってしまいます…。1 日くらい放って置いたら直ったのですが、どう直せばいいのか分からず…。ボリュームを再作成して設定し直すと直りますが、この方法を本番で使うのは無理。一旦この状態になるとボリュームの deregister や CSI プラグインを停止しても直らないのでどうしたもんか、と思っています。もっと CSI について理解しないとどこが悪さしているのか特定できない。おこ。
あと、security_opt = ["label=disable"]
についてもよく分からんので理解したい。多分 disable にしないで適切に設定すべきところ。この辺が手掛かりになりそう。
- Nomad connect functionality not working with SELinux enabled · Issue #7290 · hashicorp/nomad · GitHub
- Drivers: Docker | Nomad by HashiCorp
- Docker Documentation - Security configuration
シングルモードしか使えない話については DigitalOcean の CSI リポジトリに気になるディレクトリ (pod-multi-volumes) があり、もしかして使える…?もしくは将来使えるようになるとか。
ここが更新されてないだけ??
感想
Nomad with CSI はまだ出たばかりで情報が少ないのもありだいぶ苦労した…。CSI よく分からんし。
が、動かせたことで使っていくモチベーションが上がったので引き続き調査していく。💪
参考リンク
チュートリアル:
- HashiCorp Nomad Container Storage Interface (CSI) Beta
- Stateful Workloads with Container Storage Interface | Nomad - HashiCorp Learn
CSI について:
- Introducing Container Storage Interface (CSI) Alpha for Kubernetes - Kubernetes
- spec/spec.md at master · container-storage-interface/spec · GitHub
- Kubernetesに初めて実装された「Container Storage Interface」は、Kubernetes、Mesos、Docker、Cloud Foundryが共同策定したコンテナ用ストレージAPI - Publickey
ドキュメント:
- Drivers - Kubernetes CSI Developer Documentation
- csi_plugin Stanza - Job Specification | Nomad by HashiCorp
- Commands: volume register | Nomad by HashiCorp
- Drivers: Docker | Nomad by HashiCorp
リポジトリ: