Skip to content

The State File

Terraform state is a JSON file that maps every resource in your configuration to the real-world object it created, and it is the only thing that lets Terraform know what it manages.

When you run terraform apply, Terraform creates resources through Google Cloud’s API — a Compute Engine instance, a Cloud Storage bucket, an IAM binding. The GCP API has no concept of “Terraform.” If a project has 500 Compute Engine instances, the Compute Engine API cannot tell you which of those instances Terraform created for a given resource "google_compute_instance" "web" block, which ones a teammate created by hand in the Console, and which ones belong to a completely different Terraform configuration.

Terraform solves this by keeping its own record. Every time you apply, Terraform writes the mapping between your configuration’s resource addresses and the real infrastructure IDs into a state file, by default named terraform.tfstate:

resource "google_compute_instance" "web" {
name = "web-vm"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
}
}

After apply, the state file records something like google_compute_instance.web -> projects/my-project-id/zones/us-central1-a/instances/web-vm, plus every attribute the Compute Engine API returned for that instance — its internal IP, its self link, its network interfaces, and so on. That cached data means Terraform does not have to call the GCP API and re-describe every resource just to compute a plan; it can compare your configuration against the last known state first, which is both how it detects what changed and a real performance win on large configurations. State is also where Terraform stores dependency metadata, so it knows the order in which resources must be created, updated, or destroyed.

The plaintext risk hiding in your state file

Section titled “The plaintext risk hiding in your state file”

State files are not sanitized. Any attribute value Terraform receives from a provider — or that you set directly in your configuration — is written into terraform.tfstate as plaintext, including values that are secrets. A common example:

resource "google_sql_user" "app" {
name = "app_admin"
instance = google_sql_database_instance.main.name
password = "S3cretPassword!"
}

You never ran terraform output on that password and you never printed it in a log line. It still lands in terraform.tfstate in plaintext, because Terraform has to remember every attribute it set in order to detect drift on the next plan. Anyone who can read that file can read the password.

This is why terraform.tfstate must never be committed to version control. Add it to .gitignore from the very first commit of a new project:

.gitignore
terraform.tfstate
terraform.tfstate.backup
*.tfstate
*.tfstate.*

The next lesson covers moving state off your laptop entirely and into a properly access-controlled remote backend, which is the real fix for this risk — a .gitignore entry only stops the most obvious accident.

Terraform ships three commands for looking inside state without opening the JSON file by hand:

Terminal window
# Human-readable dump of the entire current state
terraform show
# List every resource address currently tracked in state
terraform state list
# Show full attribute detail for one specific resource
terraform state show google_compute_instance.web

terraform state list is the fast way to answer “what does this configuration actually manage right now” — it prints addresses like google_compute_instance.web or module.network.google_compute_network.main with no other detail. terraform state show <address> drills into one of those addresses and prints every attribute Terraform has recorded for it, which is often faster than going to the Google Cloud Console when you need to check a specific value Terraform is tracking.

flowchart LR
  cfg["Terraform config (.tf files)"] --> apply["terraform apply"]
  apply --> gcp["Real GCP resources"]
  gcp -->|attributes written back| state["terraform.tfstate"]
  cfg -->|next run| plan["terraform plan"]
  state -->|read for comparison| plan
Config and real GCP resources both feed the state file that the next plan reads
Why does Terraform need a state file instead of just querying the cloud provider API each time
A database password was set as a resource attribute but was never printed with terraform output. Can it still end up readable in plain text
What is the difference between terraform state list and terraform state show
What belongs in .gitignore for a new Terraform project from the first commit