Terraform State
Every stack keeps its Terraform state in one of two modes, chosen in the wizard's Git Config step under Terraform State and shown on the stack's Settings tab. The mode cannot be changed after creation.
| Mode | Who owns the state | How it works |
|---|---|---|
| Zenfra-managed (default) | Zenfra | Zenfra stores and locks the state for you. Your source needs no backend block. |
| Customer-managed S3 | You | Your source declares an s3 backend and Zenfra never reads, writes or stores the state. |
Zenfra-managed
Zenfra keeps state versions for 30 days. Any member can list them through the API; rolling back to an earlier version requires the Admin role. A managed stack cannot be converted to customer-managed, and there is no self-service export of its state.
Customer-managed S3
Choose this when the state must stay in your own account. Your source declares its backend "s3" block; Zenfra verifies that it resolved and never touches the state.
The contract. After init, the worker checks which backend the tool actually resolved and accepts s3 only. No backend block, a local backend, any other remote backend, or unreadable backend metadata fails the run before plan or apply with BACKEND_POLICY_REFUSED and a log line naming the resolved type when there is one, or otherwise why verification failed. Without this refusal a missing backend would write state into the run's temporary workspace and the next run would propose recreating everything.
What you provide.
- Credentials for the bucket, the same way you provide them for providers: through the stack's AWS integration, which puts credentials in the run environment, or through stack, bundle or run variables. Zenfra does not broker credentials for the backend itself.
- Locking. State locking is whatever your backend block configures (
use_lockfileor a DynamoDB table). Zenfra's own lease still serializes runs of one stack, but it does not lock the remote state. - The bucket, key and region, and any endpoint settings for S3-compatible storage.
Backend changes are your migration. The mode is fixed, but the backend block is source code. Every run starts in a clean workspace with a non-interactive init, so Zenfra cannot migrate state between backends. Move the state out of band (for example terraform init -migrate-state from an operator machine) before running the changed source.
Workers. Runs of a customer-managed stack are dispatched only to workers that advertise the external-state-v1 capability. On a private pool, upgrade the worker image; until then such runs stay Queued.
Not available in this mode: state versions and rollback, and the resources inventory.
Reserved variables
This applies to every stack, in both modes. The worker owns the tool's working data directory, home directory and CLI configuration for each run, and refuses the variables that would relocate or bypass them. Setting any of these in a stack, bundle or run variable fails the run with ENV_POLICY_REFUSED and a message naming the variables:
| Variable | Why |
|---|---|
TF_DATA_DIR, HOME, TF_CLI_CONFIG_FILE |
Worker-owned, set per run |
TF_PLUGIN_CACHE_DIR, TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE |
Would relocate provider binaries outside the run |
TF_CLI_ARGS, TF_CLI_ARGS_<subcommand> |
Would inject arguments into tool commands, including init |
TERRAFORM_CONFIG |
Deprecated alias for TF_CLI_CONFIG_FILE |
TF_REATTACH_PROVIDERS |
Would bypass provider installation |
TF_VAR_* and TF_WORKSPACE pass through unchanged. Custom CLI configuration and provider mirrors are not supported while these values are worker-owned. This policy arrived with the September 2026 release; see the upgrade notes if runs started failing with ENV_POLICY_REFUSED.