Skip to main content
BlogDevOps

OpenTofu Ephemeral Resources and Write-Only Attributes

How OpenTofu 1.11 ephemeral resources, ephemeral variables and write-only attributes keep secrets out of state, plus the enabled meta-argument, with HCL.

Matthews Wong

Software Engineer

October 202612 min read
OpenTofu Ephemeral Resources and Write-Only Attributes

Key Takeaway

OpenTofu 1.11, released on 9 December 2025, keeps secrets out of state and plan files with ephemeral variables, outputs and resources, plus write-only attributes that are always stored as null. Plan cannot see changes to a write-only value, so pair each one with a version attribute. The new enabled meta-argument replaces count for zero-or-one instances.

The question that sent me into the OpenTofu 1.11 documentation was a plain one: why does a database password still end up in a state file when the configuration never prints it? Marking it sensitive hides it from the terminal. It does not remove it from the file that sits in your bucket.

This post reads the primary sources closely: the 1.11 what's new page, the release announcement of 9 December 2025, and the language reference pages for ephemerality, ephemeral resources, write-only attributes, variables, outputs and the enabled meta-argument. Every rule below comes from those pages. Where the docs gave a skeleton but no full example, the HCL here is my own illustration and says so.

Sensitive hides a secret; it does not remove it

The variables reference is blunt about this. A sensitive variable has its value hidden in plan and apply output, but sensitive values are still recorded in state in cleartext, so anyone with access to the state can read them. The ephemerality page recommends ephemeral values wherever possible, and says that if data must be stored, you should mark it sensitive and enable state and plan encryption.

Ephemeral values change the storage guarantee itself. The release announcement says they exist only in memory during a single OpenTofu phase, so they are never persisted in state snapshots or plan files. Write-only attributes sit at the other end of the pipe: they carry an ephemeral value into a managed resource without leaving a copy behind. The table puts the three mechanisms side by side.

MechanismIn stateIn plan fileCLI output
sensitive = trueStored in cleartextStored (encrypt it if you need to keep it)Shown as sensitive value
ephemeral variable or resourceNever storedNever stored; only the variable name is savedNot applicable: usable only in listed contexts
write-only attributeAlways nullAlways nullShown as write-only attribute

Read the table as a ladder. Sensitive is a display setting. Ephemeral and write-only are storage rules, enforced by OpenTofu rather than by good manners. The docs still recommend state and plan encryption for anything you cannot make ephemeral, because the two features cover different values.

Ephemeral variables and outputs in practice

An input variable becomes ephemeral with one argument. OpenTofu does not store its value in state, and the plan keeps only the variable name. Anything computed from it is ephemeral too: the docs show trimspace applied to an ephemeral password yielding an ephemeral local.

variable "db_admin_password" {
  type      = string
  ephemeral = true   # never written to state; the plan keeps only the variable name
}

locals {
  # Anything derived from an ephemeral value is itself ephemeral.
  sanitized = trimspace(var.db_admin_password)
}

# modules/secrets/outputs.tf -- a CHILD module. A root module output block
# cannot carry ephemeral = true.
output "db_password" {
  value     = local.sanitized
  ephemeral = true
}

Outputs follow a stricter rule. An output that references an ephemeral value must itself be marked ephemeral, and the outputs reference states that the argument is for child modules only: a root module output block cannot be configured with it. That detail matters because a scouted summary of the feature said outputs can be ephemeral without that qualification. In a module tree, secrets flow upward through child outputs, never out of the root.

For a root module variable the variables page only says it needs careful handling around the var and var-file options, and points to the apply docs. My own rule follows from that: do not put the value in a tfvars file in the repository, and do not type it on a command line that lands in shell history. Read it into an environment variable instead, as the second block shows.

# Wrong: the secret is now in your shell history (and often in a tfvars file in git).
tofu apply -var="db_admin_password=hunter2"

# Right: read it silently into the environment, then apply.
# Environment variables are literal strings by default, so no escaping is needed.
read -rs TF_VAR_db_admin_password && export TF_VAR_db_admin_password
tofu apply

Ephemeral resources: read a secret, never store it

An ephemeral block is the read-only sibling of a data block, with one difference that matters: its result is a temporary value that OpenTofu will not store in state or plan. The language page lists three steps in its lifecycle. OpenTofu validates the configuration with the provider, opens the resource to retrieve the value, and closes it once the current phase no longer needs it. It works only with providers that offer the capability, and it is not a drop-in replacement for data or resource blocks.

The reference supports depends_on, count, for_each, provider and lifecycle, and lifecycle takes only precondition and postcondition blocks. The OpenTofu pages I read give only the skeleton, so the AWS example below is my illustration. I confirmed the ephemeral block syntax from the docs, not the AWS type or attribute names; check them in the provider documentation before you copy.

terraform {
  required_version = ">= 1.11.0"   # ephemeral blocks need OpenTofu 1.11 or later
}

# Skeleton confirmed from the OpenTofu docs. The AWS type and the secret_string
# attribute below are ILLUSTRATIVE: verify them in your provider's documentation.
ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/db/admin"

  lifecycle {
    postcondition {
      condition     = self.secret_string != ""
      error_message = "The secret came back empty."
    }
  }
}

# Allowed: a provider configuration. Not allowed: count, for_each, lifecycle enabled.
provider "postgresql" {
  host     = "127.0.0.1"
  username = "admin"
  password = ephemeral.aws_secretsmanager_secret_version.db.secret_string
}

If the configuration is not fully known during planning, the open is deferred to apply, and it happens only after all dependencies are satisfied. The 1.11 announcement names two real uses: time-limited AWS credentials from OpenBao, and a temporary SSH tunnel for the cyrilgdn postgresql provider.

Write-only attributes and the version companion

A write-only attribute lives only in the configuration section of a resource. It is written to state and plan as null, the provider always returns it as null, and the CLI renders it as write-only attribute. Unlike a normal attribute it may reference both regular and ephemeral values, which is the whole point: an ephemeral secret can finally flow into a managed resource.

resource "aws_secretsmanager_secret" "db" {
  name = "app/db-password"
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id

  # Write-only: state and plan hold null, the CLI prints (write-only attribute).
  secret_string_wo = var.db_admin_password

  # Regular companion: OpenTofu cannot see a change to the line above, so it
  # watches this one. Bump it (1 -> 2) to make the next apply write the new value.
  secret_string_wo_version = 1
}

The catch follows directly. OpenTofu cannot detect a change to a write-only value, because state holds null and the provider returns null. So providers are asked to pair each write-only attribute with a regular one. Changing the companion tells the provider to apply the new value. The docs use the AWS secret version resource, with secret_string_wo and secret_string_wo_version.

The practical consequence is that rotation is something you do on purpose. Edit the secret in the external system, then bump the version number in the configuration so the next apply writes it. If you forget the bump, plan shows no change and the resource silently keeps the old value.

Where an ephemeral value is allowed to go

Ephemeral values are marked at the source and tracked by OpenTofu. Using one anywhere outside the permitted contexts produces an error, which is the mechanism that stops a secret leaking into state by accident. The table collects the rules from the ephemeral resources, ephemerality and enabled pages.

Where the value is usedAllowedWhat the docs say
Ephemeral resources, variables, outputs, localsYesListed contexts; expressions derived from them stay ephemeral
Provider configurationYesThe point of a short-lived credential
Provisioners and connection blocksYesBut provisioner output is not suppressed; mark values sensitive too
Write-only attributesYesThe only managed resource arguments that accept them
Ordinary resource attributesNoUsing them elsewhere produces an error
count and for_eachNoExplicitly listed as a limitation
lifecycle enabled, including tofu.applyingNoThe enabled page also bars ephemeral values

Two helpers soften the rules. The ephemeralasnull function sanitises an ephemeral value for places that cannot accept one, and tofu.applying lets configuration tell the plan and apply phases apart. Validation conditions may also reference ephemeral values.

The enabled meta-argument replaces count for one instance

The other 1.11 feature in this assignment is enabled, a clearer alternative to count for a resource or module with zero or one instance. It goes inside the lifecycle block, which the what's new page says avoids conflicts with variables or arguments already called enabled. If it is true or omitted the resource behaves normally; if false it is not created and evaluates to null.

Concerncountlifecycle enabled
Typical expressioncount = var.create ? 1 : 0lifecycle with enabled = var.create
Referencing the resourceaws_instance.example[0]aws_instance.example
When disabledEmpty listEvaluates to null
Value restrictionsNumber; no ephemeral valuesBoolean only: not unknown, sensitive, null or ephemeral
CombiningCannot sit beside enabledCannot sit beside count or for_each
# Before: count with an index on every reference.
resource "aws_instance" "example" {
  count         = var.create_instance ? 1 : 0
  ami           = "ami-12345678"
  instance_type = "t3.micro"
}
output "instance_id" {
  value = var.create_instance ? aws_instance.example[0].id : null
}

# After: enabled inside lifecycle. No moved block needed; state is moved for you.
resource "aws_instance" "example" {
  ami           = "ami-12345678"
  instance_type = "t3.micro"

  lifecycle {
    enabled = var.create_instance   # boolean, known at plan time
  }
}
output "instance_id" {
  # A disabled resource is null, so compare before reading an attribute.
  value = aws_instance.example != null ? aws_instance.example.id : null
}

Migration is cheap. According to the enabled page, switching from count to enabled lets you drop the index, and OpenTofu moves the existing state automatically, so no moved block is needed. The cost is on the read side: a disabled resource is null, and touching an attribute of null fails during plan or apply. The page suggests guarding with try or a null check, and warns that try can mask other errors, so I prefer the explicit null comparison.

The what's new page also lists a smaller 1.11 item that fits this theme: the S3 backend can now tag state objects with state_tags, and tag the lock object with lock_tags when native S3 locking through use_lockfile is on. The page cites organisation and cost tracking as the uses. It says OpenTofu may also need the s3:PutObjectTagging permission, and it gives no version note for either argument.

terraform {
  backend "s3" {
    bucket       = "acme-tofu-state"
    key          = "prod/network.tfstate"
    region       = "ap-southeast-3"
    use_lockfile = true

    # New in the 1.11 line per the what's new page. You may also need
    # s3:PutObjectTagging on the state object.
    state_tags = {
      "object:type" = "state"
      "team"        = "platform"
    }
    lock_tags = {
      "object:type" = "lock"
    }
  }
}

Where it goes wrong

None of these failures is exotic. Each is a direct reading of a rule the docs state, and each fails quietly rather than loudly.

A write-only change that you forget to version produces no diff. State holds null and the provider returns null, so OpenTofu has nothing to compare. Treat the version attribute as part of the secret rotation runbook, not as an optional extra.

  • Sensitive is not a substitute. If a value cannot be ephemeral, it still lands in state in cleartext, so the docs' advice to enable state and plan encryption stands.
  • Provisioner output is not suppressed for ephemeral values. A secret passed to a provisioner can still be printed unless you also mark it sensitive.
  • Ephemeral values cannot drive count, for_each or lifecycle enabled. If the number of instances depends on a secret, restructure so it depends on a regular variable.
  • A disabled enabled resource is null. Any output or reference that reads an attribute directly will error at plan time for the false case, so test both values of the flag.
  • Compatibility is a goal, not a promise. The ephemerality page says the design is inspired by the Terraform 1.12 concept and aims for compatibility, but breaking changes may be accepted later if incompatibilities arise. Pin the version you tested.

One more limit worth stating: this is only available to providers that implement it. Ephemeral resource types and write-only arguments are provider features, so an older provider release may offer neither for the resource you care about. I did not survey which providers ship them, and I am not going to guess.

What to do on Monday

Start by measuring the problem rather than the feature. The first command in the block below searches your current state for secret-looking attribute names; it is a crude grep, not an audit, but a hit tells you which resource to convert first.

  1. Run the grep against tofu state pull and against a saved plan, and list every resource that stores a password, token or private key.
  2. Raise required_version to at least 1.11.0, and check that the provider version you use documents the ephemeral type or write-only argument you need.
  3. Convert one secret end to end: ephemeral variable or resource in, write-only attribute out, plus its version companion.
  4. Replace one count with 0 or 1 pattern with enabled, run plan on both flag values, and confirm no moved block is needed.
  5. Add state_tags to the S3 backend, and keep state and plan encryption on for everything that remains sensitive rather than ephemeral.
# 1. Crude but fast: which attributes in state look like secrets?
tofu state pull | grep -i -E '"[a-z_]*(password|secret|token|private_key)[a-z_]*"'

#    The same check on a saved plan, which can also hold values.
tofu plan -out=tfplan
tofu show -json tfplan | grep -i -E '"[a-z_]*(password|secret|token|private_key)[a-z_]*"'

# 2. Confirm the binary before you rely on any of this.
tofu version        # ephemeral blocks, write-only attributes, enabled: 1.11 or newer

# 3. After converting a resource, plan with the flag both ways.
tofu plan -var="create_instance=true"
tofu plan -var="create_instance=false"

The what's new page notes that the 1.11.x line is no longer actively maintained and points to 1.13.x as the latest documentation, so plan the upgrade to a maintained release rather than stopping at 1.11.0.

Rule to carry: decide for every secret whether it needs to exist in state at all. If the answer is no, make it ephemeral on the way in, write-only on the way out, and bump the version when you rotate it. Use sensitive and encryption only for what genuinely has to be stored.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is an ephemeral resource in OpenTofu?

It is an ephemeral block, available from OpenTofu 1.11, that reads a temporary value such as a secret and does not store it in state or plan. OpenTofu validates it, opens it to fetch the value and closes it when the phase no longer needs it. It only works with providers that offer ephemeral resource types.

What is a write-only attribute in OpenTofu?

It is a managed resource argument that accepts a transient value, such as a password, and is always written to state and plan as null. It can reference both regular and ephemeral values. In plan and apply output it is shown as write-only attribute.

How do I update a write-only attribute after the secret changes?

OpenTofu cannot detect the change, because state holds null and the provider returns null. Providers pair the write-only attribute with a regular version attribute, and changing that version tells the provider to apply the new value. The AWS secret version resource, for example, uses secret_string_wo and secret_string_wo_version.

Can a root module output be ephemeral in OpenTofu?

No. The outputs reference says the ephemeral argument is for child modules only, and a root module output block cannot be configured with it. An output that references an ephemeral value must be marked ephemeral, so secrets travel up through child module outputs but not out of the root.

How does the enabled meta-argument differ from count?

Enabled sits inside the lifecycle block and takes a boolean for a zero-or-one instance. When false the resource evaluates to null instead of an empty list, so you reference it without an index. It cannot be combined with count or for_each, and its value cannot be unknown, sensitive, null or ephemeral.