Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 8 min read

Stop Leaking Secrets With Terraform Ephemeral Resources and Write-Only Arguments

Terraform has always written your secrets to state in plaintext. Ephemeral resources and write-only arguments finally close that hole. Here's how to use both.

  • #terraform
  • #secrets
  • #ephemeral
  • #security
  • #state
  • #write-only

For most of Terraform’s life there was an uncomfortable truth nobody wanted to say out loud at the design review: if a value passes through a resource, it lands in state, and state is plaintext JSON. Your database password, your generated API keys, the private half of a TLS keypair — all sitting in a backend that, encrypted-at-rest or not, anyone with read access to state could terraform show and walk away with.

Ephemeral resources and write-only arguments are the two features that finally fix this properly. They’re related but distinct, and using them well changes how you handle secrets across your whole codebase.

The old workarounds were all bad

Before these features, your options for keeping a secret out of state were a parade of compromises:

  • Pull the secret from a data source — but data sources still persist their results in state.
  • Generate it outside Terraform and inject it — which breaks reproducibility.
  • Accept that it’s in state and lock the backend down hard — which is “hope nobody reads it.”

None of these actually kept the value out of state. They moved the problem around.

Ephemeral resources: values that never persist

An ephemeral resource produces a value that exists only during a single Terraform operation. It’s created, used, and discarded within the same plan or apply — it never touches state and never appears in the plan file.

ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/db/credentials"
}

ephemeral "random_password" "rotation" {
  length = 32
}

You reference these through the ephemeral. namespace. The key constraint is that an ephemeral value can only flow into other ephemeral contexts or into write-only arguments — Terraform will error if you try to assign it to a normal resource attribute, because that would persist it. That restriction is the whole guarantee: the type system stops you from accidentally leaking the value back into state.

Ephemeral resources also support open, renew, and close lifecycle phases, which makes them ideal for short-lived credentials like Vault dynamic secrets that need to be issued, used, and explicitly revoked within the run.

Write-only arguments: the secret goes in, but not into state

The companion feature is write-only arguments. These are resource arguments — conventionally suffixed _wo — whose values are sent to the provider but never stored in state. They pair with a version argument that tells Terraform when to re-send the value.

resource "aws_db_instance" "main" {
  identifier = "prod-db"
  engine     = "postgres"

  username                  = "appuser"
  password_wo               = ephemeral.aws_secretsmanager_secret_version.db.secret_string
  password_wo_version       = 1

  allocated_storage = 20
  instance_class    = "db.t3.medium"
}

Here password_wo takes the ephemeral secret and hands it to the provider to set the actual database password. But when you run terraform state show aws_db_instance.main, the password isn’t there. There is nothing to leak.

The password_wo_version is the clever part. Because the value isn’t in state, Terraform has no way to detect that it changed. So you bump the version integer to signal “re-send this.” Rotating the secret becomes:

  password_wo         = ephemeral.aws_secretsmanager_secret_version.db.secret_string
  password_wo_version = 2  # bump to trigger rotation

A realistic rotation pattern

Combine the two and you get a clean rotation story with zero plaintext at rest:

ephemeral "aws_secretsmanager_secret_version" "db" {
  secret_id = var.db_secret_id
}

resource "aws_db_instance" "main" {
  identifier          = "prod-db"
  engine              = "postgres"
  username            = "appuser"
  password_wo         = ephemeral.aws_secretsmanager_secret_version.db.secret_string
  password_wo_version = var.db_password_version

  allocated_storage = 20
  instance_class    = "db.t3.medium"

  lifecycle {
    ignore_changes = [allocated_storage]
  }
}

Rotate the secret in Secrets Manager, increment db_password_version, apply. The new password gets pushed to RDS and nothing sensitive ever hits your .tfstate.

Gotchas worth knowing before you rely on this

  • Provider support is per-attribute. A resource only has a password_wo if the provider implemented it. Check the provider docs for the specific resource; not every secret-bearing field is write-only yet.
  • Ephemeral values can’t be outputs (normally). You can’t pass an ephemeral value out as a regular module output, because outputs persist. There’s a separate ephemeral-output mechanism for module-to-module passing — don’t reach for a normal output block.
  • No drift detection is the trade-off. Because the value isn’t in state, Terraform can’t tell you it drifted out-of-band. If someone changes the DB password by hand, your version-gated config won’t notice. That’s the cost of not storing it.
  • Audit your existing state. Adopting write-only args going forward doesn’t scrub secrets already sitting in old state files. You still need to rotate anything that was previously persisted and consider re-keying your backend.

Roll this out incrementally

You don’t need to convert everything at once. Start with the highest-value targets: database passwords, generated tokens, and TLS private keys — the things you’d most regret leaking. Convert those to write-only arguments fed by ephemeral resources, rotate the underlying secrets so the old plaintext copies are worthless, and move outward from there.

When you’re reviewing these changes, pay close attention to the version arguments — a forgotten version bump silently fails to rotate, which is the kind of thing that’s easy to miss in a quick read. Running secret-handling changes through a code review pass helps catch the “this looks right but never actually rotates” class of bug. For more on hardening your Terraform workflow, see our full set of Terraform guides.

This is one of those rare features that’s strictly an improvement with almost no downside. If your state files still hold plaintext secrets, this is the week to start fixing it.

Provider support for write-only arguments varies by resource and version. Verify against current provider documentation before relying on these features for production secrets.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.