在 Nomad 中使用 Terraform 部署 DigitalOcean CSI 卷:端到端实战指南

【免费下载链接】nomad Nomad is an easy-to-use, flexible, and performant workload orchestrator that can deploy a mix of microservice, batch, containerized, and non-containerized applications. Nomad is easy to operate and scale and has native Consul and Vault integrations. 【免费下载链接】nomad 项目地址: https://gitcode.com/gh_mirrors/no/nomad

本指南完整讲解 demo/csi/digitalocean 目录中的 Terraform 演示:如何在一个已运行 Docker 任务驱动的 Nomad 集群上,通过 CSI(Container Storage Interface)将 DigitalOcean 块存储卷接入 Nomad,实现"创建云盘 → 部署 CSI 插件 → 注册卷 → 消费卷"的全自动化闭环。读完本文,你将掌握 nomad volume status 的完整字段含义、csi_plugin 与 volume 任务组声明的写法,以及 Terraform 的 nomad_volume / nomad_job Provider 资源如何协同工作。

1. 演示概览与前置条件

DigitalOcean CSI 演示位于 demo/csi/digitalocean,属于仓库 demo/csi 目录下众多 CSI 插件示例之一(同目录还包括 ceph-csi-plugin、hostpath、nfs、portworx-csi-plugin 等)。demo/csi/README.md 明确说明:这些 demo 用于展示"为这些插件注册 CSI 插件任务和卷"的流程,由社区贡献、不被 Nomad 核心开发团队提供支持。

使用本演示需要满足以下前提:

  1. 已运行一个 Nomad 集群,且客户端启用了 Docker 任务驱动(docker driver);
  2. 一个 DigitalOcean 账号,以及对应的 API Token(do_token);
  3. 一台可以访问该 Nomad 集群的工作机,用于执行 terraform apply 与 nomad CLI 命令;
  4. 安装 Terraform(versions.tf 要求版本 >= 0.13)以及 hashicorp/nomad、digitalocean/digitalocean 两个 Provider。

2. 一键部署:两条命令打通全流程

根据 demo/csi/digitalocean/README.md,整个演示的部署只需两步:

# 指向你的 Nomad 集群地址(将 IP_ADDRESS 替换为实际地址)
export NOMAD_ADDR=http://${IP_ADDRESS}:4646

# 执行 Terraform,传入 DigitalOcean API Token
terraform apply -var do_token=${DIGITALOCEAN_TOKEN}

NOMAD_ADDR 环境变量让 hashicorp/nomad Provider 与 nomad CLI 都能定位到集群的 HTTP API 端点(默认端口 4646)。-var do_token 覆盖 variables.tf 中声明的 do_token 变量——该变量没有默认值,因此必须显式提供,否则 Terraform 会在 apply 前报错。

Terraform 变量说明

variables.tf 声明了三个变量:

变量默认值说明
do_token无(必填)DigitalOcean API Token
regionnyc1卷创建的数据中心区域
volume_idnomad-csi-test卷在 Nomad 中的注册 ID(也是卷名)

而 terraform.tf 仅做了 Provider 的 Token 注入:

provider "digitalocean" {
  token = var.do_token
}

versions.tf 则固定了 Provider 来源与 Terraform 版本约束:

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
    }
    nomad = {
      source = "hashicorp/nomad"
    }
  }
  required_version = ">= 0.13"
}

一次 terraform apply 会依次完成四个动作:创建 DigitalOcean 块存储卷、通过 nomad_job 提交 CSI 插件任务、通过 nomad_volume 在 Nomad 中注册卷、再通过第二个 nomad_job 提交消费该卷的应用任务(Redis)。

3. main.tf 拆解:四条资源如何协同

main.tf 是演示的核心编排逻辑,包含四个资源块。

3.1 创建 DigitalOcean 卷

resource "digitalocean_volume" "test_volume" {
  region                  = var.region
  name                    = "csi-test-volume"
  size                    = 50
  initial_filesystem_type = "ext4"
  description             = "a volume for testing Nomad CSI"
}

在 nyc1 区域创建一个 50GB 的 ext4 文件系统卷,ID 随后通过 external_id 关联到 Nomad 的卷注册中。

3.2 提交 CSI 插件任务

resource "nomad_job" "plugin" {
  jobspec = templatefile("${path.module}/plugin.nomad", { token = var.do_token })

  hcl2 {
    enabled = true
  }
}

使用 templatefile 把 do_token 渲染进 plugin.nomad,再作为 HCL2 作业规范提交给 Nomad。插件任务是一个 type = "system" 的作业,会调度到每个匹配的客户端节点上。

3.3 注册 CSI 卷

resource "nomad_volume" "test_volume" {
  volume_id             = var.volume_id
  name                  = var.volume_id
  type                  = "csi"
  plugin_id             = "digitalocean"
  external_id           = digitalocean_volume.test_volume.id
  deregister_on_destroy = true

  capability {
    access_mode     = "single-node-writer"
    attachment_mode = "block-device"
  }
}

关键点:

  • external_id 直接引用上面 digitalocean_volume 资源的 ID,实现 Terraform 状态层面的依赖;此时 CSI 卷已经在 DigitalOcean 侧真正创建,Nomad 注册的只是一个指向云盘 UUID 的 CSI 卷声明;
  • plugin_id = "digitalocean" 必须与插件任务中 csi_plugin 块的 id 一致,Nomad 据此把卷与插件实例关联;
  • capability 声明了卷的访问模式与挂载模式(见第 5 节);
  • deregister_on_destroy = true 保证在 terraform destroy 时自动注销卷,避免在 Nomad 中留下残留卷记录。

3.4 提交消费卷的应用任务

resource "nomad_job" "redis" {
  jobspec    = templatefile("${path.module}/volume-job.nomad", { volume_id = nomad_volume.test_volume.id })
  depends_on = [nomad_volume.test_volume]

  hcl2 {
    enabled = true
  }
}

volume-job.nomad 通过模板变量 volume_id 拿到卷在 Nomad 中的注册 ID(即 var.volume_id,默认 nomad-csi-test),depends_on 确保卷完成注册后再提交应用作业,避免调度竞争。

4. 插件任务详解:plugin.nomad

plugin.nomad 是 Nomad 调度 CSI 插件的方式:

job "digitalocean" {
  datacenters = ["dc1"]
  type        = "system"

  group "csi" {
    task "plugin" {
      driver = "docker"

      config {
        image = "digitalocean/do-csi-plugin:v2.1.1"
        args = [
          "--endpoint=${CSI_ENDPOINT}",
          "--token=${token}",
          "--url=https://api.digitalocean.com/",
        ]

        privileged = true
      }

      csi_plugin {
        id        = "digitalocean"
        type      = "monolith"
        mount_dir = "/csi"
      }

      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}

几个要点:

  • type = "system":插件必须运行在集群中的每个(相关)节点上,因此使用系统作业而非服务作业;
  • privileged = true:CSI 插件需要在容器内执行挂载/卸载等特权操作;
  • ${CSI_ENDPOINT}:这是 Nomad 注入的环境变量,指向插件与 Nomad 通信用的 unix socket 路径;
  • csi_plugin 块:id 用于标识插件(与 nomad_volume.plugin_id 对应),type = "monolith" 表示同一实例同时承担 Controller 与 Node 两类 RPC。

csi_plugin 块的源码语义

csi_plugin 块的字段定义在 nomad/structs/csi.go 的 TaskCSIPluginConfig 结构体中,其语义为:

  • ID:插件标识符,官方建议使用插件的 FQDN;
  • Type:CSIPluginType 枚举,取值见 nomad/structs/csi.go:
    • node:仅执行 Node RPC;
    • controller:仅执行 Controller RPC;
    • monolith:同时承担 Controller 与 Node RPC(本 demo 所用);
  • MountDir:插件在容器内创建通信 socket 的目录,默认 /csi。Nomad 期望插件在该目录中创建名为 csi.sock 的 socket(常量 CSISocketName 定义在 nomad/structs/csi.go),并在同目录下的 volumes 子目录(CSIIntermediaryDirname,见 nomad/structs/csi.go)中创建卷的中间挂载点;
  • StagePublishBaseDir:插件在容器内执行 staging/publish 挂载的基础目录,默认 /local/csi;
  • HealthTimeout:CSI 插件不健康时,任务在等待该时长后会被杀掉。

csi_plugin 块由客户端 AllocRunner 的 csi_hook(client/allocrunner/csi_hook.go)在分配运行前介入处理,它会等待远程 CSI 卷完成挂载后再继续启动任务;对不依赖 CSI 卷的分配而言该 hook 是 noop。整个插件生命周期(健康检查、指纹上报、注册进插件目录)由 client/pluginmanager/csimanager 管理。

5. 消费卷的任务详解:volume-job.nomad

volume-job.nomad 展示 Nomad 作业如何声明并挂载 CSI 卷:

job "example" {
  datacenters = ["dc1"]

  group "cache" {
    volume "test" {
      type            = "csi"
      source          = "${volume_id}"
      access_mode     = "single-node-writer"
      attachment_mode = "block-device"
    }

    task "redis" {
      driver = "docker"

      config {
        image = "redis:7"

        port_map {
          db = 6379
        }
      }

      volume_mount {
        volume      = "test"
        destination = "/test"
      }

      resources {
        cpu    = 500
        memory = 256

        network {
          mbits = 14
          port "db" {}
        }
      }
    }
  }
}
  • 组级 volume 块:声明本组需要类型为 csi 的卷,source 指向第 3.3 节注册的卷 ID(nomad-csi-test,经 Terraform 模板渲染);
  • 任务级 volume_mount 块:把组级卷 test 挂载到容器内 /test 路径,Redis 的数据即可持久化到 DigitalOcean 云盘;
  • access_mode / attachment_mode:声明了使用方式,必须与卷注册时的 capability 匹配(见下节);
  • 资源块中声明了 mbits = 14 的带宽与 port "db" {}(6379),供应用对外提供服务。

6. 验证成果:解读 nomad volume status 输出

部署完成后,用 README 中的命令验证卷是否注册成功:

$ nomad volume status nomad-csi
ID                   = nomad-csi-test
Name                 = nomad-csi-test
External ID          = 58c4ef75-25d1-11eb-a381-0a58ac1449b9
Plugin ID            = digitalocean
Provider             = dobs.csi.digitalocean.com
Version              = v2.1.1
Schedulable          = true
Controllers Healthy  = 1
Controllers Expected = 1
Nodes Healthy        = 1
Nodes Expected       = 1
Access Mode          = single-node-writer
Attachment Mode      = block-device
Mount Options        = <none>
Namespace            = default

Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created  Modified
8d223dc7  ce46add9  cache       0        run      running  21s ago  3s ago

该输出由 command/volume_status_csi.go 的 formatCSIBasic 渲染,各字段含义如下:

字段含义
ID / Name卷在 Nomad 中的注册 ID 与名称(即 var.volume_id)
External ID云厂商侧的真实卷 ID,此处是 DigitalOcean 卷 UUID,由 Terraform 的 external_id 注入
Plugin IDCSI 插件标识,必须与 csi_plugin.id 一致
Provider插件上报的 CSI 驱动名称(dobs.csi.digitalocean.com)
Version插件版本(v2.1.1,与镜像 tag 对应)
Schedulable卷是否可被调度器使用(true 表示可被分配)
Controllers Healthy / Expected健康/期望的 Controller 插件实例数(本例均为 1,即 monolith 插件的 Controller 侧)
Nodes Healthy / Expected健康/期望的 Node 插件实例数(均为 1,即插件所运行的客户端节点)
Access Mode访问模式:single-node-writer(单节点读写)
Attachment Mode挂载模式:block-device(块设备)
Mount Options挂载选项,未配置时为 <none>
Namespace卷所属命名空间(default)
Allocations当前使用该卷的分配列表,此处显示 cache 组的 Redis 任务正在运行,即卷已被成功消费

关于访问模式与挂载模式,其枚举定义在 api/csi.go:

  • Attachment Mode:block-device(块设备直通)与 file-system(文件系统挂载)两种;
  • Access Mode:single-node-reader-only、single-node-writer、multi-node-reader-only、multi-node-single-writer、multi-node-multi-writer 五种。

示例中同时采用 block-device 与 single-node-writer,意味着该卷以块设备形式被单节点独占读写。需要特别说明:本 demo 的卷声明为块设备模式,但消费任务 volume-job.nomad 通过 volume_mount 将其挂载为容器内路径 /test,实际使用效果由 DigitalOcean CSI 插件 v2.1.1 的具体实现决定。

7. 清理与运维提示

  • 需要拆除整个演示时,执行 terraform destroy:nomad_volume 的 deregister_on_destroy = true 会先注销卷,插件任务与 Redis 任务随之删除;
  • 若集群有多个节点,type = "system" 的插件作业会调度到每个节点,nomad volume status 中的 Nodes 健康数会相应增加;
  • 本 demo 使用 hashicorp/nomad Provider 的 hcl2 特性直接渲染作业规范,因此 NOMAD_ADDR 指向的集群必须可用且已被 Provider 正确鉴权;
  • 如需调整云盘大小或文件系统,直接修改 main.tf 中 digitalocean_volume 资源的 size / initial_filesystem_type 后重新 apply 即可。

8. 扩展阅读

【免费下载链接】nomad Nomad is an easy-to-use, flexible, and performant workload orchestrator that can deploy a mix of microservice, batch, containerized, and non-containerized applications. Nomad is easy to operate and scale and has native Consul and Vault integrations. 【免费下载链接】nomad 项目地址: https://gitcode.com/gh_mirrors/no/nomad

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐