Laptop to Production

Terraform state

How Terraform remembers — and keeping that memory safe.

Time
45 minutes
Level
Intermediate

Part 1: The problem

QuickCart, the platform team grows

For six months, one engineer — Maya — ran all of QuickCart’s Terraform from her laptop. The code was in git, so when Leo joined, he had all of it.

He ran terraform apply. Terraform planned to create a second load balancer and a second database. Nothing in the code had changed, and nobody had asked for duplicates.

Two weeks later, after a quick fix, they both ran apply within the same minute. Afterwards, Terraform no longer knew about a database Maya had just built. It was still running and still costing money, but nobody’s code was managing it any more.

Both problems have the same cause: a file called terraform.tfstate that lived only on Maya’s laptop.

This lesson is about what that file is, and the four things a team does to keep it safe.

Part 2: The idea

After an apply, Terraform writes terraform.tfstate: a record of every resource it created, linked to that resource’s real ID in AWS.

Your code says an ECR repository named quickcart-orders. Your AWS account may have thousands of things in it. State is how Terraform knows which one is yours, and whether it already exists.

Key point: State is Terraform’s memory. Lose it, and Terraform forgets everything it built.

Every plan compares three things

Your codeStateAWSWhat the plan does
has itdoesn’tdoesn’tCreate it
has ithas ithas it, matchingNothing
has ithas itsomeone changed itChange it back to match the code
deleted ithas ithas itDestroy it
has itlosthas itCreate a duplicate — Leo’s problem

That last row is why state matters so much. Without it, Terraform can’t tell that something already exists.

Why two people at once breaks it

What’s inside it

Every attribute of every resource: IDs, names, addresses, settings AWS filled in — and any secret that passed through Terraform, in plain text. So state has to be treated like a password file, because in part it is one.

The fix, in four parts

PartWhat it doesWhich problem it solves
Remote — kept in S3Everyone reads and writes the same copyLeo’s duplicates, and a lost laptop
LockingOnly one apply at a time; others are refusedTwo applies erasing each other’s record
VersioningEvery past version of the file is keptA damaged state can be rolled back
Encryption and no public accessUnreadable to anyone who shouldn’t see itThe secrets inside the state

The backend block, every setting explained

You tell Terraform where state lives with a backend block, usually in a file called backend.tf:

backend.tf
terraform {
  backend "s3" {
    bucket       = "quickcart-tfstate-111122223333"
    key          = "dev/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}
SettingWhat it doesIf you leave it outWhen you’d change it
backend "s3"Store state in S3 instead of on your laptopState stays in a local file, with all the problems aboveNever, once a team shares the code
bucketWhich bucket holds the file. Bucket names are unique across all of AWS, so adding your account number keeps yours uniqueRequired — terraform init stops with an errorNever after the first init; moving state is a deliberate migration
keyThe file’s path inside the bucket. Each environment gets its own, like dev/… and stg/…Required — terraform init stops with an errorOnce per environment
regionWhich region the bucket is in — not where your servers areTerraform uses your default region and may not find the bucketOnly if the bucket is in another region
encryptEncrypt the state file when it’s written to S3S3 still encrypts new objects by default today, but saying so makes it explicit and survives a bucket setting being changedNever
use_lockfileWhile a command runs, keep a lock file next to the state in S3. Needs Terraform 1.10 or laterNo locking: two people can apply at onceNever

The chicken and the egg

The bucket that holds state can’t be created by the Terraform code that needs it — where would that state live? So you create it once, by hand, and never manage it with Terraform. It’s the one place where clicking in the console is correct.

Part 3: See it

First, the three-way comparison. Pick what happens, and see what the next plan does about it.

What happens

Your code

what you asked for
VPC
Load balancer
Orders API

State file

what Terraform remembers
VPC
Load balancer
Orders API

AWS

what really exists
VPC
Load balancer
Orders API
$ terraform plan

No changes. Your infrastructure matches the configuration.
What the plan doesCode, state and AWS all agree, so Terraform does nothing. That is why running apply twice is safe.

Now the race. Maya and Leo press apply at the same moment. Play it with locking off, then again with locking on, and watch the state file.

State locking
Maya
About to run terraform apply
Hasn't read the state yet
State file in S3v3
No lock in use
VPC
Load balancer
What really exists in AWS
VPC
Load balancer
Leo
About to run terraform apply
Hasn't read the state yet
Step 0 of 7Maya and Leo both press apply at the same moment. The state file is at version 3: a VPC and a load balancer.

What to notice: with locking off, both applies finished successfully — no error, no warning. That’s what makes it dangerous. Nothing tells you anything went wrong until a later plan tries to create something that already exists.

Part 4: Do it

Pennies — one S3 bucket holding a few small files, and an empty ECR repository, which is free.

You’ll build a tiny Terraform project, move its state into S3, watch the lock refuse a second command, and bring back an old version of the state. About 30 minutes.

You need Terraform 1.10 or later and a working AWS login in your terminal. Set up your computer covers both. Check with:

Terminal
terraform version
PartWhat it means
terraform versionprint the version of Terraform installed
You should see
Terraform v1.16.4
on linux_amd64

Any version from 1.10 upwards works. Yours may be newer, and the second line names your system, such as darwin_arm64 on a Mac.

  1. Make a small project with local state

    If you already have a Terraform folder from Your first resource, you can use it and skip to step 2. Otherwise, make a new folder and move into it:

    Terminal
    mkdir quickcart-tf
    PartWhat it means
    mkdirmake a directory
    quickcart-tfits name, inside the folder you’re in now
    Terminal
    cd quickcart-tf
    PartWhat it means
    cdchange directory — every later command runs in here
    quickcart-tfthe folder you just made

    In that folder, create main.tf:

    main.tf
    terraform {
      required_version = ">= 1.10.0"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 6.0"
        }
      }
    }
    
    provider "aws" {
      region = "us-east-1"
    }
    
    resource "aws_ecr_repository" "orders" {
      name                 = "quickcart-orders"
      image_tag_mutability = "IMMUTABLE"
      force_delete         = true
    
      tags = {
        Project = "quickcart"
      }
    }
    SettingWhat it doesIf you leave it outWhen you’d change it
    required_versionRefuse to run on a Terraform older than 1.10, which has no use_lockfileAn old Terraform would fail later with a confusing error about the backendWhen you start relying on a newer feature
    required_providers → awsWhich plugin talks to AWS: hashicorp/aws, any 6.x versionTerraform guesses the source and takes the newest version, which may one day bring breaking changesWhen you deliberately upgrade to a new major version
    provider "aws" → regionWhere the resources are createdTerraform uses the region from your AWS login settings, which may not be the one you expectTo build somewhere else
    aws_ecr_repository "orders"An ECR repository — a private store for container images. orders is Terraform’s name for it; quickcart-orders is AWS’s——
    nameThe repository’s name in AWSRequiredNever after creation — changing it replaces the repository
    image_tag_mutabilityIMMUTABLE means a tag like v1 can never be pointed at a different imageDefaults to MUTABLE: someone can push a different image under the same tagRarely. Storing images: ECR explains why
    force_deleteLet terraform destroy delete the repository even if it holds imagesDestroy fails while any image is insideSet to false for production repositories
    tagsLabels, so you can find and cost everything belonging to QuickCartNo labels; harder to find laterAdd owner, environment and cost-centre tags in real projects

    Set up the folder and build the repository:

    Terminal
    terraform init
    PartWhat it means
    terraform initdownload the AWS provider and prepare this folder
    You should see
    Terraform has been successfully initialized!
    Terminal
    terraform apply
    PartWhat it means
    terraform applywork out a plan, show it, and carry it out once you type yes

    It shows the plan and asks you to confirm. Type yes.

    You should see
    Plan: 1 to add, 0 to change, 0 to destroy.
    
    Do you want to perform these actions?
      Terraform will perform the actions described above.
      Only 'yes' will be accepted to approve.
    
      Enter a value: yes
    
    aws_ecr_repository.orders: Creating...
    aws_ecr_repository.orders: Creation complete after 1s [id=quickcart-orders]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

    You now have a file called terraform.tfstate in the folder. That’s Terraform’s memory, sitting on your laptop — Maya’s situation.

  2. Create the state bucket in the console

    First, find your account number: open the account menu at the top right of the console and copy the 12-digit Account ID. In this course it’s shown as 111122223333; use yours instead.

    In the AWS console: S3General purpose bucketsCreate bucket

    FieldEnter
    Bucket typeGeneral purpose
    Bucket namequickcart-tfstate- followed by your account ID, like quickcart-tfstate-111122223333
    Object OwnershipACLs disabled — the default
    Block all public accessOn — the default. Leave it
    Bucket VersioningEnable
    Default encryptionSSE-S3 — the default. Leave it

    Choose Create bucket.

    You should see

    The bucket in the list. Open it, go to Properties, and check that Bucket Versioning says Enabled.

    Versioning is the setting people skip, and regret. It’s the only way back if the state is ever damaged.

  3. Add the backend block

    In the same folder as main.tf, create backend.tf. Put your own bucket name in place of the highlighted account number:

    backend.tf
    terraform {
      backend "s3" {
        bucket       = "quickcart-tfstate-111122223333"
        key          = "dev/terraform.tfstate"
        region       = "us-east-1"
        encrypt      = true
        use_lockfile = true
      }
    }

    Every setting is explained in the idea, above.

  4. Move the state across

    Terminal
    terraform init -migrate-state
    PartWhat it means
    terraform initset the folder up again, now with the S3 backend
    -migrate-statecopy the state you already have into the new backend

    It asks before copying. Type yes.

    It asks, then confirms
    Initializing the backend...
    Do you want to copy existing state to the new backend?
      Pre-existing state was found while migrating the previous "local" backend to the
      newly configured "s3" backend. No existing state was found in the newly
      configured "s3" backend. Do you want to copy this state to the new "s3"
      backend? Enter "yes" to copy and "no" to start with an empty state.
    
      Enter a value: yes
    
    
    Successfully configured the backend "s3"! Terraform will automatically
    use this backend unless the backend configuration changes.
  5. Check it worked

    In the AWS console: S3General purpose bucketsyour bucketdev/

    You should see

    A file called terraform.tfstate.

    Now ask Terraform to compare everything again, reading state from S3 this time:

    Terminal
    terraform plan
    PartWhat it means
    terraform plancompare code, state and AWS, and show what would change — without changing anything
    You should see
    No changes. Your infrastructure matches the configuration.

    Same result as before. Only the state’s location changed.

  6. Watch the lock refuse a second command

    To see the lock you need a command that stops half-way and holds it. apply does that while it waits for you to type yes — but only when there’s a change to approve. So first make a small change: add a second tag, Team, at the end of main.tf. The whole file now reads:

    main.tf
    terraform {
      required_version = ">= 1.10.0"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 6.0"
        }
      }
    }
    
    provider "aws" {
      region = "us-east-1"
    }
    
    resource "aws_ecr_repository" "orders" {
      name                 = "quickcart-orders"
      image_tag_mutability = "IMMUTABLE"
      force_delete         = true
    
      tags = {
        Project = "quickcart"
        Team    = "platform"
      }
    }

    Now open two terminal windows, both in the quickcart-tf folder.

    Window 1 — start an apply. When it asks Enter a value:, don’t answer yet:

    Terminal
    terraform apply
    PartWhat it means
    terraform applytakes the lock, plans the tag change, then waits for you — still holding the lock

    Window 2 — while window 1 is waiting:

    Terminal
    terraform plan
    PartWhat it means
    terraform planalso needs the lock, even though it changes nothing, so it reads a state nobody is halfway through writing
    Window 2 is refused
    Error: Error acquiring the state lock
    
    Lock Info:
      ID:        6f1c2d9e-3b7a-4c55-9e2f-8a0d4b1c7e21
      Path:      quickcart-tfstate-111122223333/dev/terraform.tfstate
      Operation: OperationTypeApply
      Who:       maya@maya-laptop
      Version:   1.16.4
      Created:   2026-09-25 10:22:11.482913 +0000 UTC
      Info:
    
    Terraform acquires a state lock to protect the state from being written
    by multiple users at the same time. Please resolve the issue above and try
    again. For most commands, you can disable locking with the "-lock=false"
    flag, but this is not recommended.

    Your ID, name and times will differ, and there’s an extra Error message line from S3 explaining the refusal. That refusal is the whole point. It tells you who holds the lock and since when.

    Now go back to window 1 and type yes. The tag is added and the lock is released. Run terraform plan in window 2 again, and this time it works.

  7. Bring back an old version

    Because of the tag change, the bucket now holds more than one version of the state.

    In the AWS console: S3your bucketdev/terraform.tfstateVersions

    You should see

    Two or more versions, each with a time. The newest is at the top, marked Current version.

    Tick the older version, choose Download, and move the file into your project folder, renamed to recovered.json. Then replace the state with it:

    Terminal
    terraform state push recovered.json
    PartWhat it means
    terraform state pushreplace the state in S3 with a file you give it
    recovered.jsonthe older version you downloaded

    Straight afterwards, see how the recovered state compares with reality:

    Terminal
    terraform plan
    PartWhat it means
    terraform planrefresh the state from AWS, then compare it with the code
    You should see
    No changes. Your infrastructure matches the configuration.

    The old version didn’t know about the Team tag, but plan reads the real repository from AWS first and finds the tag already there. The recovery was safe because the old state still pointed at the same real resource.

  8. Clean-up step: Clean up

    Delete the repository Terraform built:

    Terminal
    terraform destroy
    PartWhat it means
    terraform destroyplan the removal of everything in the state, and carry it out once you type yes
    You should see
    Destroy complete! Resources: 1 destroyed.

    Keep the bucket if you’re carrying on with the course. It costs pennies, and later lessons and the pipeline project use it.

    If you’re not carrying on, empty it and delete it: S3 → your bucket → Empty, type permanently delete, then Delete the bucket itself. Emptying a versioned bucket removes every old version too.

If a lock ever gets stuck

A laptop goes to sleep mid-apply, or a terminal is closed. The lock is never released, and everyone is refused. First, check that nobody is really applying — the error tells you who holds the lock. Then remove it using the ID from the error:

Terminal
terraform force-unlock LOCK_ID
PartWhat it means
terraform force-unlockremove a lock by hand
LOCK_IDreplace this with the ID line from the lock error
You should see
Do you really want to force-unlock?
  Terraform will remove the lock on the remote state.
  This will allow local Terraform commands to modify this state, even though it
  may still be in use. Only 'yes' will be accepted to confirm.

  Enter a value: yes

Terraform state has been successfully unlocked!

Part 5: Check yourself

Leo clones the repository and runs apply. Terraform plans to create duplicates of everything. Why?

What is wrong with committing terraform.tfstate to git?

Two people apply at the same time without locking. What happens?

Your dev and stg environments keep state in the same bucket. What must be different between them?

Someone's apply crashed hours ago and now nobody can apply. What do you do first?

Part 6: Cheat sheet

On one card
Command or settingWhat it does
terraform state listList everything Terraform is tracking
terraform state show aws_ecr_repository.ordersEvery detail of one resource
terraform init -migrate-stateMove state into a new backend
terraform state pullPrint the current state, to save a copy
terraform state push FILEReplace the state. Refused if the file is older, unless you add -force
terraform force-unlock LOCK_IDClear a stuck lock, after checking nobody holds it
keyOne per environment: dev/…, stg/…
use_lockfile = trueOne command at a time. Terraform 1.10 or later
Bucket versioningTurn it on before you need it
.gitignore*.tfstate and *.tfstate.* — never commit state

Finished the build and the quiz? Mark it done to fill in its stop on the route map.