Terraform
The Terraform module shape for a Flux deployment — what modules would exist, what their inputs and outputs are, and where to fall back to provider modules until an official one ships.
This page describes the module surface, not a working .tf file. The intent is to give you a target to author against — or to evaluate a community module against — without fabricating HCL that doesn’t exist.
Module set
Four modules cover a production Flux deployment. Each composes provider-specific resources behind a Flux-shaped interface.
flux_postgres
The database tier.
Inputs:
name— instance name / identifier.version— Postgres major version (default 16).instance_size— provider-shaped (e.g.,db.t4g.mediumon AWS).vpc_id/subnet_ids— network placement.multi_az— boolean, defaults totruein prod.backup_retention_days— defaults to 7.tags— map.
Outputs:
connection_url— fullpostgresql://user:password@host:port/dbURL, ready forFLUX_DATABASE_URL.admin_secret_arn(AWS) /secret_id(GCP) /secret_uri(Azure) — pointer to the master credential in the cloud’s secret store.
The module creates the instance, database, application user with CREATE permission, and the secret holding the connection URL.
flux_secrets
The bootstrap configuration tier — bootstrap token, encryption key, plus references to the DB credential from flux_postgres.
Inputs:
name_prefix— to namespace secret names.kms_key_id(or platform equivalent) — for encryption-at-rest of the secret store.database_url_arn— output fromflux_postgres.
Outputs:
bootstrap_token_ref— secret-store reference (ARN, resource ID, or URI).encryption_key_ref— same shape.database_url_ref— pass-through from input.
The module generates the bootstrap token and encryption key on first apply, stores them in the cloud’s secret manager, and exports the references for the server and worker modules to inject as env vars. Important: the lifecycle { ignore_changes = [secret_string] } block guards against accidental rotation on re-apply — rotation is a deliberate operation, not an idempotency event.
flux_server
The server tier.
Inputs:
image— full container image reference.bootstrap_token_ref,encryption_key_ref,database_url_ref— fromflux_secrets.service_subnet_ids— where to place the server task / instance.load_balancer_target_group_arn(AWS) /backend_service_id(GCP) /ingress_id(Azure) — to register with the LB.auth_provider—api_keys,oidc, orboth. The module sets the matchingFLUX_SECURITY__AUTH__*env vars.replica_count— number of server replicas, default 1. Values above 1 are valid with PostgreSQL; server replicas coordinate through the database (advisory-lock scheduler singleton,SKIP LOCKEDdispatch), so the module’s job is wiring, not prevention.extra_env— passthrough for additionalFLUX_*env vars.
Outputs:
service_arn(or platform equivalent) — for downstream resources to depend on.internal_url— the FQDN workers should use.
Replica semantics: the module maps replica_count to the platform’s fixed-count knob — desired_count on ECS, min_instances/max_instances on Cloud Run, minReplicas/maxReplicas on Container Apps — with no auto scaling on the server tier (replica count is an availability decision, and each replica adds pool_size + max_overflow connections to the PostgreSQL budget). When replica_count > 1 the module must also enable connection affinity on the load balancer (ALB target-group stickiness, Cloud Run --session-affinity, Container Apps sticky sessions): a worker’s SSE stream lives on the replica it connected to. Two validations are worth encoding: reject replica_count > 1 when the database URL is SQLite (single-node only), and keep a flux db upgrade hook (a one-shot task/job) ordered before the service so rolling multiple replicas never races the schema migration. See High availability.
flux_worker_pool
The worker tier. One instance of this module per pool.
Inputs:
pool_name— used to name resources and rendered as--label pool=<pool_name>in the worker’s container command.image— same image as server.bootstrap_token_ref— fromflux_secrets.server_url— fromflux_server.internal_url.min_replicas,max_replicas— for the platform’s auto-scaler.scale_metric—cpu,memory, or a custom CloudWatch / Cloud Monitoring / Azure Monitor metric reference.extra_labels— additional key-value pairs, each rendered as an extra--label key=valueflag.
Outputs:
service_arn(or equivalent).pool_labels— the resolved label set, for downstream routing-config use.
Unlike flux_server, this module does have auto-scaling — workers scale freely.
Composition
A typical deployment composes the four modules:
module "flux_postgres" { ... }
module "flux_secrets" {
database_url_arn = module.flux_postgres.database_url_arn
}
module "flux_server" {
bootstrap_token_ref = module.flux_secrets.bootstrap_token_ref
encryption_key_ref = module.flux_secrets.encryption_key_ref
database_url_ref = module.flux_secrets.database_url_ref
service_subnet_ids = module.network.private_subnet_ids
load_balancer_target_group_arn = module.lb.target_group_arn
auth_provider = "api_keys"
replica_count = 2 # >1 requires PostgreSQL + sticky routing on the LB
}
module "flux_worker_default" {
source = "..."
pool_name = "default"
bootstrap_token_ref = module.flux_secrets.bootstrap_token_ref
server_url = module.flux_server.internal_url
min_replicas = 2
max_replicas = 20
}
module "flux_worker_gpu" {
source = "..."
pool_name = "gpu"
bootstrap_token_ref = module.flux_secrets.bootstrap_token_ref
server_url = module.flux_server.internal_url
min_replicas = 0
max_replicas = 5
extra_labels = { gpu = "true" }
}
The pseudo-code is shape-only; pick the cloud-provider resources from the AWS, GCP, or Azure recipes for the actual resource lists each module wraps.
State and rotation
Two state-management notes when authoring against this shape.
- Bootstrap-token rotation must not be silent on
terraform apply. Random values inside arandom_passwordresource will regenerate if theirkeeperschange, and Terraform will happily destroy and recreate the secret in-place. Uselifecycle { ignore_changes = [...] }on the secret-version resource, and trigger rotation via a separate apply with a-replaceflag. - Encryption-key destruction is fatal. If a
terraform destroyremoves the Key Vault / KMS / Secret Manager entry that holds the encryption key, every encrypted column in the Flux DB becomes unrecoverable. Production state should put the key resource in a separate root module from the rest of the deployment, withprevent_destroy = true. The “destroy the dev environment” routine should not be able to touch it.
What can go wrong
terraform apply recreates the bootstrap secret
Symptom. Every terraform apply shows the bootstrap-token secret as needing replacement; workers lose authentication after each apply.
Cause. The random_password resource feeding the secret is keyed on something that changes (a timestamp, a derived value), or lifecycle { ignore_changes } is missing on the secret version.
Fix. Add ignore_changes = [secret_string] on the secret-version resource and decouple rotation from regular applies.
replica_count = 2 on a SQLite database URL
Symptom. The module provisioned two server replicas against a SQLite database_url; the second replica can’t share the file, and worker claim safety silently degrades.
Cause. Multi-replica coordination lives entirely in PostgreSQL (advisory locks, FOR UPDATE SKIP LOCKED, LISTEN/NOTIFY); SQLite is single-node only. If you’re writing the module, encode the pairing as a validation block:
variable "replica_count" {
type = number
default = 1
validation {
condition = var.replica_count >= 1
error_message = "replica_count must be at least 1."
}
}
# cross-variable check (Terraform >= 1.9 can reference other variables;
# otherwise use a precondition on the service resource)
variable "database_url" {
type = string
validation {
condition = var.replica_count == 1 || startswith(var.database_url, "postgresql://")
error_message = "replica_count > 1 requires PostgreSQL; SQLite is single-node only."
}
}
Older Flux module sketches hard-rejected replica_count > 1 outright — that constraint dated from the pre-0.5x scheduler, which had no cross-replica coordination. It no longer applies; keep only the SQLite guard.
Drift between Terraform state and rotated secret
Symptom. You rotated the bootstrap token out-of-band; terraform plan now wants to revert it.
Cause. Out-of-band changes drift from Terraform’s last-known state.
Fix. Either import the rotated value (terraform import on the secret-version resource, then update the source to match), or accept that out-of-band rotation is a one-way migration — re-run the IaC-managed rotation flow on the next apply to bring state and reality back in sync.
Modules coming soon
An official terraform-flux module set is planned. Until it ships, the shape on this page is your authoring target. If you build something worth sharing, contribute it to flux-recipes.