Terraform basics · Lesson 3 of 9 · about 20 minutes

State: how Terraform remembers

What Terraform's state file is, how to look inside it, why it can hold secrets and must stay out of Git, what happens when it's lost, and how to rename or stop tracking a resource safely.

By the end you'll know

  • What state is, and why Terraform needs it
  • How to look inside it with terraform state list and terraform state show
  • Why state can hold secrets, and must never go in Git
  • What happens when state is lost
  • How to rename a resource safely with a moved block, and stop tracking one with terraform state rm

Where you are

At the end of Lesson 2, you could read every kind of change in a plan. But each of those plans depended on something you haven't looked at yet: Terraform knew hello.txt already existed, knew its id, and knew what it contained before you changed it. That knowledge lives in one file, terraform.tfstate, and this lesson is about that file.

Before you start, set your terraform-practice folder back to a known point. Make sure main.tf holds just the one resource from Lesson 1, with its original content:

HCL
resource "local_file" "hello" {
  filename = "hello.txt"
  content  = "Hello from Terraform!"
}

Run terraform apply, and type yes if it plans a change.

What state is

When you leave your coat in a cloakroom, you get a ticket, and the attendant keeps the other half. The ticket is how they know which coat on the rail is yours. Without it, they'd be guessing, and a coat that looks like yours isn't necessarily yours.

State is Terraform's half of the ticket. For every resource in your configuration, it records which real thing in the world belongs to it, by its id, along with everything Terraform knows about it. That's how terraform plan works: it reads your files for what you want, reads the state for what it made, checks the real world, and works out the difference.

Your main.tf says "there should be a file called hello.txt". Only the state says "and this one, with this id, is the one I made".

Looking inside

You can ask Terraform what it's tracking:

BASH
terraform state list
OUTPUT
local_file.hello

One resource, by its address: the type and the name together. In a big project, this list is the quickest way to see everything Terraform manages. To see everything it knows about one resource:

BASH
terraform state show local_file.hello

Your screen shows a few more lines, mostly checksums of the file's content. These are the ones that matter:

Code
# local_file.hello:
resource "local_file" "hello" {
    content              = "Hello from Terraform!"
    filename             = "hello.txt"
    id                   = "cadbafd9572a72dc7769c97730552f5955e135fd"
}

Now open terraform.tfstate itself in your editor. It's JSON, the format from Python Lesson 5, and you'll find the same details in it, including "content": "Hello from Terraform!". Look, but don't edit it by hand: Terraform has commands for every change you'd want to make, and a typo here can make Terraform lose track of real things. You'll also see a terraform.tfstate.backup file next to it: the state as it was before the last change, kept in case something goes wrong.

State holds secrets

Look again at that content line. Terraform recorded exactly what it wrote into the file. If that content had been a password, the password would be sitting in terraform.tfstate, in plain text.

That's true of everything Terraform creates. A database's admin password, an API token, a private key: if Terraform made it, or was told it, the value is in state. Lesson 5 shows how to mark values as sensitive, which hides them from your screen, but it doesn't hide them from the state file. So:

  • Never commit state to Git. Anyone who can read the repository, today or in five years, would have every secret in it.
  • Treat the state file like a password. On a shared project, it lives somewhere encrypted and access-controlled, which is Lesson 8.

If your project is a Git repository, a .gitignore file with these lines keeps state, and Terraform's downloaded providers, out of it:

TEXT
.terraform/
*.tfstate
*.tfstate.*

In my real setup

CivicGrid's state contains the API's database connection string, password included. Terraform creates the Kubernetes Secret that holds it, and everything Terraform creates is recorded in state, secrets and all. That's why the state never lives on my laptop: it's stored encrypted in HCP Terraform, which Lesson 8 covers. And the repository's .gitignore blocks *.tfstate anyway, with a comment that says "NEVER commit these".

Renaming safely with moved

In Lesson 2, renaming hello to greeting made Terraform plan to destroy one resource and create another, because to Terraform a new name is a new resource. With a file that's harmless, but you don't want to destroy a database just to give it a better name in your code.

A moved block tells Terraform "this is the same resource, under a new name". Change main.tf to this:

HCL
resource "local_file" "greeting" {
  filename = "hello.txt"
  content  = "Hello from Terraform!"
}

moved {
  from = local_file.hello
  to   = local_file.greeting
}

The resource has its new name, and the moved block says where it came from. Run terraform plan:

OUTPUT
  # local_file.hello has moved to local_file.greeting
    resource "local_file" "greeting" {
        id                   = "cadbafd9572a72dc7769c97730552f5955e135fd"
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

Your screen may say a different number of unchanged attributes. What matters is the first line, "has moved to", and the summary: nothing added, nothing destroyed. Run terraform apply and type yes: Terraform updates the name in its state, and the file isn't touched. Run terraform state list to see the new address, local_file.greeting.

Once a move has been applied everywhere the project is used, the moved block can be deleted. Until then, leave it in: it does no harm, and it's also a note to anyone reading the code about what changed.

Three things a plan comparesterraform plan looks at three things. main.tf says what you want: a file called hello.txt. The state file, terraform.tfstate, says what Terraform made: local_file.greeting, with its id and content. The real world has the actual file on disk. Comparing the three tells Terraform what to add, change or destroy.main.tfwhat you wantterraform.tfstatewhat Terraform made, by idhello.txt on diskwhat really existsterraform plancompares all threeThe planadd, change, destroy
Without the state in the middle, Terraform knows what you want and what exists, but not which existing things are its own. That's the cloakroom ticket.

Break it on purpose

Try each one, then put it back.

Lose the state. Move the state file somewhere else, as if it had been deleted, then plan:

BASH
mv terraform.tfstate ../saved.tfstate
terraform plan

mv moves a file, here into the folder above. The plan says:

OUTPUT
  # local_file.greeting will be created

and Plan: 1 to add, 0 to change, 0 to destroy. But hello.txt is right there. Without state, Terraform has no ticket: it can't know that file is its own, so it plans to make a new one. With a file, applying would just write over it. With a cloud server, applying would create a second server, while the first one keeps running and billing you, and Terraform no longer knows it exists. Don't apply. Put the state back, and plan again:

BASH
mv ../saved.tfstate terraform.tfstate
terraform plan
OUTPUT
No changes. Your infrastructure matches the configuration.

Stop tracking something. Sometimes you want Terraform to let go of a resource without destroying it, for example because another project is taking it over. Run:

BASH
terraform state rm local_file.greeting
OUTPUT
Removed local_file.greeting
Successfully removed 1 resource instance(s).

terraform state list now shows nothing, but hello.txt is still on disk: Terraform has torn up its ticket, and that's all. Run terraform plan, and, just like losing the state, it plans to create the file again, because the code still describes one. In real life, after state rm you'd delete or move the resource's block too. Here, run terraform apply and type yes: Terraform writes the file again and starts tracking it, with nothing lost. (Bringing something that already exists under Terraform, without recreating it, is called importing, and it's a topic for after this track.)

Edit state by hand. Don't. This one's a warning rather than an exercise: hand-editing terraform.tfstate is how people make Terraform lose track of real infrastructure, or think things exist that don't. Every change you'd want has a command, like moved and state rm above.

Where you are now

You know what state is, how to read it, why it holds secrets, and what happens without it. You can rename a resource without destroying it, and stop tracking one without deleting it. So far, everything has been a file on your own computer. Lesson 4 adds a second provider, Docker, and real infrastructure that depends on other infrastructure.

Check yourself

  1. What is Terraform's state?

  2. What does terraform state list show?

  3. Why must terraform.tfstate never go in Git?

  4. You rename a resource and add a moved block. What does the plan show?

  5. You lose terraform.tfstate, but hello.txt still exists. What does terraform plan show?

  6. What does terraform state rm local_file.greeting do?

Key terms

  • State: Terraform's record of what it created, kept in terraform.tfstate by default.
  • Address: a resource's type and name together, like local_file.hello.
  • terraform state list: lists every resource in the state.
  • terraform state show: shows everything the state records about one resource.
  • moved block: tells Terraform a resource has a new name, so it isn't destroyed and recreated.
  • terraform state rm: removes a resource from the state, without touching the real thing.
  • Importing: bringing something that already exists under Terraform's management.
  • .gitignore: the file that tells Git which files to leave out, like *.tfstate.

Go deeper