Terraform State in GCP: Remote State, GCS Backend, and Locking

Terraform tracks your infrastructure using a state file. Without it, Terraform has no record of what already exists and would try to create everything from scratch on every run. For anything beyond solo experiments, that state file belongs in Cloud Storage: shared, versioned, and locked against concurrent access. This page explains how state works, how to set up the GCS backend, how locking protects your infrastructure, and how to recover when things go wrong.

What Terraform state is in plain English

When you write Terraform configuration and run terraform apply, Terraform creates your GCP resources. It then writes a state file that records what it created: the resource type, its name in your config, its ID in GCP, and its current settings.

The next time you run Terraform, it reads that state file and compares it against your current configuration. This comparison is how Terraform knows whether to create something new, update something existing, or delete something you removed from your config.

Without state, every terraform apply would try to create everything from scratch. With stale or wrong state, Terraform makes incorrect decisions. It might try to create a resource that already exists, or fail to update one that needs changing.

Remote state means the state file lives in Cloud Storage instead of on your local machine. Everyone on your team, including your CI/CD pipeline, reads and writes the same file. State locking prevents two runs from writing to that file at the same time.

Analogy

Think of state as the inventory database in a warehouse. The warehouse holds the actual goods (your GCP resources). The database records exactly what is there and where. If the database drifts out of sync with the warehouse, workers start making bad decisions: ordering stock that already exists, looking for items that were moved, or deleting the wrong thing. Terraform state has the same job. Keep it accurate, keep it shared, and keep it backed up.

Why Terraform state matters

Terraform state is not a cache or a convenience feature. It is the mechanism Terraform uses to map your configuration to real infrastructure. Every create, update, and delete decision Terraform makes starts by reading state.

State enables several things that are otherwise impossible:

  • Tracking resource identity. GCP assigns IDs to resources when they are created. Terraform stores those IDs in state so it can reference them on future runs.
  • Detecting drift. If someone makes a manual change directly in GCP, Terraform can compare state against reality and flag the difference.
  • Planning changes safely. terraform plan compares your configuration against state to show exactly what will change before you apply anything.
  • Enabling destructive operations. When you remove a resource from your config, Terraform reads state to know what to delete. Without state, it has nothing to delete.

State also contains sensitive data: resource IDs, internal IP addresses, and sometimes credentials. It must be stored securely and never committed to git. See Secrets in CI/CD Pipelines for the broader pattern of keeping sensitive data out of source control.

How remote state with the GCS backend works

By default, Terraform stores state as a file named terraform.tfstate in your working directory. This is local state. It works for one person on one machine with throwaway infrastructure. It fails as soon as anyone else needs to run Terraform, or you need to run it in a CI pipeline.

The GCS backend tells Terraform to store state in a Cloud Storage bucket instead. You configure it with a backend block inside your terraform block:

terraform {
  backend "gcs" {
    bucket = "my-app-tfstate"
    prefix = "terraform/prod"
  }
}
Where the state file lives

With the configuration above, Terraform stores state at gs://my-app-tfstate/terraform/prod/default.tfstate. Every person and every CI run that points at the same bucket and prefix reads the same file. Nobody works from a stale private copy.

The prefix value is the folder path within the bucket. It is how you isolate state for different environments. terraform/dev and terraform/prod are completely separate state files. A run against one has no knowledge of the other. See Dev vs Staging vs Production for why environment isolation matters beyond just Terraform.

Locking is automatic with the GCS backend. No extra configuration is required. When a terraform apply begins, Terraform acquires a lock object in the bucket. Any concurrent run that finds a lock exits immediately rather than proceeding. The lock is released when the apply finishes.

How state locking works in practice

State locking prevents two Terraform runs from modifying state at the same time. Without locking, two simultaneous applies can each read the same state, make independent decisions, then both write conflicting results back. The outcome is duplicate resources and a corrupted state file that no longer accurately reflects what exists in GCP.

Analogy

Locking works like a sign-out sheet for shared tools. When someone takes the cordless drill, they sign it out. Nobody else can borrow it until it is signed back in. If someone forgets to return it and disappears, a manager can remove the record (force-unlock) — but only once they are absolutely certain the drill is not mid-use. Doing it while someone is drilling is the problem you are trying to avoid.

The sequence for a normal apply looks like this:

  1. You run terraform apply
  2. Terraform acquires a lock on the state file in GCS
  3. Terraform reads state, plans changes, and applies them
  4. Terraform writes the updated state back to GCS
  5. Terraform releases the lock

If a second apply starts while the first is running, Terraform immediately reports that the state is locked, shows you the lock ID and a timestamp, and exits without making any changes.

If a Terraform run is interrupted by a network failure, a timeout, or a cancelled CI job, the lock may not be released automatically. To release a stuck lock:

# Release a stuck state lock using the ID shown in the error message
terraform force-unlock LOCK_ID
Warning

Only run force-unlock after you have confirmed the previous Terraform process has fully exited. Force-unlocking during an active apply corrupts state. Check Cloud Logging or your CI dashboard to confirm nothing is running before you unlock.

When to use remote state

Local state is acceptable when:

  • You are learning Terraform and creating throwaway resources
  • You are running a quick experiment that will be destroyed immediately after
  • You are the only user and the infrastructure is genuinely temporary

Remote state is required when:

  • Any real project. Even a solo project you intend to keep should use remote state. Local state gets lost when machines fail, gets corrupted in editor crashes, and is invisible to anyone helping you debug.
  • Team environments. The moment two people need to run Terraform against the same infrastructure, local state breaks. Remote state with locking is the only option.
  • CI/CD pipelines. CI runners are ephemeral and cannot persist local state between runs. Any pipeline that runs terraform apply must use a remote backend. See Managing Environments in CI/CD for how this fits into a full pipeline setup.
  • Production infrastructure. There is no acceptable reason to use local state in production. Use the GCS backend, enable versioning, and restrict access to the bucket.
Starting fresh?

Set up the GCS backend before you write your first resource. Migrating state later is easy, but starting with remote state means you never have a period where state exists only on your laptop. It costs almost nothing to set up and saves real pain later.

Creating the state bucket

The state bucket must exist before you run terraform init with the GCS backend. You cannot manage the state bucket with the same Terraform configuration that uses it. If Terraform tried to create its own state bucket, it would need state to do that. Create the bucket manually or with a separate bootstrap configuration.

Versioning is required. Without it, there is no recovery path if state gets corrupted. Enable it when you create the bucket:

# Create a dedicated bucket for Terraform state
gcloud storage buckets create gs://my-app-tfstate \
  --location=europe-west2 \
  --project=my-app-platform \
  --uniform-bucket-level-access

# Enable versioning immediately
gcloud storage buckets update gs://my-app-tfstate \
  --versioning
Naming strategy

Include the project or team name in the bucket name to avoid conflicts. A name like myteam-myapp-tfstate is clear and unique. Use a dedicated bucket for state only. Do not mix state files with application data or build artifacts.

On IAM: the Terraform service account needs roles/storage.objectAdmin on the state bucket. Only Terraform service accounts need access. Restrict it with least-privilege IAM and use Managing IAM with Terraform to keep the permission assignment in code.

Local state vs remote state

If you are deciding whether your project needs remote state, this comparison makes the answer clear:

Local stateRemote state (GCS)
Where it livesYour working directoryCloud Storage bucket
CollaborationOne machine onlyShared across team and CI
LockingNoneAutomatic
CI/CD compatibleNoYes
Recovery optionsManual backup onlyGCS versioning
Risk of lossHighLow
Suitable for productionNoYes

Remote state costs almost nothing. Cloud Storage is extremely cheap for a few small JSON files. The benefit is enormous. Default to remote state for any project you care about.

Prefixes vs separate buckets for environments

You have two options for isolating state across environments:

One bucket with separate prefixes

Use terraform/dev, terraform/staging, and terraform/prod as separate prefixes within one bucket. Each prefix has its own isolated state file. This is simple, cheap, and works well for most teams:

# dev configuration
terraform {
  backend "gcs" {
    bucket = "my-app-tfstate"
    prefix = "terraform/dev"
  }
}
# prod configuration
terraform {
  backend "gcs" {
    bucket = "my-app-tfstate"
    prefix = "terraform/prod"
  }
}

Separate buckets per environment

Some organisations prefer separate buckets to enforce stricter IAM boundaries. If your production IAM policy means the dev service account has zero access to the prod bucket, that is a stronger isolation than prefix separation alone. The tradeoff is more buckets to manage.

For most teams, one bucket with separate prefixes and correctly-scoped IAM is sufficient. See Terraform Project Structure for how environment directories and backend configurations fit together.

Migrating from local state to GCS

If you started with local state and need to move to Cloud Storage, Terraform handles the migration automatically during terraform init.

Before you start:

  • Make sure the state bucket exists and versioning is enabled
  • Back up your local terraform.tfstate file to a safe location
  • Confirm no other apply is in progress

Add the backend block to your configuration, then run init:

# Add this to main.tf or versions.tf
terraform {
  backend "gcs" {
    bucket = "my-app-tfstate"
    prefix = "terraform/prod"
  }
}
# terraform init detects existing local state and offers to copy it
terraform init
# When prompted "Do you want to copy existing state to the new backend?", enter: yes

After migration, verify state is accessible from the remote backend before deleting your local copy:

# Confirm state is readable from GCS
terraform state list

# Only delete local state once you have verified the remote copy is intact
rm terraform.tfstate terraform.tfstate.backup
Add these to .gitignore

Add state files to your .gitignore if they are not already excluded. The .terraform/ directory should also be ignored as it contains provider binaries and local backend config that should never be committed.

# .gitignore
terraform.tfstate
terraform.tfstate.backup
.terraform/

Useful Terraform state commands

Use Terraform’s built-in commands to inspect and modify state. Never edit the state file directly. Direct edits almost always cause corruption. If you need to make a change, there is a command for it.

Inspecting state

# List all resources tracked in state
terraform state list

# Show the full details of one resource
terraform state show google_storage_bucket.assets

terraform state list tells you everything Terraform currently manages. terraform state show shows the stored attributes of a specific resource, which is useful for debugging or confirming what values Terraform recorded after an apply.

Moving resources

# Rename a resource in state without destroying and recreating it
terraform state mv google_storage_bucket.old_name google_storage_bucket.new_name

# Move a resource into a module
terraform state mv google_storage_bucket.assets module.storage.google_storage_bucket.assets

Use terraform state mv when you rename a resource in your configuration or refactor it into a module. Without this, Terraform interprets a rename as delete-old and create-new, causing unnecessary downtime or data loss for stateful resources.

Removing resources from state

# Stop tracking a resource without deleting it from GCP
terraform state rm google_storage_bucket.old_bucket

Use terraform state rm when you want to hand a resource to another Terraform configuration, or stop managing it with Terraform entirely. The resource stays in GCP. Terraform just stops tracking it.

Importing existing resources

# Import an existing GCP resource into state
terraform import google_storage_bucket.assets my-existing-bucket-name

Use terraform import when you have resources in GCP that were created outside Terraform and you want to bring them under Terraform management. After importing, run terraform plan to check for any drift between the imported resource and your configuration.

Common mistakes

  1. Committing state files to git. State files contain sensitive resource data: sometimes credentials, always internal IDs and IP addresses. They also cause merge conflicts. Add terraform.tfstate, terraform.tfstate.backup, and .terraform/ to .gitignore on day one and use a remote backend.

  2. Sharing one state prefix across environments. If dev and prod point at the same prefix, a terraform apply in dev can modify production resources, and a terraform destroy in dev can delete them. Separate prefixes per environment are not optional.

  3. Not enabling versioning on the state bucket. If state gets corrupted and versioning is off, there is no recovery path. Enable versioning when you create the bucket, not later when something goes wrong.

  4. Manually editing the state file. The state file format is managed by Terraform. Hand-editing causes corruption. Use terraform state mv, terraform state rm, or terraform import for any state changes you need to make.

  5. Force-unlocking too early. Confirm the previous process has fully exited before running terraform force-unlock. Unlocking during an active apply writes two conflicting state versions. Check your CI logs or Cloud Logging first.

  6. Using overly broad IAM on the state bucket. The state file contains sensitive data. Access should be restricted to the specific service accounts that run Terraform. Do not grant broad project-level storage access to the whole team.

Never do this

Committing a terraform.tfstate file to a git repository is one of the most common ways teams leak GCP credentials and resource metadata. Even a private repository is not an acceptable substitute for a remote backend. State files belong in Cloud Storage, not in version control.

Recovery and troubleshooting

Partial applies

A partial apply happens when Terraform is interrupted mid-run. Some resources were created or updated; others were not. The state file reflects only what completed before the interruption.

To recover, run terraform plan first. Terraform shows exactly what remains to be done. Run terraform apply to complete the work. Terraform is idempotent and will not duplicate resources that were already created. If a lock is stuck from the interrupted run, confirm the previous process has fully exited, then run terraform force-unlock LOCK_ID before your recovery apply.

Stale locks

If you see a lock error and you are certain no Terraform process is running, look up the lock ID in the error output or in your CI logs. Run terraform force-unlock LOCK_ID. Then run terraform plan to confirm state looks correct before applying anything.

Restoring a previous state version

If state gets corrupted or you need to roll back to an earlier version, GCS versioning gives you a full history. In the GCS console, navigate to the state file, view its version history, and restore the version you need. Or use gsutil:

# List all versions of the state file
gsutil ls -a gs://my-app-tfstate/terraform/prod/default.tfstate

# Copy a specific version back as the current state
gsutil cp "gs://my-app-tfstate/terraform/prod/default.tfstate#VERSION_ID" \
  gs://my-app-tfstate/terraform/prod/default.tfstate

After restoring, always run terraform plan before applying anything. Confirm what Terraform sees matches what you expect. See Versioning in Cloud Storage for how GCS versioning works in more detail.

Drift from manual changes

If someone makes manual changes to resources in GCP outside of Terraform, state will not reflect those changes. Run terraform plan to see the drift. Terraform will show what it would change to bring resources back to the configuration-defined state. If you want to accept the manual changes instead, use terraform import to update state to match reality, then adjust your configuration to match.

Best practices

  • Use remote state for all real projects. Local state is for throwaway experiments only. Default to the GCS backend from the start. Migrating later is extra work that interrupts your team.
  • Enable versioning on the state bucket. Non-negotiable. Versioning is your recovery option when things go wrong.
  • Keep state out of git. Add terraform.tfstate and .terraform/ to .gitignore on day one.
  • Restrict IAM access to the state bucket. Only Terraform service accounts should have write access. Use IAM managed with Terraform to enforce this consistently.
  • Isolate environments with separate prefixes. Never share a state path between dev, staging, and prod.
  • Review plans before every apply. terraform plan is not optional. In CI, require explicit approval for production applies. See Secure CI/CD Pipelines for how to structure this approval gate.
  • Use a dedicated service account for CI Terraform runs. Do not use personal credentials in CI. Prefer Workload Identity Federation over service account keys. It avoids long-lived credentials entirely.

Frequently asked questions

What is Terraform state in simple terms?

Terraform state is a file that records which real GCP resources correspond to which blocks in your configuration. When you run terraform plan or apply, Terraform reads this file to understand what already exists before deciding what to create, change, or delete.

Should I ever keep Terraform state locally?

Local state is fine for throwaway experiments or learning exercises where you are the only user and the infrastructure is temporary. For anything real, use a remote backend. The risk of losing or corrupting local state is too high.

Does the GCS backend support locking automatically?

Yes. The GCS backend uses Cloud Storage object locking to prevent concurrent terraform apply runs. Locking is automatic when you configure the backend. No extra setup is required.

What happens if my Terraform state gets corrupted?

If you have versioning enabled on the state bucket, you can recover a previous version from the GCS console or with gsutil. List object versions, identify the last known-good state, and restore it. This is why enabling versioning on the state bucket is not optional.

Can multiple environments share one state bucket?

Yes. Use separate prefixes for each environment within one bucket. Each prefix has its own isolated state file. Mixing environments in a single prefix will cause Terraform to treat all your infrastructure as one unit, which can lead to destructive applies.

Last verified: 25 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.