Use Container Storage Interface (CSI) with Nomad

Use Container Storage Interface (CSI) with Nomad

今まで Nomad で DB 等のステートフルなワークロードを実行したい場合、以下のような選択肢がありました。

  • host_volume スタンザを使用し、ホストのボリュームを使用する。
  • Docker を使用したタスクの場合は volumes パラメータを使用し同様にホストのボリュームをマウントして使用する。

しかし、上記の方法はホスト間でのデータを同期しないため、例えばタスクが終了後他のノードでタスクが再起動された場合にデータを保持することができません。

また、ephemeral_disk スタンザというものがあります。こちらの migrate パラメータを有効にすると、タスクを元のノードに配置できない場合にデータをホストを越えて移行することができます。しかし、以下の制約があります。

  • データの移行はベストエフォートで行われるため、データは失われる可能性がある。
  • 大きいサイズのデータには適していない。
  • そもそも想定された使い方はホスト間でのデータの共有ではなく、その名前の通りグループ内で一時的な共有スペースを持つことである。

従って永続的なデータをホストに依存せず持ちたい場合は Portworx のようなサードパーティ製ツールを使用して用意する必要がありました。なお Nomad と Portworx を使用するチュートリアルがあります。

私の以前使用していた環境では Portworx を動かすための要件を満たしておらず、苦肉の策で Docker イメージに全部詰め込む方法を使用していました。

Nomad クラスター上で Ghost ブログを動作させる
このブログは数年間単一のサーバーで稼働していました。 学習も兼ねてわざわざ自前でサーバーを運用していたのですが、最近新しい構成に移したくなり、Kubernetesの流行もあってクラスターやマイクロサービスといった時代の流れに乗るべく、Nomad クラスターへの移行を行いました。 Nomad クラスターの作成までは下記記事にまとめています (インフラやクラスターの設定等はここからさらに変更しています)。 * Nomad クラスターを作成する [https://blog.lorentzca.me/create-nomad-cluster-on-digitalocean/] 今回は…

Nomad 0.11 のアップデートで CSI が使用できるようになった 🎉

そんな状況の中、最近ついに Nomad の標準機能としてこの問題に対する答えが発表されました!

HashiCorp Nomad Container Storage Interface (CSI) Beta
Nomad 0.11 includes a new way to manage external storage volumes for your stateful workloads—support for the Container Storage Interface (CSI). We’ll provide an overview of this new capability in this blog post.

CSI とは Container Storage Interface の略で、CO (コンテナオーケストレーションシステム: Kubernetes や Nomad 等) 側でストレージを管理するための仕様です。これまでも Kubernetes からストレージを管理する機能はありましたが、以下の問題があり、CSI 誕生のモチベーションになったようです。

  • Kubernetes が動作する環境は様々であり、そのためストレージを管理するための API も様々になる。これらを扱うためのボリュームプラグインは Kubernetes のコアリポジトリ内にあった。
  • そのためボリュームプラグインのリリースは Kubernetes 本体のリリースに合わせる必要があり、多くのプラグイン開発者にとって苦痛だった。
  • 既存の Flex ボリュームプラグインはこの問題に対処しようとしたが、完全な方法ではなかった。

CSI が誕生したことでボリュームプラグインは Kubernetes 本体の外で開発できるようになり、プラグイン自体がコンテナ化されることで標準の Kubernetes へデプロイすることも用意になりました。

上記の話は Kubernetes の公式ブログで書かれており、ナルホデュウスだなあと思いました。

Introducing Container Storage Interface (CSI) Alpha for Kubernetes
One of the key differentiators for Kubernetes has been a powerful volume plugin system that enables many different types of storage systems to: Automatically create storage when required. Make storage available to containers wherever they’re scheduled. Automatically delete the storage when no longe…

話を Nomad に戻すと、この CSI は Kubernetes 用ではなく仕様に則っていればどの CO でも使えるので、Nomad でも採用されています。

また CSI 対応のストレージは以下にまとめられていました。

Drivers - Kubernetes CSI Developer Documentation
This site documents how to develop and deploy a Container Storage Interface (CSI) driver on Kubernetes.

CSI の仕様は以下。

container-storage-interface/spec
Container Storage Interface (CSI) Specification. Contribute to container-storage-interface/spec development by creating an account on GitHub.

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 で公開されています。

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 にしないで適切に設定すべきところ。この辺が手掛かりになりそう。

シングルモードしか使えない話については DigitalOcean の CSI リポジトリに気になるディレクトリ (pod-multi-volumes) があり、もしかして使える…?もしくは将来使えるようになるとか。

ここが更新されてないだけ??

感想

Nomad with CSI はまだ出たばかりで情報が少ないのもありだいぶ苦労した…。CSI よく分からんし。

が、動かせたことで使っていくモチベーションが上がったので引き続き調査していく。💪

参考リンク

チュートリアル:

CSI について:

ドキュメント:

リポジトリ: