Files
seaweedfs/terraform/modules/core/variables.tf
T
Chris Lu a10607f90a Add Terraform support for VM-based SeaweedFS deployment (#9754)
* terraform: add cloud-agnostic core renderer module

Renders per-node weed argv, systemd units, config files, disk-mount and secret-fetch scripts, and cloud-init from an address map. Creates zero cloud resources. Flags verified against the weed binary: volume uses -mserver for the master list, gRPC is -port.grpc (auto http+10000), minFreeSpacePercent is a string, filer store via -defaultStoreDir.

* terraform: add mTLS and JWT security module

Generates the CA, per-component certs with distinct CNs, and JWT signing keys via the tls/random providers. Emits a core_security object plus PEMs for secret-store delivery.

* terraform: add AWS deployment module and examples

Reserves stable ENIs first, renders config via the core, then creates instances, prevent_destroy EBS data disks mounted at /data, and the cluster security group. With enable_security, generates certs/JWT, stores them in SSM SecureString, grants an instance role, and fetches them at boot so secrets stay out of user_data. Keyed for_each on every stateful tier.

* terraform: add local cluster test harnesses

run_local_cluster.sh and run_local_secure.sh render a cluster with the core and run real weed processes, asserting master quorum, volume registration, filer/s3 round-trips, mutual-TLS formation, and JWT enforcement. Use an isolated high port range with a guard so they never touch a cluster already running on the machine. The weed binary defaults to $(go env GOPATH)/bin/weed.

* terraform: add CI workflow and README

fmt/validate/tofu-test plus smoke jobs that build weed and run both harnesses.

* terraform: guard against empty filesystem UUID in mount script

An empty UUID made grep -q match any fstab line, skipping the fstab entry and breaking the mount. Fail fast when blkid returns no UUID.

* terraform: sanitize cluster name in WEED_CLUSTER env keys

Hyphens or spaces in cluster_name produced invalid systemd/bash env var names; map non-alphanumerics to underscores.

* terraform: omit empty jwt.signing block from security.toml

With enable_security and no JWT key, the template emitted [jwt.signing] key="". Gate the block on a non-empty key and cover it with a test.

* terraform: mark core security input as sensitive

The security object carries JWT signing keys; keep them out of plan output and known values.

* terraform: enforce jwt_length minimum of 32

* terraform: note region/AZ coupling in HA example

* terraform: guard WORKDIR before recursive delete in test harnesses

* terraform: fix README fence language and test count

* terraform: handle embedded s3 with no filer nodes

Indexing sort(keys(var.filers))[0] errored at plan time when embedded S3 was enabled but no filers were defined; fall back to an empty config source.

* terraform: scope kms:Decrypt to a configurable key arn

Replace the hardcoded Resource="*" with a kms_key_arn variable (default "*") so production can restrict decrypt to a specific CMK.

* terraform: encrypt EBS data volumes at rest

Set encrypted = true on the volume/filer data disks and the all-in-one example disk.

* terraform: protect filer instances from API termination

Filers hold the leveldb2 metadata store, so they are stateful and get the same disable_api_termination as masters and volumes.

* terraform: stop instance before detaching in all-in-one example

* terraform: drop stale references to the removed plan doc

* terraform: correct stale mount-step comment in aws module

* terraform: mark Terraform support as experimental in README
2026-05-30 23:43:17 -07:00

319 lines
13 KiB
Terraform

# =============================================================================
# SeaweedFS Terraform core module - input variables
#
# This module is a PURE RENDERER. It creates zero cloud resources. It takes a
# per-node address map plus configuration and emits, per node, the rendered
# `weed` argv, systemd unit, cloud-init, and config files. Both the per-cloud
# infra wrappers (consume cloud_init/systemd_unit) and the local test harness
# (consume argv/config_files) read its `nodes` output.
#
# Flag names below are verified against the real `weed` binary, not the chart.
# Notably: volume uses -mserver for the master list; gRPC is -port.grpc
# (auto = http+10000); minFreeSpacePercent is a string.
# =============================================================================
variable "weed_binary" {
description = "Path to the weed executable on the target host."
type = string
default = "/usr/bin/weed"
}
variable "cluster_name" {
description = "Logical cluster name (WEED_CLUSTER_DEFAULT)."
type = string
default = "sw"
}
variable "log_level" {
description = "Global glog verbosity (-v)."
type = number
default = 1
}
variable "log_to_stderr" {
description = "Log to stderr (journald) instead of -logdir. Recommended on systemd."
type = bool
default = true
}
variable "monitoring_enabled" {
description = "Gate the -metricsPort/-metrics.address flags. When false the metrics port is not bound, matching the chart."
type = bool
default = false
}
variable "enable_security" {
description = "Emit the security.toml [grpc] mTLS block and reference per-component certs. Independent of JWT."
type = bool
default = false
}
# ----------------------------------------------------------------------------
# Security / secrets (the core only references paths + renders security.toml;
# cert/secret material is generated by the wrapper, never defaulted here).
# ----------------------------------------------------------------------------
variable "security" {
description = "mTLS + JWT signing configuration. Secret values must be supplied (never defaulted in module code)."
type = object({
cert_dir = optional(string, "/usr/local/share/ca-certificates")
allowed_wildcard_domain = optional(string, "")
allowed_common_names = optional(string, "")
jwt_signing_key = optional(string, "")
jwt_signing_read_key = optional(string, "")
jwt_filer_signing_key = optional(string, "")
jwt_filer_signing_read_key = optional(string, "")
})
default = {}
sensitive = true
}
variable "hardening" {
description = "systemd hardening directives (OpenShift SCC analogs). Relax per field if needed."
type = object({
run_as_user = optional(string, "seaweedfs")
no_new_privileges = optional(bool, true)
protect_system = optional(bool, true)
cap_drop_all = optional(bool, true)
})
default = {}
}
variable "env_file" {
description = "Path to the systemd EnvironmentFile holding secret env (DB creds, S3 admin key) fetched at boot. Optional (-prefixed in the unit, so a missing file is tolerated)."
type = string
default = "/etc/seaweedfs/weed.env"
}
variable "render_secret_files" {
description = <<-EOT
When true (default), secret-bearing config (security.toml, S3 identity JSON)
is written directly into cloud-init user_data. Convenient for the local test
harness and simple deployments. Set to false to keep secrets OUT of user_data
(and out of the cloud metadata service): the files are then exposed only via
the secret_files_by_node output, and the wrapper is expected to deliver them
from a secret store via boot_fetch_script.
EOT
type = bool
default = true
}
variable "boot_fetch_script" {
description = "Optional shell script written to /opt/seaweedfs/fetch-secrets.sh (root, 0755) and run before the weed unit starts. Used by wrappers to pull certs/secrets from a cloud secret store at boot."
type = string
default = ""
}
# ----------------------------------------------------------------------------
# Master (Raft quorum). Keyed `nodes` map -> for_each, never count.
# ----------------------------------------------------------------------------
variable "master" {
description = "Master tier. nodes is a map keyed by stable node id (m0/m1/m2)."
type = object({
enabled = optional(bool, true)
nodes = optional(map(object({
address = string
port = optional(number)
grpc_port = optional(number)
data_dir = optional(string)
disk_mounts = optional(list(object({
mountpoint = string
fstype = optional(string, "ext4")
devices = optional(list(string), [])
})), [])
})), {})
port = optional(number, 9333)
grpc_port = optional(number, null) # null => weed auto = port+10000
metrics_port = optional(number, 9327)
metrics_ip = optional(string, "")
ip_bind = optional(string, "0.0.0.0")
data_dir = optional(string, "/var/lib/seaweedfs/master")
default_replication = optional(string, "000")
volume_size_limit_mb = optional(number, 1000)
volume_preallocate = optional(bool, false)
garbage_threshold = optional(number, null)
disable_http = optional(bool, false)
raft_hashicorp = optional(bool, false)
resume_state = optional(bool, true) # deliberate deviation: binary default false
election_timeout = optional(string, "10s")
heartbeat_interval = optional(string, "300ms")
white_list = optional(string, "")
extra_args = optional(list(string), [])
})
default = {}
}
# ----------------------------------------------------------------------------
# Volume (stateful disk owners). Keyed `nodes` map.
# ----------------------------------------------------------------------------
variable "volume" {
description = "Volume tier. Each node owns one or more data dirs (-> -dir / -max)."
type = object({
enabled = optional(bool, true)
nodes = optional(map(object({
address = string
port = optional(number)
grpc_port = optional(number)
public_url = optional(string)
rack = optional(string)
data_center = optional(string)
data_dirs = optional(list(object({
path = string
max_volumes = optional(number, 0) # 0 => auto on non-windows
})))
idx_dir = optional(string)
disk_mounts = optional(list(object({
mountpoint = string
fstype = optional(string, "ext4")
devices = optional(list(string), [])
})), [])
})), {})
port = optional(number, 8080)
metrics_port = optional(number, 9327)
metrics_ip = optional(string, "")
ip_bind = optional(string, "0.0.0.0")
data_dirs = optional(list(object({
path = string
max_volumes = optional(number, 0)
})), [{ path = "/data", max_volumes = 0 }])
idx_dir = optional(string, "")
read_mode = optional(string, "proxy")
compaction_mbps = optional(number, 50)
min_free_space_percent = optional(string, "1")
file_size_limit_mb = optional(number, null)
index = optional(string, "memory")
images_fix_orientation = optional(bool, false)
white_list = optional(string, "")
extra_args = optional(list(string), [])
})
default = {}
}
# ----------------------------------------------------------------------------
# Filer (+ optional embedded S3). Keyed `nodes` map.
# ----------------------------------------------------------------------------
variable "filer" {
description = "Filer tier. Default leveldb2-replicated HA (each filer has its own store)."
type = object({
enabled = optional(bool, true)
nodes = optional(map(object({
address = string
port = optional(number)
grpc_port = optional(number)
data_dir = optional(string)
disk_mounts = optional(list(object({
mountpoint = string
fstype = optional(string, "ext4")
devices = optional(list(string), [])
})), [])
})), {})
port = optional(number, 8888)
metrics_port = optional(number, 9327)
metrics_ip = optional(string, "")
ip_bind = optional(string, "0.0.0.0")
data_dir = optional(string, "/var/lib/seaweedfs/filer") # -defaultStoreDir for leveldb2
default_replica_placement = optional(string, "000")
dir_list_limit = optional(number, 100000)
max_mb = optional(number, null)
disable_dir_listing = optional(bool, false)
disable_http = optional(bool, false)
encrypt_volume_data = optional(bool, false)
rack = optional(string, "")
data_center = optional(string, "")
filer_group = optional(string, "")
extra_env = optional(map(string), {})
extra_args = optional(list(string), [])
s3 = optional(object({
enabled = optional(bool, false)
port = optional(number, 8333)
https_port = optional(number, 0)
domain_name = optional(string, "")
config_path = optional(string, "/etc/seaweedfs/s3_config.json")
}), {})
})
default = {}
}
# ----------------------------------------------------------------------------
# S3 gateway (standalone). Keyed `nodes` map (stateless, but kept keyed for
# parity; wrappers may instead drive an ASG).
# ----------------------------------------------------------------------------
variable "s3" {
description = "Standalone S3 gateway tier."
type = object({
enabled = optional(bool, false)
nodes = optional(map(object({
address = string
port = optional(number)
grpc_port = optional(number)
})), {})
port = optional(number, 8333)
https_port = optional(number, 0)
iceberg_port = optional(number, null)
metrics_port = optional(number, 9327)
ip_bind = optional(string, "0.0.0.0")
domain_name = optional(string, "")
filer_address = optional(string, "") # override; else first filer node
config_path = optional(string, "/etc/seaweedfs/s3_config.json")
audit_log_config_path = optional(string, "")
cert_file = optional(string, "")
key_file = optional(string, "")
cacert_file = optional(string, "")
verify_client_cert = optional(bool, false)
extra_args = optional(list(string), [])
})
default = {}
}
# ----------------------------------------------------------------------------
# S3 identities -> rendered into the s3 config JSON (non-admin only; the admin
# credential is delivered via EnvironmentFile, never the JSON).
# ----------------------------------------------------------------------------
variable "s3_identities" {
description = "Non-admin S3 identities rendered into the config JSON. Admin key goes in the EnvironmentFile, not here."
type = list(object({
name = string
access_key = string
secret_key = string
actions = list(string)
}))
default = []
sensitive = true
}
# ----------------------------------------------------------------------------
# All-in-one (single `weed server` process).
# ----------------------------------------------------------------------------
variable "all_in_one" {
description = "All-in-one tier (weed server). Mutually exclusive with the distributed tiers."
type = object({
enabled = optional(bool, false)
nodes = optional(map(object({
address = string
data_dir = optional(string)
disk_mounts = optional(list(object({
mountpoint = string
fstype = optional(string, "ext4")
devices = optional(list(string), [])
})), [])
})), {})
data_dir = optional(string, "/data")
ip_bind = optional(string, "0.0.0.0")
master_port = optional(number, 9333)
volume_port = optional(number, 8080)
filer_port = optional(number, 8888)
default_replication = optional(string, "000")
volume_size_limit_mb = optional(number, 1000)
idle_timeout = optional(number, 30)
metrics_port = optional(number, 9324)
disable_http = optional(bool, false)
s3 = optional(object({
enabled = optional(bool, false)
port = optional(number, 8333)
config_path = optional(string, "/etc/seaweedfs/s3/s3_config.json")
domain_name = optional(string, "")
}), {})
extra_args = optional(list(string), [])
})
default = {}
}