Pulumi
The Pulumi component shape for a Flux deployment — TypeScript and Python sketches, plus the rotation and destruction guardrails that apply.
This page describes the component surface and shows TypeScript and Python sketches. The intent is to give you a target to author against. The same set of guardrails from the Terraform page applies — rotation must not be silent, encryption-key resources must be protected from destruction, and server replica counts above one are only valid with PostgreSQL plus sticky routing on the load balancer.
Component set
Four components mirror the Terraform module set.
FluxPostgres
The database tier.
Inputs (TypeScript):
interface FluxPostgresArgs {
name: string;
version?: string; // "16"
instanceSize: string; // provider-shaped
vpcId: pulumi.Input<string>;
subnetIds: pulumi.Input<string[]>;
multiAz?: boolean; // default true
backupRetentionDays?: number; // default 7
tags?: Record<string, string>;
}
Outputs:
connectionUrl: pulumi.Output<string>— secret-typed, ready to feedFluxSecrets.adminSecretRef: pulumi.Output<string>— pointer into the cloud’s secret store.
FluxSecrets
Bootstrap configuration.
Inputs:
namePrefix: stringkmsKeyId?: pulumi.Input<string>databaseUrl: pulumi.Input<string>(theconnectionUrlfromFluxPostgres).
Outputs:
bootstrapTokenRef: pulumi.Output<string>encryptionKeyRef: pulumi.Output<string>databaseUrlRef: pulumi.Output<string>
The component generates the bootstrap token and encryption key on first run via a RandomPassword or random.RandomBytes resource, stores them in the cloud’s secret manager, and exports the references. The crucial bit: the resource has pulumi.ResourceOptions({ protect: true, ignoreChanges: ["..."] }) so a routine pulumi up doesn’t rotate them silently.
FluxServer
The server tier.
Inputs:
image: pulumi.Input<string>bootstrapTokenRef,encryptionKeyRef,databaseUrlRef— fromFluxSecrets.serviceSubnetIds: pulumi.Input<string[]>loadBalancerTargetGroupArn/backendServiceId/ingressId— platform-shaped.authProvider: "apiKeys" | "oidc" | "both"replicaCount?: number— default 1; values above 1 are valid with PostgreSQL.extraEnv?: Record<string, pulumi.Input<string>>
Outputs:
serviceArn/serviceId— platform-shaped.internalUrl: pulumi.Output<string>— for workers.
The component maps replicaCount onto the platform’s fixed-count knob (desiredCount on ECS, minInstances/maxInstances on Cloud Run, minReplicas/maxReplicas on Container Apps) with no auto-scaling policy on the server tier — replica count is an availability decision, and each replica adds its connection-pool ceiling (40 by default) to the PostgreSQL budget. At replicaCount > 1 the component must also enable connection affinity on the load-balancer resource it wires up (target-group stickiness, sessionAffinity, sticky sessions) — a worker’s SSE stream lives on the replica it connected to — and its constructor should throw if the database URL is SQLite, which is single-node only. See High availability.
FluxWorkerPool
The worker tier — one per pool.
Inputs:
poolName: stringimage: pulumi.Input<string>bootstrapTokenRef: pulumi.Input<string>serverUrl: pulumi.Input<string>(fromFluxServer.internalUrl).minReplicas: numbermaxReplicas: numberscaleMetric: "cpu" | "memory" | CustomMetricRefextraLabels?: Record<string, string>
TypeScript composition sketch
import * as pulumi from "@pulumi/pulumi";
import { FluxPostgres, FluxSecrets, FluxServer, FluxWorkerPool } from "@pulumi/flux";
const network = /* your VPC component */;
const db = new FluxPostgres("flux-db", {
name: "flux",
instanceSize: "db.t4g.medium",
vpcId: network.vpcId,
subnetIds: network.privateSubnetIds,
multiAz: true,
});
const secrets = new FluxSecrets("flux-secrets", {
namePrefix: "flux",
databaseUrl: db.connectionUrl,
});
const server = new FluxServer("flux-server", {
image: "myregistry/flux:0.56.0",
bootstrapTokenRef: secrets.bootstrapTokenRef,
encryptionKeyRef: secrets.encryptionKeyRef,
databaseUrlRef: secrets.databaseUrlRef,
serviceSubnetIds: network.privateSubnetIds,
loadBalancerTargetGroupArn: lb.targetGroupArn,
authProvider: "apiKeys",
replicaCount: 2, // >1 requires PostgreSQL + sticky routing on the LB
});
new FluxWorkerPool("worker-default", {
poolName: "default",
image: "myregistry/flux:0.56.0",
bootstrapTokenRef: secrets.bootstrapTokenRef,
serverUrl: server.internalUrl,
minReplicas: 2,
maxReplicas: 20,
scaleMetric: "cpu",
});
new FluxWorkerPool("worker-gpu", {
poolName: "gpu",
image: "myregistry/flux:0.56.0",
bootstrapTokenRef: secrets.bootstrapTokenRef,
serverUrl: server.internalUrl,
minReplicas: 0,
maxReplicas: 5,
scaleMetric: "cpu",
extraLabels: { gpu: "true" },
});
Python composition sketch
import pulumi
from pulumi_flux import FluxPostgres, FluxSecrets, FluxServer, FluxWorkerPool
db = FluxPostgres("flux-db",
name="flux",
instance_size="db.t4g.medium",
vpc_id=network.vpc_id,
subnet_ids=network.private_subnet_ids,
multi_az=True)
secrets = FluxSecrets("flux-secrets",
name_prefix="flux",
database_url=db.connection_url)
server = FluxServer("flux-server",
image="myregistry/flux:0.56.0",
bootstrap_token_ref=secrets.bootstrap_token_ref,
encryption_key_ref=secrets.encryption_key_ref,
database_url_ref=secrets.database_url_ref,
service_subnet_ids=network.private_subnet_ids,
load_balancer_target_group_arn=lb.target_group_arn,
auth_provider="api_keys")
FluxWorkerPool("worker-default",
pool_name="default",
image="myregistry/flux:0.56.0",
bootstrap_token_ref=secrets.bootstrap_token_ref,
server_url=server.internal_url,
min_replicas=2,
max_replicas=20,
scale_metric="cpu")
The sketches are illustrative — @pulumi/flux / pulumi_flux are not real packages today.
Guardrails
The same rotation and destruction concerns from Terraform apply, expressed in Pulumi’s vocabulary.
ignoreChangeson the secret resources that hold the bootstrap token and encryption key. Without it, a routinepulumi upcan replace the secret-version and lock out every existing worker.protect: trueon the encryption-key resource. Losing the encryption key makes every encrypted column in the Flux DB unrecoverable.protectprevents accidentalpulumi destroydeletion.- Stack separation. Put the encryption key in a
secretsstack that’s separate from theappstack. A routinepulumi destroyon the app stack should not be able to reach into the secrets stack. - The server’s replica count is validated, not hard-coded.
replicaCount > 1is legitimate — replicas coordinate through PostgreSQL — but the constructor should throw onreplicaCount > 1with a SQLite database URL, and should refuse to wire a multi-replica server to a load balancer without connection affinity. (Older component sketches hard-coded the count to one; that constraint dated from the pre-0.5x scheduler and no longer applies.)
What can go wrong
pulumi up triggers a replacement of the bootstrap secret
Symptom. A routine pulumi up plan shows ~ flux-secrets:bootstrap-token as needing replacement; applying it logs every worker out.
Cause. ignoreChanges is missing on the secret-version resource, or a keepers-style trigger derives from a value that drifted.
Fix. Add pulumi.ResourceOptions({ ignoreChanges: ["secretString"] }) to the secret-version resource. Rotation moves to a separate flow with pulumi up --target on a dedicated rotation stack.
Destroying the dev stack destroyed the encryption key
Symptom. A teammate ran pulumi destroy on the dev stack and the encryption key went with it. Every encrypted column in dev is unreadable.
Cause. The encryption-key resource lived in the same stack as the rest of the dev environment, with no protect: true.
Fix. Move the key resource to a separate stack and add protect: true. For the affected dev environment, the recovery is “restore from backup” if you have one for dev, or “rebuild from scratch and accept the secret loss” if you don’t.
Cross-stack reference broke after a refactor
Symptom. StackReference lookups return empty values after one stack was renamed or refactored.
Cause. The reference name didn’t match after the change.
Fix. Update the StackReference name; redeploy. For the long-term, name stack outputs deliberately and don’t change them without a deprecation cycle.
Modules coming soon
An official @pulumi/flux (and pulumi_flux) package is planned. Until it ships, use the shape on this page as your authoring target.
Next
- Terraform for the HCL-flavored equivalent.
- Production checklist for the operational items the modules don’t cover.