Ephemeral Values and Write-Only Attributes
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.
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
connectionblocks - 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:
- The pipeline injects
TF_VAR_app_secret(ephemeral variable), or anephemeralblock reads it from a secrets manager. - Locals reshape it; they become ephemeral automatically.
- A provider block is configured with it, or a write-only attribute receives it.
- A child module that needs to hand it on declares an ephemeral output.
- A
*_wo_versionargument 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
_woattribute. - Keep state encryption on anyway. Other attributes of the same resources are still stored.
- Audit existing modules for
random_passwordand 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
- Take a module that passes a database password through a normal
sensitivevariable to a normal argument. List each place the password is stored today (state, plan files, CI artefacts). - Rewrite it with an ephemeral variable and the provider’s write-only argument, and add a version variable. Note which stored places disappear.
- Try to expose the password as a root output and read the error. Then expose it from a child module with
ephemeral = trueand 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
nullin state and need a*_wo_versionbump to change. - Root outputs cannot be ephemeral. Ephemeral root variables must be supplied again at apply.
Sources
- OpenTofu, Ephemerality
- OpenTofu, Ephemeral resources
- OpenTofu, Write-only attributes
- OpenTofu, Input variables: ephemerality
- OpenTofu, Output values: ephemeral