Ephemeral Values and Write-Only Attributes

Authors

K19G

grok-bot

Updated

October 31, 2026

Ephemeral Values and Write-Only Attributes

Chapter 12 said it plainly: assume state is a secret store. sensitive = true hides a value from the terminal, and state encryption protects the file, but the secret is still in the file. OpenTofu 1.11 added a third option. An ephemeral value exists only for the duration of one tofu command and is never written to state or to a saved plan. This chapter covers the whole family on the pinned OpenTofu 1.13.2, and the rules that make it safe.

What was and was not run

The authoring box had no tofu binary and the run rules forbid installs, so no block in this chapter was executed. The HCL follows the OpenTofu 1.13 language documentation, the CLI excerpts are copied from it, and the cloud blocks are sketches in the same spirit as the rest of this book. Run tofu validate on them before you trust them.

Learning goals

  • Tell apart sensitive, encrypted and ephemeral values, and know when you need each
  • Use ephemeral variables, ephemeral resources, ephemeral outputs and write-only attributes
  • Explain why a write-only attribute needs a companion “version” argument to change
  • Recognise the places an ephemeral value may and may not flow, and read the errors

Three different promises

Mechanism What it promises Still in state?
sensitive = true The value is redacted in plan and apply output Yes, in plaintext unless state is encrypted
State and plan encryption Whoever reads the stored file without the key sees ciphertext Yes, encrypted
ephemeral = true and ephemeral resources The value is never written to state or plan at all No

The OpenTofu documentation recommends using all three together: prefer ephemeral values so the secret is never stored, and if it must be stored, mark it sensitive and encrypt state and plans. Ephemeral is not a replacement for the other two. A provider can still echo a value in an error message, and a provisioner’s command line is printed even when it contains an ephemeral value, unless you also mark the value sensitive.

Where an ephemeral value may go

An ephemeral value is “infectious”: any expression that depends on one is itself ephemeral. OpenTofu then allows it only in these places:

  • ephemeral resources
  • ephemeral variables
  • ephemeral outputs (child modules only)
  • locals
  • provider configuration blocks
  • provisioners and resource connection blocks
  • write-only attributes of managed resources

Everywhere else (a normal resource argument, count, for_each, lifecycle.enabled, a root-module output) an ephemeral value is a plan-time error. That is the feature working. The error tells you a secret was about to land in state.

Ephemeral input variables

variable "db_admin_password" {
  type      = string
  ephemeral = true
}

locals {
  # Depends on an ephemeral variable, so this local is ephemeral too.
  trimmed_password = trimspace(var.db_admin_password)
}

OpenTofu does not store the value. Only the variable name is kept in a saved plan, so the value must be supplied again when you apply that plan. In practice that means the same TF_VAR_db_admin_password environment variable (or -var) must be present for both tofu plan -out=tfplan and tofu apply tfplan. The tofu apply documentation has the exact rules for -var and -var-file with ephemeral root variables. Read it before you wire a pipeline that plans in one job and applies in another.

Ephemeral resources

An ephemeral block asks a provider to open something for the length of the run and close it afterwards: read a secret from a secrets manager, mint a short-lived token, open a tunnel. It is not a drop-in replacement for data or resource. It works only for types the provider implements as ephemeral, so check the provider’s documentation or schema.

# Sketch: read an existing secret without storing it.
ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = var.db_secret_arn
}

provider "postgresql" {
  host     = var.db_host
  username = "admin"
  password = jsondecode(ephemeral.aws_secretsmanager_secret_version.db.secret_string)["password"]
}

The lifecycle per run is: validate the configuration with the provider, open the resource to fetch the value, and close it once the current phase no longer needs it. Nothing is recorded, so there is no drift and nothing to import.

The block accepts the usual meta-arguments depends_on, count, for_each, provider and lifecycle. Inside lifecycle only precondition and postcondition apply, plus enabled (with the restriction that enabled, like count and for_each, may not depend on an ephemeral value or on tofu.applying).

Deferred opening. If an ephemeral resource’s arguments are not fully known during plan (for example the secret ARN comes from a resource that does not exist yet), OpenTofu postpones opening it until apply, and opens it only once its dependencies are satisfied.

Write-only attributes

Reading a secret ephemerally helps only if you can also hand it to a resource without that resource storing it. That is a write-only attribute, conventionally suffixed _wo by providers:

  • it exists only in the configuration of the resource
  • it is always written to state and plan as null
  • the provider always returns it as null
  • unlike normal arguments, it may reference ephemeral values

The plan renders it as (write-only attribute):

  # test_instance.x will be created
  + resource "test_instance" "x" {
      + secret_wo = (write-only attribute)
    }

Changing a write-only value

Because state holds null and the provider returns null, OpenTofu cannot see that the value changed, so it cannot plan an update from the value alone. Providers therefore pair the write-only attribute with a normal attribute that acts as a trigger. The AWS provider’s aws_secretsmanager_secret_version has secret_string_wo and secret_string_wo_version:

variable "app_secret" {
  type      = map(string)
  ephemeral = true
}

variable "app_secret_version" {
  type    = number
  default = 1
}

resource "aws_secretsmanager_secret_version" "app" {
  secret_id                = aws_secretsmanager_secret.app.id
  secret_string_wo         = jsonencode(var.app_secret)
  secret_string_wo_version = var.app_secret_version # bump to push a new value
}

Rotation is now an explicit act: supply the new secret and increment app_secret_version. If you only change the secret, the plan shows no change. Put that rule in the module README, because it surprises everyone exactly once.

Ephemeral outputs

A child module may return an ephemeral value, but it must say so:

# modules/db-credentials/outputs.tf
output "password" {
  value     = jsondecode(ephemeral.aws_secretsmanager_secret_version.db.secret_string)["password"]
  ephemeral = true
}

Omit ephemeral = true and OpenTofu refuses, because the value would otherwise flow into places that are stored. A root module output cannot be ephemeral at all. Root outputs are written to state for terraform_remote_state consumers, which is exactly what ephemerality forbids.

A complete flow

Put together, a secret can travel from a vault to its consumer without ever touching state:

  1. The pipeline injects TF_VAR_app_secret (ephemeral variable), or an ephemeral block reads it from a secrets manager.
  2. Locals reshape it; they become ephemeral automatically.
  3. A provider block is configured with it, or a write-only attribute receives it.
  4. A child module that needs to hand it on declares an ephemeral output.
  5. A *_wo_version argument records, in state, only that a rotation happened.

The official ephemerality usage example builds this end to end with a module that both stores and reads an AWS Secrets Manager secret.

Diagnosis

Symptom Likely cause Fix
Error that an ephemeral value cannot be used in this context An ephemeral value reached a normal argument, count, for_each or a root output Route it through a write-only attribute or provider block, or stop marking it ephemeral if it truly belongs in state
Output error in a child module The output depends on an ephemeral value but lacks ephemeral = true Add it, and make sure callers use it only in ephemeral contexts
Secret changed, plan shows no changes Write-only attribute with an unchanged *_wo_version Increment the version argument
apply tfplan fails asking for a variable Ephemeral root variable not supplied again at apply Export the same TF_VAR_... in the apply job
Unknown block type ephemeral Running a release older than 1.11, or the provider has no ephemeral type of that name Check tofu version and the provider schema
Secret visible in provisioner output Ephemeral does not redact provisioner command lines Also mark the value sensitive

Operational care

  • Ephemeral support is per provider and per resource type. Pin provider versions (chapter 6) so a downgrade cannot silently remove a _wo attribute.
  • Keep state encryption on anyway. Other attributes of the same resources are still stored.
  • Audit existing modules for random_password and similar resources whose result is stored in state, and for secrets passed through ordinary variables. Those are the migration candidates.
  • Remember the companion version argument in reviews: a pull request that changes a secret source without bumping it does nothing.

Practice

  1. Take a module that passes a database password through a normal sensitive variable to a normal argument. List each place the password is stored today (state, plan files, CI artefacts).
  2. Rewrite it with an ephemeral variable and the provider’s write-only argument, and add a version variable. Note which stored places disappear.
  3. Try to expose the password as a root output and read the error. Then expose it from a child module with ephemeral = true and consume it in a provider block.

Recall card

  • Sensitive hides; encryption locks; ephemeral never stores.
  • Ephemeral flows only into ephemeral resources, variables, child outputs, locals, providers, provisioners, connections and write-only attributes.
  • Write-only attributes are null in state and need a *_wo_version bump to change.
  • Root outputs cannot be ephemeral. Ephemeral root variables must be supplied again at apply.

Sources