Terraform Associate: inspecting state from the CLI addresses, state list and show, and where sensitive values really live
List before you show, quote the address, and remember that sensitive changes what is printed, not what is stored.
The problem
Someone asks two questions: what does Terraform think exists, and what is the database password it generated? State answers both, because Terraform uses it to "map real world resources to your configuration". The answers come through several commands, and each shows a different slice. Some hide sensitive values, some print them in plain text, and an address typed into a shell can break before Terraform reads it.
The Terraform Associate 004 exam tests Terraform 1.12. It asks which command fits a job and what that command shows (objective 7b, "Use the CLI to inspect state"). It also touches resource addressing (4b), state (2d) and sensitive data (4h).
Every command below was run in a lab with only local providers: random, local, and the built-in terraform_data resource type. The state lived in a file on disk (the local backend), so no cloud account was involved. The lab ran Terraform 1.16.0, plus Terraform 1.12.2 (the exam's version) for the steps where the two versions differ. All passwords and names in the transcripts are throwaway values from one lab run.
The idea, in plain words
Point at the exact instance you mean, using its address. List addresses before you show one, because show accepts only a single instance. Quote the address so the shell leaves it alone. Remember that marking a value sensitive = true changes what the CLI (command-line interface) prints, not what the state file stores. Inspection commands read recorded state and do not look at the real objects. Terraform refreshes state from real infrastructure before operations such as plan, so a plain inspection can show a stale picture.
Resource addresses and terraform state list
State is the file where Terraform records which real objects it manages and the attributes of each. A resource instance is one tracked object. A single resource block can create several instances through count (a meta-argument that creates N copies numbered from 0) or for_each (which creates one copy per key in a set or map). A module is a folder of configuration called from a module block. A resource address is the string that names instances in this structure, and the documentation defines it as "a string that identifies zero or more resource instances in your overall configuration".
Addresses exist so that every command names the same object the same way, even when one block produced several identical-looking instances. Here is the lab's root configuration:
terraform {
required_providers {
random = { source = "hashicorp/random", version = "~> 3.8" }
local = { source = "hashicorp/local", version = "~> 2.6" }
}
}
variable "envs" {
type = set(string)
default = ["dev", "prod"]
}
# count: instances are numbered 0, 1, ...
resource "random_pet" "name" {
count = 2
}
# for_each: instances are keyed by the set's strings
resource "random_password" "db" {
for_each = var.envs
length = 16
special = false
}
resource "local_file" "cfg" {
for_each = var.envs
filename = "${path.module}/rendered/${each.key}.conf"
content = "app=${random_pet.name[0].id}\n"
}
module "net" {
source = "./modules/net"
cidr = "10.20.0.0/16"
}
output "pet_names" {
value = random_pet.name[*].id
}
output "db_password" {
value = random_password.db["prod"].result
sensitive = true
}
The random_pet.name block uses count = 2, so it yields random_pet.name[0] and random_pet.name[1]. The random_password.db and local_file.cfg blocks use for_each over the set ["dev", "prod"], so their instances are keyed by those strings. The net module (in modules/net/main.tf) creates two terraform_data instances with count = 2, which gives addresses beginning with module.net. Outputs are not resources, so they never appear in the list.
$ terraform state list
local_file.cfg["dev"]
local_file.cfg["prod"]
random_password.db["dev"]
random_password.db["prod"]
random_pet.name[0]
random_pet.name[1]
module.net.terraform_data.subnet[0]
module.net.terraform_data.subnet[1]
That is eight addresses. The documentation says the listing is "sorted according to module depth order followed alphabetically", which explains why the root module's resources come first and module.net comes last. Because the same grammar applies everywhere, you can filter with it. Patterns are given in resource addressing format, and the lab tried three filters:
$ terraform state list random_password.db
random_password.db["dev"]
random_password.db["prod"]
$ terraform state list module.net
module.net.terraform_data.subnet[0]
module.net.terraform_data.subnet[1]
$ terraform state list -id=fresh-alien
random_pet.name[1]
The first filter named a resource and returned both of its instances. The second named a module and returned everything inside it, and the documentation adds that this includes submodules. The third, -id=, searches by the object's ID. The pet name fresh-alien is the ID of a random_pet, so the command located the configuration behind a value seen in the real world.
The address grammar is [module path][resource spec]. A count instance is written [N] with a 0-based number. A for_each instance is written ["INDEX"] with a string key. The documentation also states that omitting the index "means that the address references all instances".
On the exam: state list takes optional addresses, lists matching resources from the state file, and accepts -id=. An address without an index names every instance.
Symptom: terraform state show random_password.db fails.
Error: No instance found for the given address!
Cause: state show requires an address that points to a single resource instance, and the bare address names every instance of a for_each resource.
Fix: run terraform state list random_password.db first, then show one of the listed addresses.
terraform state show and quoting addresses in a shell
terraform state show prints the attributes of one resource instance as recorded in state. It exists so that you can read what Terraform stored without opening the state file, which HashiCorp says not to edit directly. Because a for_each address contains double quotes, the shell can alter it before Terraform runs. The documentation requires single quotes around the whole address in that case.
$ terraform state show 'random_password.db["prod"]'
# random_password.db["prod"]:
resource "random_password" "db" {
bcrypt_hash = (sensitive value)
id = "none"
length = 16
lower = true
min_lower = 0
min_numeric = 0
min_special = 0
min_upper = 0
number = true
numeric = true
result = (sensitive value)
special = false
upper = true
}
With the quoted address, the command exited 0. The attributes are listed, and Terraform replaced the sensitive result and bcrypt_hash with the placeholder (sensitive value). Count and module instances use the same form:
$ terraform state show 'random_pet.name[1]'
# random_pet.name[1]:
resource "random_pet" "name" {
id = "fresh-alien"
length = 2
separator = "-"
}
$ terraform state show 'module.net.terraform_data.subnet[1]'
# module.net.terraform_data.subnet[1]:
resource "terraform_data" "subnet" {
id = "65292efd-f80a-48c7-1ebe-b2c482bc36fc"
input = "10.20.1.0/24"
output = "10.20.1.0/24"
}
Unquoted, the same address fails in three different ways. In bash, echo shows what the shell does to an unquoted address:
$ echo random_password.db["prod"]
random_password.db[prod]
The double quotes vanished before any program saw them. Terraform 1.12.2 then received random_password.db[prod] and failed with exit code 1:
Error parsing instance address: random_password.db[prod]
This command requires that the address references one specific instance.
Terraform 1.16.0, given the same input, crashed with panic: can't construct diagnostic(s) from string and exit code 11. That is a crash in that release, not documented behaviour, and a script checking only for exit code 1 would miss it. In zsh the failure came earlier, with zsh:1: no matches found: random_password.db[prod] and exit 1, because zsh treats the square brackets as a file-matching pattern and stops before Terraform starts.
The documentation gives one form per shell. On Linux, macOS and UNIX, use terraform state show 'packet_device.worker["example"]'. PowerShell uses terraform state show 'packet_device.worker[\"example\"]', and Windows cmd.exe uses terraform state show packet_device.worker[\"example\"]. The lab did not test PowerShell or cmd.exe.
The -json flag differs by version. In Terraform 1.16.0, state show -json worked and the output had the keys diagnostics, format_version and resource. Terraform 1.12.2 answered Error parsing command-line flags: flag provided but not defined: -json with exit 1. This matches the documentation, which says JSON output "requires Terraform v1.16 or later". On a 1.12 exam, the machine-readable route is terraform show -json, which reads the whole latest state snapshot.
On the exam: state show needs a single instance address, and addresses containing double quotes go inside single quotes on Linux, macOS and UNIX.
Symptom: terraform state show random_password.db["prod"] fails with Error parsing instance address: random_password.db[prod].
Cause: the shell removed the double quotes before Terraform received the argument.
Fix: wrap the whole address in single quotes, as the documentation prescribes.
Where sensitive values are hidden, and where they are not
A sensitive value is one marked with sensitive = true on a variable or output, and an output is a named value a configuration exposes after apply. The marking exists so that casual terminal output and logs do not show secrets. The documentation says Terraform "redacts those values from CLI output", and it treats expressions that reference a sensitive value as sensitive too. Redaction applies to display only, so each command either honours it or does not.
$ terraform output
db_password = <sensitive>
$ terraform output db_password
"bUf06nxjy4I8WwcM"
$ terraform output -raw db_password
bUf06nxjy4I8WwcM
$ terraform output -json db_password
"bUf06nxjy4I8WwcM"
Plain terraform output hid the value. Naming the output, or adding -raw or -json, printed it. The documentation confirms this: Terraform "does not redact sensitive values when you specify the output by name", and with -json or -raw it displays them in plain text. The -raw flag exists for shell scripts and prints strings, numbers and booleans without formatting, so scripts get usable values at the cost of the redaction.
The state file itself holds the secret in readable form:
$ terraform show -json | jq ...
{"address":"random_password.db[\"prod\"]","sensitive_values":{"bcrypt_hash":true,"result":true},"result":"bUf06nxjy4I8WwcM"}
$ jq ... terraform.tfstate
{"index_key":"dev","result":"yyKXhnLbzExQgxJE"}
{"index_key":"prod","result":"bUf06nxjy4I8WwcM"}
terraform show -json flagged result as sensitive under sensitive_values and printed the plaintext right next to it. The documentation warns that with -json, sensitive values in state "will be displayed in plain text". Reading terraform.tfstate directly showed both passwords. Local state is a plaintext file, and anyone who can open it can read every secret in it. In terraform console, the interactive prompt for evaluating expressions against state, the value is hidden until nonsensitive() strips the marking:
(sensitive value)
"bUf06nxjy4I8WwcM"
The first line came from typing random_password.db["prod"].result at the prompt, and the second from wrapping the same expression in nonsensitive(...). The documentation says that function should be used only when a new value was derived in a way that eliminates the sensitive portions, because it exposes whatever it is given.
| Command | What it printed in the lab |
|---|---|
terraform output | db_password = <sensitive> |
terraform output db_password | the password in quotes |
terraform output -raw db_password | the password, bare |
terraform output -json db_password | the password in quotes |
terraform show -json | sensitive_values flags next to the plaintext result |
terraform state show (quoted address) | (sensitive value) |
terraform console | (sensitive value); with nonsensitive(), the password |
jq on terraform.tfstate | both passwords |
The lab's state show -json run printed only the top-level keys, so it does not show how that format treats sensitive values.
On the exam: sensitive = true redacts CLI output and does not encrypt state. Protect the state file, and keep it out of version control, since storing it somewhere without locking and secure access control can expose secrets.
Symptom: a CI (continuous integration) job's log shows a database password even though the output is marked sensitive.
Cause: the job ran terraform output -raw or asked for the output by name, and neither redacts.
Fix: treat any output read by name, with -raw or with -json as secret material inside the script, and restrict who can read state.
State metadata, backups, and what inspection does not do
State carries bookkeeping beside the resources. terraform state pull downloads and prints the raw state as JSON (JavaScript Object Notation), and the lab trimmed it to the metadata:
{"version":4,"terraform_version":"1.16.0","serial":9,"lineage":"f80cf7f7-dd42-7938-e28d-9c4175f054a1"}
The serial is a number that rises as state changes, and the lineage is a UUID (universally unique identifier) that identifies one state's history. These matter to state push, which uploads a state. Terraform refuses the push if the lineage differs, since the states are "completely different", or if the destination's serial is higher, since it holds data the pushed copy lacks.
Serial changes followed applies in the lab. A no-op apply ("No changes. Your infrastructure matches the configuration.") left the serial at 9. An apply that added two resources (local_file.cfg["stage"] and random_password.db["stage"]) moved it to 12 and left a terraform.tfstate.backup whose serial was 9. The lab did not work out why one apply raised the serial by three, only that it rose. Terraform keeps "a backup of the previous state" in that file, and modifying state subcommands always write a backup. Read-only subcommands such as list do not.
Reading is also not checking. The lab deleted rendered/dev.conf by hand and then ran the commands again:
$ rm rendered/dev.conf; terraform state list local_file.cfg
local_file.cfg["dev"]
local_file.cfg["prod"]
local_file.cfg["stage"]
$ terraform plan -refresh-only
Note: Objects have changed outside of Terraform
# local_file.cfg["dev"] has been deleted
The list still showed the dev file, while terraform plan -refresh-only, which refreshes state against real infrastructure without proposing changes, reported it deleted. The documentation notes that terraform console holds a lock on state. The lab did not test that. The lab also stopped at the plan and did not reconcile anything.
On the exam: state pull outputs raw state, and state push is blocked by a differing lineage or a higher remote serial.
Symptom: terraform state list shows a resource that a colleague deleted by hand last week.
Cause: list reads the recorded state and does not refresh it.
Fix: run terraform plan -refresh-only to see the drift, then decide how to bring state and configuration back in line.
Putting it together
- Run
terraform state list(with an address or-id=to narrow it), thenterraform state showon one single-quoted instance. - Remember that outputs read by name,
-rawor-jsonprint secrets, andshow -jsonprints them next to theirsensitive_valuesflags. - On Terraform 1.12, use
terraform show -jsonfor machine-readable state. - Use
terraform state pullfor raw state, serial and lineage. - Use
terraform plan -refresh-onlyto see drift.
| Exam cue | Answer |
|---|---|
| List every resource instance in state | terraform state list |
| Find which resource has a given ID | terraform state list -id=ID |
Show attributes of one for_each instance on Linux or macOS | terraform state show 'type.name["key"]' |
state show on a bare for_each resource | fails; it needs a single instance |
Output marked sensitive, read with -raw | printed in plain text |
| JSON output of state on Terraform 1.12 | terraform show -json |
Does sensitive = true encrypt state? | No; it redacts CLI output only |
Which state subcommands write backups? | The ones that modify state, never read-only ones such as list |
The lab verified Terraform 1.16.0 and 1.12.2, random 3.9.1, local 2.9.1, terraform_data, and the local backend. It did not test remote backends or HCP Terraform, lock contention, state mv, state rm or state push, PowerShell and cmd.exe quoting, or ephemeral values.
Check yourself
for_each over ["dev", "prod"] to create random_password.db. An engineer runs terraform state show random_password.db and gets an error. What should they do?terraform state list random_password.db to see the instance addresses, then show one of them, because state show needs an address that points to a single instance.terraform state show random_password.db["prod"] returns Error parsing instance address: random_password.db[prod]. Why, and what is the fix?sensitive = true. Which of terraform output, terraform output db_password and terraform output -json db_password still hides the value?terraform output, which prints <sensitive>. Asking for the output by name, or using -raw or -json, prints it in plain text.terraform.tfstate. Is that right?sensitive = true only redacts CLI output, so access to state files must be restricted.terraform state list still shows a resource that someone deleted outside Terraform. Which command reveals the drift?terraform plan -refresh-only, because state list reads only recorded state, while the refresh compares state to the real objects.