terraform state list: The Inventory Terraform Keeps
Before you change anything about state, you read it. State is the contract between your configuration and real infrastructure (State File Internals covered what it holds); the terraform state subcommands are how you query that contract from the CLI without opening the JSON by hand. Two of them do 90% of the work, and both are read-only, so there is no excuse for running any mutation without them first.
terraform state list prints every resource address recorded in the current state:
$ terraform state list
aws_instance.web
aws_instance.app[0]
aws_instance.app[1]
aws_sns_topic.events["orders"]
aws_sns_topic.events["payments"]
aws_security_group.alarms
module.network.aws_vpc.main
module.network.aws_subnet.private["eu-west-1a"]
module.network.aws_subnet.private["eu-west-1b"]
data.aws_ami.ubuntu
Read that output carefully — it is a precise inventory with a grammar. aws_instance.app[0] and [1] are the two instances of a count = 2 resource. The quoted keys (events["orders"]) are for_each elements, and unlike count indexes they are stable identities that survive removing a sibling. The module.network. prefix tells you the address lives in a child module, and data.aws_ami.ubuntu shows that data sources are recorded too — Terraform stores their attributes so references resolve without a fresh API call on every plan.
The list answers the questions you actually have before a change: does this object exist in state at all, what is its exact address spelling, and how many instances does a multi-instance resource currently have. That last one catches real mistakes: a state showing app[0] through app[4] when the config says count = 3 tells you someone applied with a different count and the next plan will destroy two instances — good to know before the plan, not during the review.
One property matters for the rest of this phase: the command is strictly read-only. It never contacts the cloud APIs, never mutates the state file, and never takes a lock that other operations would block on (it still runs against whatever backend init configured, so it sees the shared state, not a stale local copy — another reason local state failed teams). Every address you will ever pass to state mv, state rm, or terraform show should be copied from this listing rather than typed from memory, because a typo'd address in a mutation command is a mistake with consequences while a typo here just prints No state ... matches the given address and exits 1.
The listing also encodes information that is invisible in configuration. A state containing aws_instance.app[3] when the config now says count = 2 is a live record that someone applied with a larger count and then shrank it without applying the destroy — the orphaned instances are still running and still billing, and only the state/list mismatch reveals them. Similarly, an address under module.deprecated. whose module call was deleted months ago is Terraform's way of telling you the objects were never cleaned up: no plan will ever manage them again, and the only honest next step is state rm or re-declaring the module. Reading the inventory turns "why is this bill so high" into a two-line investigation instead of an afternoon of console archaeology. Get in the habit of running it before every plan you intend to act on: ten seconds of listing saves the far more expensive discovery that the object you were about to move was never there at all.
Narrowing a 400-Resource State: Prefixes, Patterns, and Where Things Live
A production state file easily holds several hundred addresses; a plain state list scrolls past. The command takes address patterns as positional arguments, and filtering with them is the difference between a usable inventory and a wall of text.
The simplest filter is a type or module prefix:
terraform state list aws_security_group # every security group, all instances
terraform state list module.network # everything inside one child module
terraform state list module.network.aws_subnet # subnets of that module only
terraform state list 'aws_sns_topic.events["orders"]' # exactly one for_each element
The glob form is the powerful one — quote it, or your shell will expand * against the working directory before Terraform ever sees it:
terraform state list 'module.*.aws_s3_bucket' # buckets in any child module
terraform state list 'aws_instance.*' # equivalent to the type prefix
terraform state list 'module.app.*' --module=app # historical --module flag for pre-0.13 layouts
Single-quote every pattern. An unquoted aws_instance.* in a directory containing files becomes a glob expansion of filenames, and in the mutation commands you will meet in Moving and Forgetting State Entries the same quoting discipline stands between you and a typo that Terraform acts on. Pipe the filtered result onward and the inventory becomes scriptable input: terraform state list 'aws_sns_topic.*' | xargs -n1 terraform state show walks every topic's attributes without a hand-written loop, which is how a state audit scales from five resources to five hundred.
Filtering is also an analysis tool, not just a display convenience. Several habits pay for themselves the first time you use them on a real incident:
- Count before a refactor.
terraform state list 'aws_ebs_volume.*' | measure-object(orwc -lon Linux) before and after acount/for_eachconversion tells you whether you caught every instance, and a mismatch means the conversion is not finished yet. - Find the spelling you need for imports and moves.
for_eachkeys with quotes, brackets, and dots (events["a.b"]) are exactly the addresses you will paste intostate mv— copying beats retyping every time. - Diff two inventories. Capture
state listto a file on Monday and Friday;fc /l(ordiff) between the two files shows exactly what a week of applies added or removed, which is how you catch the resource someone's "harmless" revert quietly stopped managing.
The full state also answers "what does this module actually manage" for a module you inherited. terraform state list module.billing plus a glance at the addresses is faster than reading the module source to learn what landed in the last six months of deploys. And when the listing itself surprises you — an address you do not recognize, a count higher than the config declares — that surprise is the finding: state records what was applied, not what is written, and every gap between the two is either an unreconciled change or an incident waiting for its ticket. Filtered listing is also the fastest onboarding tool there is: a new engineer who can read state list module.* already knows the system's real shape, which is often more than the architecture diagram shows.
terraform show: One Address, Every Stored Attribute
state list gives you addresses; terraform show gives you contents. With no argument it prints the entire state as formatted attribute sets — hundreds of screens in a real environment. With an address it prints exactly one resource's full recorded attribute set:
$ terraform show 'aws_instance.web'
# aws_instance.web:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
arn = "arn:aws:ec2:eu-west-1:111122223333:instance/i-0abc123def456"
id = "i-0abc123def456"
instance_type = "t3.micro"
private_ip = "10.0.1.17"
tags = {
"Name" = "web-01"
}
# (34 more attributes hidden — use -no-color or the console for all)
}
Everything shown is what Terraform recorded at the last refresh/apply, not a live API query — show reads state, nothing more. That distinction is the whole point: the attributes are the exact values your configuration's references (aws_instance.web.id, aws_instance.web.private_ip) will resolve to on the next plan. When a reference mysteriously evaluates to an unexpected value, state show on the source resource is the ground truth.
The command's behavior around mistakes is worth knowing. An unknown address is a clean failure — No resource instance "aws_instance.webb" matches the given address, exit status 1 — which makes it safe to script. Data sources print the same way (terraform show data.aws_ami.ubuntu), revealing the resolved AMI ID and filters that produced it, which is how you diagnose "the AMI lookup changed and nothing else did" incidents.
For tooling, terraform show -json emits the full state as machine-readable JSON (the resources array with every values object); -json combined with an address narrows to one resource. jq turns it into an audit query:
terraform state list | jq -r '.resources[] | select(.mode=="managed") | .instances[].attributes.id'
Two reading habits make show earn its place in an incident. First, timestamps in attributes: many resources record tags.LastSeenBy-style metadata or timeouts you set, and a mismatch between those and the deploy log tells you which change touched the object. Second, empty or placeholder values: an attribute showing null or an empty list in a resource you believe is populated usually means the last apply recorded a partial write — the interrupted-operation signature that the lock-recovery topic later builds on. A quick show pass after any interrupted apply is cheap insurance against discovering that partial state at the worst possible moment.
Now the workflow these commands exist to serve: inspect before you mutate. Before any refactor — renaming a resource, converting count to for_each, splitting a module — run state list and state show on the addresses you intend to touch. You are verifying three things: the object is actually in state, its address spelling is what you assumed, and the attributes are populated (an empty or stale entry means moving it just moves a problem). A refactor that begins with state mv on addresses guessed from the config — rather than read from state — is how objects that were never actually managed get moved, and how live objects get missed. This inspection habit is also the discipline interviewers probe for: anyone can recite the mv command, but the candidate who narrates "first I list the state, then I show the target, then I move, then I plan to confirm" is describing someone who has actually done it on production infrastructure. The next topic takes these read-only habits into the first mutating commands: moving and forgetting entries.
Quiz
1. You run `terraform state list 'aws_instance.*'` and the shell reports a syntax error or lists filenames instead of resources. What went wrong?
2. What does `terraform show aws_instance.web` actually display?
3. State contains `module.network.aws_subnet.private["eu-west-1a"]`. What does that address tell you?
4. Why is running terraform state list and state show mandatory before a refactor like count-to-for_each conversion?
Flashcards
Question
What does `terraform state list` print, and what does it deliberately not do?
Click to reveal answer
Answer
Every resource address recorded in the current state — plain (`aws_instance.web`), count-indexed (`app[0]`), for_each-keyed (`events["orders"]`), module-prefixed (`module.network.aws_vpc.main`), and data sources (`data.aws_ami.ubuntu`). It is strictly read-only: no cloud API calls, no state writes, no lock contention.
Question
How do you filter terraform state list by type, module, or pattern — and what is the quoting trap?
Click to reveal answer
Answer
Pass address patterns as positional arguments: `state list aws_security_group`, `state list module.network`, `state list 'module.*.aws_s3_bucket'`. Always single-quote wildcard patterns, or the shell expands `*` against the working directory and Terraform receives filenames instead of the pattern.
Question
What is the difference between terraform show with no argument and with a resource address?
Click to reveal answer
Answer
With no argument it prints every entry in the state as formatted attribute sets (hundreds of screens in production). With an address it prints that one resource's full stored attribute set. Unknown addresses fail cleanly with exit status 1, so the command is safe to script.
Question
Does terraform show query the cloud API for live attributes? What is the alternative?
Click to reveal answer
Answer
No — show reads state, displaying values recorded at the last refresh/apply. That is exactly why it is useful as ground truth for what references resolve to. Live values require a refresh (plan/apply do one), and machine-readable state is available via `terraform show -json`.
Question
What three checks does read-only state inspection perform before any mv/rm/import?
Click to reveal answer
Answer
1) The object exists in state at that address at all. 2) The address spelling (indexes, for_each keys, module prefixes) is exactly what you assumed. 3) The entry's attributes are populated, not stale or empty. Failing any check means the mutation would move a problem or miss a live object.
Revision Notes
Key Takeaways
- 1. terraform state list is the authoritative inventory of addresses — count indexes, for_each keys, module prefixes, and data sources all appear with a consistent grammar
- 2. Filtering with type prefixes and single-quoted glob patterns turns a several-hundred-line state into a targeted query
- 3. terraform show reads recorded state, not live cloud attributes — the exact values your configuration's references resolve to on the next plan
- 4. Both commands are read-only and script-safe (exit 1 on unknown addresses), which makes them the mandatory first step before any state mutation
- 5. Inspecting state before a refactor catches three failure classes: missing objects, misspelled addresses, and empty entries that would otherwise be moved or orphaned blind
Interview Tips
- • "How do you find what Terraform manages?" — `terraform state list`, then `state show` for any address; mention the address grammar (count/for_each/module/data) unprompted, it signals real state experience
- • "show reads state, not the API — what does that mean in practice?" — attributes are the values recorded at the last refresh/apply, so show is ground truth for reference resolution and post-incident forensics, while live drift needs a refresh
- • "Walk me through a safe resource rename" — start with state list/show to confirm the address and attributes, then state mv (next topic), then plan to confirm zero changes; leading with inspection is the answer interviewers weight
- • "Large state — how do you work with it?" — filtered listings by module/type, quoted globs, count-before-and-after for refactors, and -json + jq for audits; avoid saying you scroll the raw output
Cheat Sheet
State Inspection: list and show
Inventory:
terraform state list # every address in current state
terraform state list aws_security_group # filter by type prefix
terraform state list module.network # filter by module
terraform state list 'module.*.aws_s3_bucket' # glob — ALWAYS single-quote
Address grammar:
| Form | Example | Meaning |
|---|---|---|
| plain | aws_instance.web |
single resource |
| count | aws_instance.app[0] |
position in a counted list |
| for_each | aws_sns_topic.events["orders"] |
stable key identity |
| module | module.network.aws_vpc.main |
inside a child module |
| data | data.aws_ami.ubuntu |
data source (attributes stored too) |
Attributes:
terraform show aws_instance.web # one resource's recorded attributes
terraform show -json aws_instance.web # machine-readable, for jq/audits
Rules: show/list read state, not the cloud (last refresh/apply values). Both are read-only — no lock, no writes. Unknown address → exit 1. Inspect before every mv/rm/import/refactor: confirm existence, spelling, and populated attributes.