Cloudflare Computer测试体系全解:从单元测试到load.bench与stub-soak浸泡测试
在 Nomad 中使用 Terraform 部署 DigitalOcean CSI 卷:端到端实战指南
本指南完整讲解 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 核心开发团队提供支持。
使用本演示需要满足以下前提:
- 已运行一个 Nomad 集群,且客户端启用了 Docker 任务驱动(
dockerdriver); - 一个 DigitalOcean 账号,以及对应的 API Token(
do_token); - 一台可以访问该 Nomad 集群的工作机,用于执行
terraform apply与nomadCLI 命令; - 安装 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 |
region | nyc1 | 卷创建的数据中心区域 |
volume_id | nomad-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 ID | CSI 插件标识,必须与 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/nomadProvider 的hcl2特性直接渲染作业规范,因此NOMAD_ADDR指向的集群必须可用且已被 Provider 正确鉴权; - 如需调整云盘大小或文件系统,直接修改 main.tf 中
digitalocean_volume资源的size/initial_filesystem_type后重新apply即可。
8. 扩展阅读
- 更多 CSI 插件示例(Ceph、OpenStack Cinder、HostPath、NFS、Portworx 等)见 demo/csi 目录;
- CSI 卷的数据结构与插件类型定义见 nomad/structs/csi.go;
- CSI 卷的 API 客户端封装(含能力、挂载选项等)见 api/csi.go;
- 客户端侧 CSI 挂载 hook 实现见 client/allocrunner/csi_hook.go,其测试见 client/allocrunner/csi_hook_test.go;
nomad volume相关 CLI 命令源码集中在 command 目录(如 command/volume_status_csi.go、command/volume_register_csi.go)。
更多推荐
所有评论(0)