mirror of
https://github.com/kemko/nomad.git
synced 2026-01-10 12:25:42 +03:00
also remove Error logs from client rpc and promote plugin Debug logs to Error (since they have more info in them)
430 lines
13 KiB
Plaintext
430 lines
13 KiB
Plaintext
---
|
|
layout: docs
|
|
page_title: Host volume plugins
|
|
description: |-
|
|
Learn how to implement Nomad's dynamic host volume plugins specification so that you can dynamically configure persistent storage on your client nodes. Review examples that create a directory and a Linux filesystem.
|
|
Reference plugin arguments and environment variables.
|
|
---
|
|
|
|
# Host volume plugins
|
|
|
|
This page describes the plugin specification for
|
|
[dynamic host volumes][stateful-workloads], with examples, so you can write
|
|
your own plugin to dynamically configure persistent storage on your Nomad
|
|
client nodes.
|
|
|
|
If you do not need to write your own plugin, consider Nomad's built-in
|
|
[`mkdir` plugin][mkdir_plugin], which creates a directory on the host.
|
|
|
|
If you have more complex needs than that, review the examples here to get a
|
|
sense of the specification in action. A plugin can be as basic as a short shell
|
|
script.
|
|
|
|
The full [specification](#specification) is after the examples,
|
|
followed by a list of [general considerations](#considerations).
|
|
|
|
## Examples
|
|
|
|
The specification is lean enough to be readily fulfilled in any language.
|
|
The following examples are written in `bash`.
|
|
|
|
The `custom-mkdir` plugin creates a directory, and `mkfs-ext4` creates a Linux
|
|
filesystem. You may imagine others, such as an NFS mount, perhaps, or various
|
|
storage infrastructure APIs.
|
|
|
|
Each example includes a minimal [volume specification][], which assumes that
|
|
you have placed the plugin in the [host volume plugin directory][plugin_dir]
|
|
and made it executable.
|
|
|
|
<Tabs>
|
|
<Tab heading="custom-mkdir">
|
|
|
|
`custom-mkdir` only creates a directory. There is a plugin built into Nomad that
|
|
does this called "mkdir", but this serves as a minimal example.
|
|
|
|
Volume specification:
|
|
```hcl
|
|
type = "host"
|
|
name = "mkdir-vol"
|
|
plugin_id = "custom-mkdir" # plugin filename
|
|
```
|
|
|
|
Plugin code:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
|
|
# set -u to error by name if an env var is not set
|
|
set -eu
|
|
|
|
# echo to stderr, because Nomad expects stdout to match the spec.
|
|
# this (and stdout) show up in Nomad agent debug logs.
|
|
stderr() {
|
|
1>&2 echo "$@"
|
|
}
|
|
|
|
get_path() {
|
|
# since delete runs `rm -rf` (frightening), check to make sure
|
|
# the path is something sensible.
|
|
if [ -z "$DHV_VOLUMES_DIR" ]; then
|
|
stderr "DHV_VOLUMES_DIR must not be empty"
|
|
exit 1
|
|
fi
|
|
if [ -z "$DHV_VOLUME_ID" ]; then
|
|
stderr "DHV_VOLUME_ID must not be empty"
|
|
exit 1
|
|
fi
|
|
# create and delete assign this echo output to a variable
|
|
echo "$DHV_VOLUMES_DIR/$DHV_VOLUME_ID"
|
|
}
|
|
|
|
case "$DHV_OPERATION" in
|
|
"fingerprint")
|
|
echo '{"version": "0.0.1"}'
|
|
;;
|
|
"create")
|
|
path="$(get_path)"
|
|
stderr "creating directory: $path"
|
|
# `mkdir -p` may be run repeatedly with the same result;
|
|
# it is idempotent.
|
|
mkdir -p "$path"
|
|
# 0 bytes because plain directories are not any particular size.
|
|
printf '{"path": "%s", "bytes": 0}' "$path"
|
|
;;
|
|
"delete")
|
|
path="$(get_path)"
|
|
stderr "deleting directory: $path"
|
|
rm -rf "$path" # `rm -f` is also idempotent.
|
|
;;
|
|
*)
|
|
echo "unknown operation: '$DHV_OPERATION'"
|
|
exit 1
|
|
;;
|
|
esac
|
|
```
|
|
|
|
</Tab>
|
|
<Tab heading="mkfs-ext4">
|
|
|
|
`mkfs-ext4` creates a Linux ext4 filesystem with the `mkfs.ext4` command and
|
|
mounts it as a loopback device. Unlike `mkdir`, `mkfs` can restrict the size
|
|
of the volume.
|
|
|
|
Volume specification:
|
|
```hcl
|
|
type = "host"
|
|
name = "mkfs-vol"
|
|
plugin_id = "mkfs-ext4" # plugin filename
|
|
capacity_min = "50MB" # capacity values are required by this plugin
|
|
capacity_max = "50MB"
|
|
```
|
|
|
|
Plugin code:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
version='0.0.1'
|
|
|
|
stderr() {
|
|
1>&2 echo "$@"
|
|
}
|
|
|
|
get_path() {
|
|
if [ -z "$DHV_VOLUMES_DIR" ]; then
|
|
stderr "DHV_VOLUMES_DIR must not be empty"
|
|
exit 1
|
|
fi
|
|
if [ -z "$DHV_VOLUME_ID" ]; then
|
|
stderr "DHV_VOLUME_ID must not be empty"
|
|
exit 1
|
|
fi
|
|
echo "$DHV_VOLUMES_DIR/$DHV_VOLUME_ID"
|
|
}
|
|
|
|
is_mounted() {
|
|
mount | grep -q " $1 "
|
|
}
|
|
|
|
create_volume() {
|
|
local path="$1"
|
|
local bytes="$2"
|
|
|
|
# translate to mb for dd block size
|
|
local megs=$((bytes / 1024 / 1024)) # lazy, approximate
|
|
if [ $megs -le 0 ]; then
|
|
stderr "minimum capacity must be greater than zero."
|
|
exit 2
|
|
fi
|
|
|
|
mkdir -p "$path"
|
|
# the if statements ensure idempotency
|
|
if [ ! -f "$path.ext4" ]; then
|
|
# dd only writes to stderr, so safe to run without redirection
|
|
dd if=/dev/zero of="$path.ext4" bs=1M count="$megs"
|
|
# mkfs includes stdout, so we need to redirect that to stderr
|
|
mkfs.ext4 "$path.ext4" 1>&2
|
|
fi
|
|
if ! is_mounted "$path"; then
|
|
mount "$path.ext4" "$path"
|
|
fi
|
|
}
|
|
|
|
delete_volume() {
|
|
local path="$1"
|
|
is_mounted "$path" && umount "$path"
|
|
rm -rf "$path"
|
|
rm -f "$path.ext4"
|
|
}
|
|
|
|
case "$1" in
|
|
"fingerprint")
|
|
printf '{"version": "%s"}' "$version"
|
|
;;
|
|
"create")
|
|
path="$(get_path)"
|
|
stderr "creating volume at $path"
|
|
create_volume "$path" "$DHV_CAPACITY_MIN_BYTES"
|
|
# output what Nomad expects
|
|
bytes="$(stat --format='%s' "$path.ext4")"
|
|
printf '{"path": "%s", "bytes": %s}' "$path" "$bytes"
|
|
;;
|
|
"delete")
|
|
path="$(get_path)"
|
|
stderr "deleting volume at $path"
|
|
delete_volume "$path" ;;
|
|
*)
|
|
echo "unknown operation: $1"
|
|
exit 1 ;;
|
|
esac
|
|
```
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
## Specification
|
|
|
|
A host volume plugin is registered with Nomad if it:
|
|
|
|
* Is an executable file (a script or binary)
|
|
* Is located in the [`client.host_volume_plugin_dir`][plugin_dir] directory
|
|
on Nomad client nodes
|
|
* Responds appropriately to a `fingerprint` call
|
|
|
|
### Operations
|
|
|
|
To fully manage the lifecycle of a volume, plugins must fulfill all of the
|
|
following operations:
|
|
|
|
* [fingerprint](#fingerprint)
|
|
* [create](#create)
|
|
* [delete](#delete)
|
|
|
|
Nomad passes the operation as the first positional argument to the plugin.
|
|
That and other information are passed as environment variables. Environment
|
|
variables are prefixed with `"DHV_"` (i.e. Dynamic Host Volume).
|
|
|
|
Some variables may be required for certain plugins (namely, `DHV_CAPACITY_*`
|
|
for plugins that can restrict size). Most are for plugin author convenience.
|
|
|
|
#### fingerprint
|
|
<blockquote style={{borderLeft: "solid 1px #00ca8e"}}>
|
|
|
|
Nomad calls `fingerprint` when the client agent starts (or is reloaded with a
|
|
SIGHUP) to discover valid plugins. The returned "version" is used to register
|
|
the plugin on the Nomad node for volume scheduling.
|
|
|
|
**CLI arguments:** `$1=fingerprint`
|
|
|
|
**Environment variables:**
|
|
|
|
```
|
|
DHV_OPERATION=fingerprint
|
|
```
|
|
|
|
**Expected stdout:**
|
|
|
|
```
|
|
{"version": "0.0.1"}
|
|
```
|
|
|
|
**Requirements:**
|
|
|
|
* Must complete within 5 seconds, or Nomad will kill it. It should be much
|
|
faster, as no actual work should be done.
|
|
* "version" value must be valid per the [hashicorp/go-version][go-version]
|
|
golang package.
|
|
|
|
</blockquote>
|
|
|
|
#### create
|
|
<blockquote style={{borderLeft: "solid 1px #00ca8e"}}>
|
|
|
|
Nomad calls `create` when you run [`nomad volume create`][cli-create] CLI or
|
|
use the [create API][api-create]).
|
|
|
|
You can run `create` again for the same volume if you include the volume
|
|
`id` in the volume specification.
|
|
|
|
Nomad also calls `create` when the agent starts, for each volume that was
|
|
previously created on the node, so plugins can ensure the volumes are available
|
|
after an agent restart or host reboot.
|
|
|
|
**CLI Arguments:** `$1=create`
|
|
|
|
**Environment variables:**
|
|
|
|
```
|
|
DHV_OPERATION=create
|
|
DHV_VOLUMES_DIR={directory to put the volume in}
|
|
DHV_PLUGIN_DIR={path to directory containing plugins}
|
|
DHV_NAMESPACE={volume namespace}
|
|
DHV_VOLUME_NAME={name from the volume specification}
|
|
DHV_VOLUME_ID={volume ID generated by Nomad}
|
|
DHV_NODE_ID={Nomad node ID}
|
|
DHV_NODE_POOL={Nomad node pool}
|
|
DHV_CAPACITY_MIN_BYTES={capacity_min from the volume spec, expressed in bytes}
|
|
DHV_CAPACITY_MAX_BYTES={capacity_max from the volume spec, expressed in bytes}
|
|
DHV_PARAMETERS={stringified json of parameters from the volume spec}
|
|
```
|
|
|
|
**Expected stdout:**
|
|
|
|
```
|
|
{"path": "/path/to/created/volume", "bytes": 50000000}
|
|
```
|
|
|
|
**Expected stdout on error:**
|
|
|
|
```
|
|
{"error": "error message"}
|
|
```
|
|
|
|
Returning an error message is optional. Nomad returns the error message in any
|
|
error returned to the user.
|
|
|
|
**Requirements:**
|
|
|
|
* Must complete within 60 seconds, or Nomad will kill it.
|
|
* Must create a path on disk, within `DHV_VOLUMES_DIR` or otherwise, and return
|
|
it as `"path"`. Nomad will mount this path into workloads that request the
|
|
volume, and it will also be sent to `delete` later as `DHV_CREATED_PATH`.
|
|
* Must be idempotent - running create with the same inputs should produce the
|
|
same result.
|
|
* Must be safe to run concurrently, per volume name per node.
|
|
* If the plugin fails partway through create, it must clean up after itself
|
|
and exit non-0. Nomad will not attempt to delete partial creates.
|
|
* However, if during an _initial_ create, Nomad fails to save the volume in its
|
|
own state, it will issue `delete` automatically to avoid leaving any
|
|
stray volumes on disk.
|
|
|
|
</blockquote>
|
|
|
|
#### delete
|
|
<blockquote style={{borderLeft: "solid 1px #00ca8e"}}>
|
|
|
|
Nomad calls `delete` when you run [`nomad volume delete`][cli-delete] CLI or
|
|
use the [delete API][api-delete].
|
|
|
|
**CLI Arguments:** `$1=delete`
|
|
|
|
**Environment variables:**
|
|
|
|
```
|
|
DHV_OPERATION=delete
|
|
DHV_CREATED_PATH={path that `create` returned}
|
|
DHV_VOLUMES_DIR={directory that volumes should be put in}
|
|
DHV_PLUGIN_DIR={path to directory containing plugins}
|
|
DHV_NAMESPACE={volume namespace}
|
|
DHV_VOLUME_NAME={name from the volume specification}
|
|
DHV_VOLUME_ID={volume ID generated by Nomad}
|
|
DHV_NODE_ID={Nomad node ID}
|
|
DHV_NODE_POOL={Nomad node pool}
|
|
DHV_PARAMETERS={stringified json of parameters from the volume spec}
|
|
```
|
|
|
|
**Expected stdout:** none (stdout is discarded)
|
|
|
|
**Expected stdout on error:**
|
|
|
|
```
|
|
{"error": "error message"}
|
|
```
|
|
|
|
Returning an error message is optional. Nomad returns the error message in any
|
|
error returned to the user.
|
|
|
|
**Requirements:**
|
|
|
|
* Must complete within 60 seconds, or Nomad will kill it.
|
|
* Must remove the `DHV_CREATED_PATH` that was returned by `create`,
|
|
or derive the path from other variables the same way that `create` did.
|
|
* Must be idempotent - calling delete on an already deleted volume must not
|
|
return an error.
|
|
* Must be safe to run concurrently, per volume name per node.
|
|
* Will be run after a failed `create` operation during initial volume creation.
|
|
|
|
</blockquote>
|
|
|
|
## Considerations
|
|
|
|
Plugin authors should consider these details when writing plugins.
|
|
|
|
### Execution
|
|
|
|
* The plugin is executed as the same user as the `nomad` agent (likely root).
|
|
* Plugin `stdout` and `stderr` are exposed as client agent debug logs,
|
|
so plugins should not output sensitive information.
|
|
* Nomad does not retry automatically on error. The caller of create/delete
|
|
must retry manually. The plugin may do so internally with its own retry
|
|
logic, provided it still completes within the deadline.
|
|
* Errors from `create` while restoring a volume during Nomad agent start
|
|
do not halt the client. The error will be in client logs, and the volume
|
|
is not registered as available on the node.
|
|
* Be aware that the `DHV_VOLUME_NAME` and `DHV_PARAMETERS` fields are controlled
|
|
by the volume author. If the expected volume authors are not the Nomad
|
|
administrators, you should ensure your plugin handles these fields safely or
|
|
ignores them.
|
|
|
|
### Uniqueness
|
|
|
|
* Volume `name` is unique per _node_, and volume `ID` is unique per _region_.
|
|
* Only one create/delete operation at a time is executed per volume `name`
|
|
per node, and similarly by `id` on Nomad servers, but many create/delete
|
|
operations for different volume IDs may run concurrently, even on the same
|
|
node.
|
|
* We suggest placing volumes in `DHV_VOLUMES_DIR` for consistency, but it is not
|
|
required. The built-in `mkdir` plugin uses `$DHV_VOLUMES_DIR/$DHV_VOLUME_ID`
|
|
to ensure uniqueness across the cluster. We recommend against using
|
|
`DHV_VOLUME_NAME` in the path unless the plugin guards against path
|
|
traversal.
|
|
* Plugins that write into network storage need to take care not to delete
|
|
remote/shared state by `name`, unless they know that there are no other
|
|
volumes with workloads using that name.
|
|
|
|
### Configuration
|
|
|
|
* Per-_volume_ configuration should be set in the volume specification file's
|
|
`parameters`. Per-_node_ configuration should be put in config file(s) as
|
|
described next.
|
|
* There is no mechanism built into Nomad for plugin configuration. As a
|
|
convention, we suggest placing any necessary configuration file(s) next to
|
|
the executable plugin in the plugin directory. You may use `DHV_PLUGIN_DIR`
|
|
to refer to the directory.
|
|
* If a plugin needs to retain state across operations (e.g. delete needs
|
|
some value that was generated during create), then you may store that on
|
|
the host filesystem, or some external data store of your choosing, perhaps
|
|
even Nomad variables.
|
|
|
|
[stateful-workloads]: /nomad/docs/operations/stateful-workloads#host-volumes
|
|
[plugin_dir]: /nomad/docs/configuration/client#host_volume_plugin_dir
|
|
[volume specification]: /nomad/docs/other-specifications/volume/host
|
|
[go-version]: https://pkg.go.dev/github.com/hashicorp/go-version#pkg-constants
|
|
[cli-create]: /nomad/docs/commands/volume/create
|
|
[api-create]: /nomad/api-docs/volumes#create-dynamic-host-volume
|
|
[cli-delete]: /nomad/docs/commands/volume/delete
|
|
[api-delete]: /nomad/api-docs/volumes#delete-dynamic-host-volume
|
|
[mkdir_plugin]: /nomad/docs/other-specifications/volume/host#mkdir-plugin
|
|
[host_volumes_dir]: /nomad/docs/configuration/client#host_volumes_dir
|