Secrets: sensitive, ephemeral, and Write-Only Arguments
Throughout Part II we kept repeating one line: state stores every attribute in plaintext, including secrets. Article 7 even saw a password sitting right in the file. This article handles it. There are three tiers of tooling, and the one easily misunderstood is the first, sensitive, which does not solve the state problem despite many people thinking it does. The next two, ephemeral resources and write-only arguments, actually keep secrets out of state, and they're new features in recent Terraform releases.
Goal
Clearly distinguish three mechanisms: what sensitive does and doesn't do, and how ephemeral resources and write-only arguments fix the gap sensitive leaves. Prove it by grepping the state file directly.
sensitive: only hides the screen
Mark a variable or output sensitive so Terraform doesn't print its value to the terminal:
variable "db_password" {
type = string
sensitive = true
default = "super-secret-123"
}
output "password_echo" {
value = var.db_password
sensitive = true
}
Apply, and the output is hidden:
$ terraform apply -auto-approve
+ password_echo = (sensitive value)
+ conn_string = (sensitive value)
...
Outputs:
conn_string = <sensitive>
password_echo = <sensitive>
A nice detail: sensitivity propagates. If you take length(var.db_password) or embed the password in a connection string, the result is also treated as sensitive. Terraform even errors if an output contains a sensitive value without being marked sensitive, so you don't accidentally leak it:
Error: Output refers to sensitive values
...
Terraform requires that any root module output containing sensitive data
be explicitly marked as sensitive, to confirm your intent.
To see the real value you have to ask explicitly, and that's by design:
$ terraform output -raw password_echo
super-secret-123
So far sensitive looks fine. But check the state file:
$ grep 'super-secret-123' terraform.tfstate
super-secret-123
The password sits right there in state, unencrypted. The docs spell out this limitation: sensitive only "prevents Terraform from showing the value in the CLI output," while "Terraform will still store the value of a sensitive variable in your state." In other words, sensitive guards against shoulder-surfing, not against reading the state file. Anyone who gets the state still gets the secret. This is why Article 6 had to encrypt the state bucket — but better still is to keep secrets out of state in the first place.
ephemeral: a value that lives only within one run
Terraform 1.10 introduced the concept of ephemeral. The docs define an ephemeral resource as "a resource that is inherently temporary," and "Terraform does not store information about ephemeral resources in state or plan." They exist for exactly one operation then vanish, fitting things like a "temporary password" or a "connection to another system."
The syntax is an ephemeral block in place of resource:
ephemeral "aws_secretsmanager_secret_version" "db" {
secret_id = "my-db-secret"
}
The core difference from resource: a regular resource is written to state by Terraform; an ephemeral one is written nowhere. You use it to read a secret from a safe store (Secrets Manager, Vault) at apply time, use that value to configure another resource, while the secret itself never touches state. A value from an ephemeral resource can only flow into places that accept ephemeral values, and the prime example of such a place is a write-only argument.
write-only arguments: send to the provider then forget
Terraform 1.11 added write-only arguments. The idea, per the docs: "the provider uses the write-only value to configure the resource, then Terraform discards the value without storing it." The secret is sent to the provider during apply, the provider applies it to infrastructure, and once done the value evaporates, never landing in state or the plan.
Naming convention: a write-only argument has the suffix _wo, paired with a _wo_version argument. The _wo value itself doesn't go into state; only the _wo_version number is stored, so you control when it updates: bump the version and the provider writes the new secret. The AWS provider supports it on many resources, for example aws_db_instance has password_wo + password_wo_version, and aws_secretsmanager_secret_version has secret_string_wo + secret_string_wo_version (requires Terraform 1.11+).
Proof: the same password, two fates
Stand up two secrets side by side — one using the old-style secret_string, one using the write-only secret_string_wo — with the same value, then look at state:
variable "secret_value" {
type = string
sensitive = true
default = "p@ssw0rd-bai8-demo"
}
# OLD: the value will land in state
resource "aws_secretsmanager_secret_version" "legacy" {
secret_id = aws_secretsmanager_secret.legacy.id
secret_string = var.secret_value
}
# NEW: write-only, the value does NOT land in state
resource "aws_secretsmanager_secret_version" "wo" {
secret_id = aws_secretsmanager_secret.wo.id
secret_string_wo = var.secret_value
secret_string_wo_version = 1
}
Apply, then count how many times the password appears in state:
$ terraform apply -auto-approve
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
$ grep -o 'p@ssw0rd-bai8-demo' terraform.tfstate | wc -l
1
Exactly once, even though there are two secrets with the same value. Inspect each resource version closely:
legacy -> secret_string: p@ssw0rd-bai8-demo | secret_string_wo: None
wo -> secret_string: | secret_string_wo: None
The legacy version stores the password in the secret_string field. The wo version has both secret_string and secret_string_wo empty/null in state — the value was not recorded. That grep "1" is from the legacy version.
A natural question: did the write-only version's password actually reach AWS, or was it lost entirely? Check directly on AWS:
$ aws secretsmanager get-secret-value --secret-id tf-series-bai8-wo --query SecretString --output text
p@ssw0rd-bai8-demo
The value is right there on AWS. It was sent to the provider during apply and written to Secrets Manager — it's just that Terraform didn't keep it in state. This is exactly what write-only promises: the secret passes through Terraform to its destination, but leaves no trace in the state file. (The ARN in the real output contains the account number, replaced here with 111122223333.)
💰 Cost
Each secret in Secrets Manager costs roughly USD 0.40 per month, prorated by lifetime. This article creates two secrets and destroys them right away, so actual cost is a few cents. recovery_window_in_days = 0 allows immediate deletion instead of waiting the default recovery window.
Where secrets should come from
A practical point: even when using write-only, don't hardcode the secret in the .tf file like the demo does (purely for illustration). The value should come from outside: the environment variable TF_VAR_secret_value, or better, an ephemeral resource that reads straight from Secrets Manager / Vault at apply time and feeds it into the _wo argument. Combining ephemeral (read without storing) with write-only (write without storing) gives the secret a complete path while state stays clean end to end.
🧹 Cleanup
$ terraform destroy -auto-approve
Destroy complete! Resources: 4 destroyed.
Wrap-up
sensitive only hides the value from output, still storing plaintext in state — it guards against shoulder-surfing, not against reading state. Ephemeral resources (1.10) read a value without writing it to state or the plan; write-only arguments (1.11, suffix _wo + _wo_version) send the secret to the provider then throw it away. The demo showed that with the same password, the secret_string version leaks into state while the secret_string_wo version doesn't, even though the value still reaches AWS correctly. This is the current way to keep state free of secrets — replacing the old approach of relying only on state encryption.
Part II closes here: state is in a safe place, lockable, operable, and no longer leaking secrets. Part III turns to making configuration flexible. Article 9 opens with variables, outputs and locals — how to parameterize configuration so the same code runs for multiple environments, along with validation and precondition/postcondition to catch errors early.