TerraformIX · State: The Core Production ConceptProduction Terraform
Exploring the State Safely
What you'll learn
- Use terraform state list to enumerate addresses
- Use terraform state show to inspect a single resource
- Use terraform state pull to fetch the raw JSON for tooling
- Use terraform output to read declared output values
- Recognise the read-only commands and never edit the state by hand
Prerequisites
None — start here.
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13
The single most useful state-discipline habit: before any mutating
operation, run the read-only commands. state list tells you what
Terraform thinks is there. state show tells you what Terraform
knows about one resource. state pull gives you the raw JSON for
tooling. terraform output reads declared outputs. None of them
change anything. All of them inform you before you make a change.
terraform state list
The basic enumeration:
terraform state list
Output:
aws_instance.web
aws_lb.main
aws_security_group.web
aws_subnet.public[0]
aws_subnet.public[1]
aws_subnet.public[2]
data.aws_ami.ubuntu
module.network.aws_route_table.rt["us-east-1a"]
module.network.aws_route_table.rt["us-east-1b"]
module.network.aws_route_table.rt["us-east-1c"]
module.network.aws_vpc.main
Every line is a complete resource address. Compare this list against
the resource blocks in the configuration. The two should match. A
resource in configuration but not in the list is a plan error. A
resource in the list but not in configuration is an orphan — a
candidate for state rm or for adding back to configuration.
The list accepts an address pattern to filter:
terraform state list module.network
terraform state list 'aws_subnet.public[*]'
The pattern syntax mirrors resource addressing.
terraform state show
For one resource, the full state record formatted for humans:
terraform state show 'aws_instance.web'
Output:
# aws_instance.web:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
arn = "arn:aws:ec2:us-east-1:123456789012:instance/i-0a1b2c3d4e5f6a7b8"
associate_public_ip_address = false
availability_zone = "us-east-1a"
id = "i-0a1b2c3d4e5f6a7b8"
instance_type = "t3.medium"
subnet_id = "subnet-0aaa"
tags = {
"Environment" = "production"
"Name" = "web"
}
...
}
The id is the real-world identifier. The other attributes are the
last-known values from the state cache. If the values look stale
(e.g. a tag that was added yesterday is missing), the state has not
been refreshed against the provider API.
state show does not refresh. It reads what is in state. To see
what the provider API currently returns, run terraform plan -refresh-only
or terraform apply -refresh-only.
terraform state pull
The raw state JSON:
terraform state pull
Output is the full document. Pipe through jq for navigation:
terraform state pull | jq '.resources[] | select(.type == "aws_instance") | .name'
Output:
"web"
state pull is the canonical way to feed the state into other
tools: drift detection, CMDB sync, audit pipelines. The output is
the exact bytes that the backend holds; nothing is reformatted.
The -json flag on most Terraform commands produces machine-readable
output. Combine with jq for production tooling:
terraform show -json | jq '.resource_changes[] | {address: .address, action: .change.actions[0]}'
terraform output
Declared output values:
terraform output
Output:
vpc_id = "vpc-0123456789abcdef0"
subnet_ids = [
"subnet-0aaa",
"subnet-0bbb",
]
database_endpoint = "db.example.internal"
For a single output:
terraform output -raw vpc_id
Output:
vpc-0123456789abcdef0
The -raw flag strips quotes; useful in shell pipelines:
VPC_ID=$(terraform output -raw vpc_id)
aws ec2 describe-vpcs --vpc-ids "$VPC_ID"
Outputs marked sensitive = true print as (sensitive value) in
the CLI. The -raw flag still works — it prints the value but
warns. The security lessons cover the discipline.
Combining read-only commands
A production pre-flight check before any mutating operation:
terraform state list | wc -l # resource count
terraform state pull | jq '.serial' # current serial
terraform state pull | jq '.lineage' # lineage
terraform output | wc -l # output count
terraform plan -refresh-only -out=/tmp/refresh.tfplan # drift check
terraform show -json /tmp/refresh.tfplan | jq '.resource_changes | length'
If all of these match expectations, proceed. If any are unexpected, investigate before applying.
Validation
READ-ONLY
Confirm the read-only commands work end to end:
terraform state list > /tmp/state-list.txt
wc -l /tmp/state-list.txt
terraform state pull | jq '.serial' > /tmp/serial.txt
cat /tmp/serial.txt
terraform output vpc_id
Output (illustrative):
142 /tmp/state-list.txt
27
"vpc-0123456789abcdef0"
A consistent resource count, serial that matches the last known value, and outputs that match expectations are the three signals that the state backend is reachable and the read-only path is intact.
Production failure modes
Symptom: terraform state list returns an empty list. Cause:
the state is reachable but contains no resources. Confirm with
terraform plan — an empty plan confirms there is no work to do.
If the configuration declares resources, investigate why state is
empty (perhaps a fresh backend).
Symptom: terraform state show returns “Resource not found in
state”. Cause: the address is misspelled, or the resource is in
a different module path. Use state list to find the correct
address.
Symptom: terraform state pull returns malformed JSON. Cause:
the state has been corrupted (manual edit, partial write,
backend integrity failure). Stop. Restore from the versioned
backup. Do not attempt to fix the JSON.
Symptom: terraform output prints “(sensitive value)” but the
pipeline needs the actual value. Cause: the output is marked
sensitive = true. The -raw flag returns the value but emits a
warning. The discipline: use -raw and pass the value directly
to the next command; never echo to a log.
Symptom: terraform output -json returns an empty object.
Cause: no outputs are declared. Confirm with the configuration.
Recovery
The read-only commands do not need recovery because they do not mutate state. The recovery pattern when read-only output looks wrong is to inspect the state from a different angle:
terraform state pull > current.json— confirm the JSON is valid.terraform state list— confirm the resource count.terraform plan— confirm the planned diff matches expectation.- If any of these are inconsistent, restore state from the versioned backup.
What comes next
The next part covers state operations: the imperative and declarative commands for moving resources, replacing providers, and importing existing infrastructure.
Verification
- You can list every resource address in state with
terraform state list. - You can inspect a single resource with
terraform state showand read theidattribute. - You can pull the raw state JSON and navigate it with
jq. - You can read declared outputs with
terraform outputand handlesensitive = truevalues correctly.
Knowledge check · 7 questions
Q1. Which command prints the raw state JSON?
Q2. Which flag on terraform output strips quotes from a single value for use in a shell pipeline?
Q3. `terraform state show` refreshes the state from the provider API before showing the resource.
Q4. Which command lists every resource address in state, including module paths and instance keys?
Q5. Which of the following are read-only operations on state? (Select all that apply.)
Q6. A `terraform state pull | jq` pipeline returns a serial that is two higher than expected but no apply has run in the last day. What does this mean?
Q7. An operator wants to verify which real-world AWS resource IDs are managed by Terraform before applying a change. Which read-only commands provide that?
Passing score: 75%. Answers are checked in this browser.