Customizing Cluster Boot¶
The qumulo_filesystem_aws, qumulo_filesystem_azure, and qumulo_filesystem_gcp
resources let you customize the two shell scripts that boot a cluster:
- the node boot script, which runs when a node instance boots and installs qumulo-core, and
- the provisioner script, which runs on a short-lived orchestration VM and forms and configures the cluster.
Hooks (pre_run, post_run) splice your shell into the stock scripts at fixed anchor
points; Qumulo's cluster-formation logic stays untouched. Typical uses: proxy/CA setup,
bucket-policy fixes, monitoring agents, notifications.
Hooks live on two optional object attributes, node_hooks and provisioner_hooks,
identical on all three resources:
resource "qumulo_filesystem_aws" "example" {
# ... other required fields ...
node_hooks = {
pre_run = <<-EOT
export https_proxy="http://proxy.corp.example.com:3128"
EOT
}
provisioner_hooks = {
post_run = file("${path.module}/hooks/notify.sh")
}
deletion_protection = true # recommended: guard the cluster's EC2 instances and S3 buckets
timeouts {
create = "90m"
delete = "30m"
}
}
Boot timeline¶
Hooks anchor at four fixed points. Both boot scripts are rendered during
terraform apply, before any instance launches. Node instances and the provisioner VM
then boot independently; the provisioner waits for the nodes partway through.
NODE INSTANCES (each node, at boot)
|
| distro detection
| [node_hooks.pre_run]
| connectivity check
| package cache refresh, tools
| detect live cluster version
| download and install qumulo-core
| [node_hooks.post_run]
v
node runs qfsd
PROVISIONER VM (one per operation)
|
| error trap armed, deployment variables set
| [provisioner_hooks.pre_run]
| logging agent, base tooling, cloud CLI auth
| storage access validation
| wait for every node to answer on https://<node>:8000
| form or modify quorum
| configure cluster
| [provisioner_hooks.post_run]
| write completion signal
v
provisioner self-terminates
Which anchor do I want?¶
| Attribute | Field | Runs |
|---|---|---|
node_hooks |
pre_run |
On each node, after distro detection, before the node script's first network operation (connectivity check, package-cache refresh, package download). |
node_hooks |
post_run |
On each node, after qumulo-core is installed. |
provisioner_hooks |
pre_run |
On the provisioner, after the error trap is armed and deployment variables are set, before the provisioner's first network operation (logging agent install, tooling installs, cloud CLI auth, storage-access validation). |
provisioner_hooks |
post_run |
On the provisioner, after the cluster is formed and configured, right before the completion signal. |
Both node_hooks.pre_run and provisioner_hooks.pre_run run before their script touches
the network at all, so either is the right place to configure an HTTP proxy, a corporate
CA, or an air-gapped package mirror. Because the hook is spliced into the same shell
process, any exports or files it writes persist for the rest of the boot script.
The flip side: at pre_run time only the image's stock commands exist. The cloud CLI,
jq, and the other tools the scripts install are not available yet, so anything
CLI-dependent belongs in post_run (which runs after all tooling is installed and
authenticated) or must install its own prerequisites.
Safety rules¶
Hooks run as root under set -xe
- A failing command aborts the deploy. The scripts run with
-e, so treat every hook line as load-bearing, or explicitly tolerate failure (|| true). - Never embed secrets. Hook text is stored in Terraform state, and
-xtracing echoes every line into the boot logs (CloudWatch Logs on AWS, cloud-init output on Azure, Cloud Logging and the serial console on GCP). Failure details are also captured to SSM Parameter Store, Azure App Configuration, or the GCS status bucket. Get credentials from the instance's IAM role, managed identity, or service account, or from a cloud secret store. - Hooks must be idempotent. The provisioner script re-runs on scaling, node replacement, and other post-create operations; the node script runs on every node that boots (including nodes added later by scaling or replacement, and on GCP the startup script also re-runs when a node VM reboots).
- Hook bodies are spliced verbatim. They are not templates and are not
shell-escaped. A shell syntax error surfaces at boot, not at plan, so keep hooks in
.shfiles and runshellcheckon them. - A failure before the cloud CLI is ready has no way to report itself on AWS or
Azure.
provisioner_hooks.pre_runruns before the cloud CLI is installed and authenticated, so a failure there (including a failingpre_runhook itself) cannot write a status key, andterraform applywaits out its full timeout instead of failing fast. Consult the instance's boot log directly (CloudWatch Logs on AWS, cloud-init output on Azure) rather than waiting for the apply to fail. GCP is unaffected: it always reports success or failure through its status object.
Shell variables in scope¶
Hook bodies are spliced where these shell variables are already defined. Only the names below are a stable contract; anything else you spot in a rendered script may change without notice. Names follow each cloud's script, so they differ per cloud.
Provisioner script (provisioner_hooks):
| Variable | Holds |
|---|---|
region |
AWS region |
deployment_name |
Deployment unique name (seed for cloud resource names and SSM paths) |
cluster_name |
qfsd cluster name |
cluster_version |
Qumulo Core version |
cluster_persistent_bucket_names |
Comma-separated S3 bucket names |
cluster_persistent_bucket_uris |
Comma-separated S3 bucket URIs |
cluster_persistent_storage_type |
HOT or COLD |
cluster_persistent_capacity_limit |
Soft capacity limit (TB) |
node_ips, existing_node_ips, final_node_ips |
Comma-separated node IPs: operation targets, stable existing nodes, nodes kept after the operation |
instance_ids |
Comma-separated EC2 instance IDs |
fault_domain_ids |
Comma-separated fault-domain IDs |
number_azs |
Availability-zone count |
float_ips, max_float_ips |
Floating IPs and the per-cluster maximum |
netmask |
Node netmask |
networking_mode |
qumulo_managed or host_managed networking mode |
s3_region |
Bucket region |
Node script (node_hooks), in scope at pre_run:
| Variable | Holds |
|---|---|
TEMPLATE_QUMULO_VERSION |
Target Qumulo Core version |
EXISTING_NODE_IPS |
Comma-separated existing cluster node IPs (empty on initial create) |
NETWORKING_MODE |
qumulo_managed or host_managed networking mode |
AWS_REGION |
AWS region |
DISTRO_ID, DISTRO_TYPE, PKG_FORMAT, PKG_INSTALL_CMD |
Distro-detection results |
Provisioner script (provisioner_hooks):
| Variable | Holds |
|---|---|
region |
Azure location |
deployment_name |
Deployment unique name |
cluster_name |
qfsd cluster name |
cluster_version |
Qumulo Core version |
cluster_product_type |
Qumulo product type |
cluster_object_storage_uri |
Object-storage URI list |
cluster_persistent_capacity_limit |
Soft capacity limit (TB) |
container_name |
Storage container name |
az_cloud_environment |
Azure cloud (AzureCloud, AzureUSGovernment, ...) |
managed_identity_client_id |
Managed-identity client ID |
node_ips, existing_node_ips, final_node_ips |
Comma-separated primary node IPs: operation targets, stable existing nodes, nodes kept after the operation |
instance_ids |
Comma-separated VM IDs |
num_azs |
Availability-zone count |
fault_domain_ids |
Comma-separated fault-domain IDs |
new_floating_ips, max_floating_ips, cluster_floating_ip_type |
Floating-IP settings |
expected_block_device_count |
Expected data-disk count |
keyvault_uri |
Key Vault URI |
subnet_id, subnet_cidr, network_security_group_id |
Network identifiers |
networking_mode |
qumulo_managed or host_managed networking mode |
Node script (node_hooks), in scope at pre_run:
| Variable | Holds |
|---|---|
TEMPLATE_QUMULO_VERSION |
Target Qumulo Core version |
EXISTING_NODE_IPS |
Comma-separated existing cluster node IPs (empty on initial create) |
NETWORKING_MODE |
qumulo_managed or host_managed networking mode |
DISTRO_ID, DISTRO_TYPE, PKG_FORMAT, PKG_INSTALL_CMD |
Distro-detection results |
Provisioner script (provisioner_hooks):
| Variable | Holds |
|---|---|
region |
GCP region |
deployment_name |
Deployment unique name |
cluster_name |
qfsd cluster name |
bucket_uris |
Comma-separated GCS bucket URIs |
capacity_limit_tb |
Soft capacity limit (TB) |
node_ips, existing_node_ips, final_node_ips |
Comma-separated node IPs: operation targets, stable existing nodes, nodes kept after the operation |
fault_domain_ids |
Comma-separated fault-domain IDs |
number_azs |
Zone count |
float_ips |
Floating IPs |
subnet_cidr |
Cluster subnet CIDR |
networking_mode |
qumulo_managed or host_managed networking mode |
Node script (node_hooks), in scope at pre_run:
| Variable | Holds |
|---|---|
TEMPLATE_QUMULO_VERSION |
Target Qumulo Core version |
EXISTING_NODE_IPS |
Comma-separated existing cluster node IPs (empty on initial create) |
NETWORKING_MODE |
qumulo_managed or host_managed networking mode |
DISTRO_ID, DISTRO_TYPE, PKG_FORMAT, PKG_INSTALL_CMD |
Distro-detection results |
Two deliberate gaps:
- The cluster admin password is set after
pre_runand unset beforepost_runon every cloud, so it is never in scope for a hook. - The internal package download URLs are not resolved until after node
pre_run; they depend on live-cluster version detection.
Examples¶
Every hook field is a plain string, so an inline heredoc (<<-EOT) and loading a script
from a file with file(...) are interchangeable. Use heredocs for short snippets; keep
longer scripts in their own .sh files so they get editor highlighting and shellcheck.
The examples below show one of each.
Node hooks: route every node's package installs through the corporate proxy and trust the corporate CA before any download, then install a monitoring agent after qumulo-core is in place.
node_hooks = {
# BEFORE the node script's first network call. Exports and written files
# persist for the rest of boot, so cache refresh and core download use them.
pre_run = <<-EOT
export http_proxy="http://proxy.corp.example.com:3128"
export https_proxy="$http_proxy"
echo 'Acquire::http::Proxy "http://proxy.corp.example.com:3128";' > /etc/apt/apt.conf.d/95corp-proxy
cp /opt/corp/ca/corp-root.crt /usr/local/share/ca-certificates/ && update-ca-certificates
EOT
# AFTER qumulo-core is installed.
post_run = file("${path.module}/hooks/install-agent.sh")
}
Provisioner hooks: route the provisioner's egress through the corporate proxy before
its first download (the reason pre_run exists), then notify an internal tracker once
the cluster is up. Only distro-stock commands are available at pre_run; CLI-dependent
work belongs in post_run.
provisioner_hooks = {
# BEFORE the provisioner's first network operation. Everything after this,
# including the cloud CLI install and Qumulo downloads, uses the proxy.
pre_run = <<-EOT
export https_proxy="http://proxy.corp.example.com:3128"
export no_proxy="169.254.169.254,localhost,127.0.0.1"
EOT
# AFTER the cluster is formed and configured: report readiness to the CMDB.
# Loaded from a file; the script can still reference the in-scope shell
# variables (e.g. $deployment_name).
post_run = file("${path.module}/hooks/provisioner-post-run.sh")
}
Where hooks/provisioner-post-run.sh contains:
#!/usr/bin/env bash
# provisioner_hooks.post_run - runs after the cluster is formed.
curl -fsS -X POST "https://cmdb.corp.example.com/api/clusters" \
-H "Content-Type: application/json" \
-d "{\"deployment\":\"${deployment_name}\",\"status\":\"ready\"}"
A complete runnable configuration is documented in the
AWS user-data hooks example and lives in the provider repo
under examples/aws-user-data-hooks/.
When hook changes take effect¶
Changing a hook is an in-place update: it never replaces the cluster and never re-runs boot on existing instances. The new value takes effect on the next event that runs the relevant script:
- Provisioner hooks: the next operation that launches a provisioner VM (scaling, node replacement, storage updates, and similar).
- Node hooks: the first boot of the next new node (added by scaling or replacement).