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:

Outputs:

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:

Outputs:

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:

Outputs:

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:

Outputs:

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.

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.

Next