Terraform does not scan your infrastructure to find "its" resources. It keeps a state file that maps each address in the code to a real object and stores the last known attributes. Plan = desired config compared with state, after the state was refreshed from the real API.
flowchart LR CODE["Code: docker_container.web"] --> PLAN["terraform plan"] ST["State: address → id 4f1c2a"] --> PLAN API["Real API: Docker daemon"] -->|"refresh"| ST PLAN --> DIFF["Diff: add / change / destroy"] DIFF -->|"apply"| API %% step CODE: The configuration says what should exist, by address. %% step ST: The state remembers which real object belongs to each address, with every attribute. %% step API: Before planning, Terraform refreshes the state by reading each object from the provider API. %% step PLAN: Plan compares the refreshed state with the configuration. %% step DIFF: The diff lists what apply will create, update in place, replace or destroy.
What is inside
{
"version": 4,
"terraform_version": "1.13.4",
"serial": 17,
"lineage": "8b0f6c4e-...",
"resources": [
{ "mode": "managed", "type": "docker_container", "name": "web",
"instances": [ { "attributes": { "id": "4f1c2a...", "image": "sha256:...", "env": ["DB_PASSWORD=s3cret"] } } ] }
]
}
| Field | Meaning |
|---|---|
serial |
Increases on every write; an older serial cannot overwrite a newer one |
lineage |
ID created with the state; protects against pushing a state from a different project |
resources[].instances[].attributes |
Every attribute the provider returned, including passwords and keys |
outputs |
Root outputs, readable by others (module 2.4) |
Because secrets are stored in plain text, treat state like a password vault: encrypted storage, strict access, never in Git. sensitive = true only hides values from CLI output, not from state.
Inspecting state safely
| Command | Use |
|---|---|
terraform state list |
Every address |
terraform state show 'docker_container.app["web"]' |
Attributes of one instance |
terraform show -json \| jq |
Whole state as JSON for scripts |
terraform plan -refresh-only |
Show what changed outside Terraform, without proposing config changes |
terraform apply -refresh-only |
Accept those outside changes into state |
terraform state pull > backup.tfstate |
Download a copy before risky surgery |
Never edit the JSON by hand and state push it back unless you have a backup and no other choice; the state commands in topic 2.3 do the same jobs safely.
Refresh and performance
Every plan refreshes every resource. In a state with thousands of resources this is slow and hits API rate limits; that is one reason to split states (topic 2.4). -refresh=false skips the refresh for a quick look, but the plan may then be wrong.
Traps: deleting a resource from the state does not delete the real object, it only makes Terraform forget it; and two people running apply on a local state file at the same time will corrupt or lose changes, which is why teams use a remote backend with locking.