Skip to content
Lumaft documentation contents
Lumaft documentation

Connect a backend

Declare the state buckets Lumaft observes, grant it read-only access, and verify the connection.

Connect a backend

Lumaft has two integration points with your infrastructure:

Connection Direction Required Credential
State backend Lumaft → S3 Yes Read-only cloud identity (task role, instance profile, or keys)
Runner evidence Runner → Lumaft No Integration token with operations:evidence:write

The two never share a credential. The identity that reads state cannot write evidence, and the token that writes evidence cannot read anything.

State backends

Before you write the file, collect the four facts it needs — bucket, prefix, region, and layout — with Find your backend details. Guessing the layout is the most common reason a healthy backend shows no stacks.

The backends file

Backends are declared in one JSON file named by LUMAFT_BACKENDS_FILE. Lumaft reads it exactly once at startup, validates the whole document, and refuses to start if any record is invalid. Changing the file requires a restart. When the variable is unset, Lumaft starts with no backends.

{
  "schemaVersion": 1,
  "backends": [
    {
      "kind": "s3",
      "id": "production",
      "displayName": "Production infrastructure",
      "bucket": "pulumi-state-bucket",
      "prefix": "team/platform",
      "layout": "project-scoped",
      "region": "us-east-1",
      "forcePathStyle": false,
      "enabled": true
    }
  ]
}
Field Required Purpose
kind Yes Only s3 is supported
engine No pulumi (default), terraform, or opentofu
id Yes Unique lowercase identifier; stable for the life of the installation
displayName Yes Human-readable name shown in the console
bucket Yes Existing bucket containing the state
prefix No Object-key namespace before the engine's state keys
layout Pulumi only Exactly project-scoped or legacy; must be omitted for other engines
readVersions No Terraform/OpenTofu only; also walk retained object versions on a versioned bucket
region Yes AWS region, or any value the S3-compatible service accepts
endpoint No HTTPS origin of an S3-compatible service
forcePathStyle No Path-style addressing; defaults to false
enabled Yes false keeps the record visible but skips all observation

Limits: at most 100 records, file size at most 1 MiB, UTF-8, no unknown fields, no repeated id. Credentials of any kind in this file are rejected.

Layouts

Each Pulumi backend names exactly one layout. Lumaft does not guess.

Layout Checkpoint location beneath the prefix
project-scoped .pulumi/stacks/<project>/<stack>.json[.gz|.zst]
legacy .pulumi/stacks/<stack>.json[.gz|.zst]

With prefix team/platform, the Pulumi directory is team/platform/.pulumi/. A bucket that holds both layouts, or several prefixes, needs one backend record per layout and prefix.

Pulumi has deprecated the legacy layout and intends to remove it. Lumaft reads legacy state directly and is unaffected, but treat a legacy backend as something to migrate.

S3-compatible stores

Add an endpoint and, usually, forcePathStyle: true:

{
  "kind": "s3",
  "id": "private-cloud",
  "displayName": "Private cloud infrastructure",
  "bucket": "pulumi-state-bucket",
  "layout": "legacy",
  "region": "local",
  "endpoint": "https://s3.internal.example",
  "forcePathStyle": true,
  "enabled": true
}

The endpoint must be HTTPS with no path, query string, fragment, or embedded credentials. Plain HTTP is accepted only for localhost, 127.0.0.1, and [::1].

Terraform and OpenTofu backends

A terraform or opentofu record has no layout. Lumaft observes version-4 state at env:/<workspace>/<project>/terraform.tfstate and <project>/terraform.tfstate beneath the prefix, and presents each as a project-scoped stack with its resources, non-sensitive outputs, serial, and engine version.

{
  "kind": "s3",
  "engine": "terraform",
  "id": "terraform-platform",
  "displayName": "Terraform platform state",
  "bucket": "terraform-state-bucket",
  "prefix": "platform",
  "region": "us-east-1",
  "readVersions": true,
  "enabled": true
}
  • Always set a prefix. Without one the IAM list condition cannot be narrowed and the grant covers the whole bucket.
  • readVersions: true walks every retained object version the bucket still holds, oldest first, so each serial becomes a witnessed transition instead of one collapsed diff. It requires the version-read permissions below.
  • The state key must be <project>/terraform.tfstate, optionally under the default env:/<workspace>/ prefix. A custom workspace_key_prefix or a multi-segment key is not observed.

Engine support bounds. State observation is verified against Terraform 1.15.9 and OpenTofu 1.12.6; other builds are unverified. Terraform and OpenTofu can write native S3 lock files when locking is enabled, but Lumaft does not currently observe them. Backend-native update history remains unavailable for those engines, so lock and update-history observation are Pulumi-only and these backends answer engine-unsupported for both rather than showing empty results.

File rules

LUMAFT_BACKENDS_FILE is interpreted literally: no expansion, no interpolation. The path must:

  • be absolute and normalized;
  • name a regular file, opened without following a final symlink, with a non-symlinked parent;
  • have a parent directory that is not writable by group or others;
  • not be writable by group or others.

Mode 0600 owned by UID 1000 is the recommended shape. Kubernetes-style projected volumes, which expose files through symlinks, are not supported; write the file into a task-local volume from an init step instead. Each environment guide shows this.

Credentials

Lumaft reads S3 through the standard AWS SDK credential provider chain. It does not have its own credential setting. In order, the SDK looks for:

  1. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and optionally AWS_SESSION_TOKEN
  2. A web identity token (AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_ARN)
  3. The ECS task role (container credentials endpoint)
  4. The EC2 instance profile (instance metadata)

Prefer the platform identity: an ECS task role, an EC2 instance profile, or a federated role. Use static keys only where no workload identity exists, scope them to the policy below, and rotate them. Never place any of these values in the backends file or in the image.

For S3-compatible stores, the same environment variables carry the store's access key pair.

IAM policy

Lumaft sends ListObjectsV2 and GetObject. That is the whole request surface for base observation; no configuration makes it write.

Pulumi backend. Replace BUCKET_NAME and OPTIONAL_PREFIX/; remove the prefix placeholder entirely when the backend has none.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListPulumiState",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::BUCKET_NAME",
      "Condition": { "StringLike": { "s3:prefix": ["OPTIONAL_PREFIX/.pulumi/*"] } }
    },
    {
      "Sid": "ReadPulumiState",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::BUCKET_NAME/OPTIONAL_PREFIX/.pulumi/*"
    }
  ]
}

Terraform or OpenTofu backend. State objects sit directly beneath the prefix, so the grant scopes to the prefix itself.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListEngineState",
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:ListBucketVersions"],
      "Resource": "arn:aws:s3:::BUCKET_NAME",
      "Condition": { "StringLike": { "s3:prefix": ["OPTIONAL_PREFIX/*"] } }
    },
    {
      "Sid": "ReadEngineState",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "Resource": "arn:aws:s3:::BUCKET_NAME/OPTIONAL_PREFIX/*"
    }
  ]
}

Drop s3:ListBucketVersions and s3:GetObjectVersion when readVersions is not set.

Additional grants

  • SSE-KMS buckets: kms:Decrypt on that specific key, and the key policy must permit it.
  • Several buckets or disjoint prefixes: one scoped statement pair per namespace.
  • S3-compatible stores: map the equivalent bucket-list and object-read operations for the configured namespace.

This observer policy is a permanent boundary. Lumaft never needs, and never asks for, bucket creation, object write, overwrite, delete, or any deployment permission.

Activation and observation

An enabled backend starts observation immediately after bootstrap. Lumaft runs three schedulers per backend:

Scheduler Cadence What it reads
Checkpoints Every 60 s Current stack state
Locks Every 60 s idle; every 5 s while a lock is active Active Pulumi locks (Pulumi only)
Update history Every 5 min Pulumi update records (Pulumi only)
  • Every cadence carries 10 % jitter. A failed cycle backs off exponentially, from twice the base delay to a 15-minute ceiling, and resets after a clean cycle.
  • All three schedulers share one installation-wide concurrency budget of two backends at a time.
  • A credential, network, or authorization failure is isolated to its backend. It never affects readiness or other backends.
  • A disabled record is visible with enabled: false and is skipped without contacting S3. Disabling or removing a record keeps its cached projection; Lumaft does not purge it automatically. What every later change does — disable, remove, re-add, rename, migrate a layout — is in Changing your backends safely.

Verify the connection

  1. Open Administration → Backends. Each record shows its engine, layout, diagnostic result, and last successful observation.

  2. Or, from a signed-in session, read GET /api/v1/backends. It returns every configured record, including disabled ones, and requires backends:read.

  3. Open Stacks. Projects and stacks appear after the first checkpoint cycle commits, within about a minute of startup.

ScreenshotAdministration → Backends page listing three backends — a Pulumi project-scoped backend, a Pulumi legacy backend, and a Terraform backend — each with engine, layout, diagnostic status, and last observation time

The diagnostic runs once after bootstrap, asynchronously, and does not gate readiness. There is no live reload; edit the file and restart to change the set.

Observation health

Administrators can read the process-local scheduler report at GET /api/v1/observation-polling (requires storage:health:read). It reports completed-cycle counts, sanitized failure classes, current backoff depth, and the actual jittered delay per scheduler, with no backend identity or provider payload. Counters reset on restart.

Troubleshooting

Symptom Likely cause Action
Startup fails with a backends-file reason code Invalid JSON, unknown field, duplicate id, bad path or mode Validate the document; check mode 0600, parent 0700, no symlinks
Backend diagnostic reports an authorization failure Missing s3:ListBucket or s3:GetObject, wrong prefix in the condition, or KMS denied Compare the policy with the examples above; check the key policy
Backend healthy, no stacks Wrong layout or prefix Confirm where .pulumi/ sits in the bucket
Terraform backend shows engine-unsupported for locks and history Expected Those tiers are Pulumi-only
Observation stalls with rising backoff Intermittent network or throttling Read /api/v1/observation-polling; check egress and NAT

Runner evidence (optional)

A backend record can tell Lumaft that an update completed. It cannot tell Lumaft that a run started, was cancelled, or was a preview. Runner integration closes that gap: your pipeline or developer machine reports the operation to Lumaft's ingestion API as it runs, using a separate write-only token.

It is optional, it never changes a Pulumi command's exit code, and it is documented on its own page: Runner integration.