GCP for DevOps — Terraform State in GCS

2 minute read

Terraform State in GCS

Terraform state tracks what resources exist and their current configuration. In team environments, state must be stored remotely in a GCS backend with versioning and locking.

Why Remote State?

┌─────────────────────────────────────────────────────┐
│             Local vs Remote State                    │
├────────────────────┬────────────────────────────────┤
│    Local State     │    Remote State (GCS)          │
├────────────────────┼────────────────────────────────┤
│ terraform.tfstate  │ gs://bucket/env/terraform.state│
│ Single developer   │ Team collaboration             │
│ No locking         │ Built-in locking               │
│ No versioning      │ GCS versioning (recovery)      │
│ Lost if disk fails │ Durable (99.999999999%)        │
└────────────────────┴────────────────────────────────┘

Set Up the State Bucket

# Create the state bucket (do this ONCE, manually or with a bootstrap script)
$ gcloud storage buckets create gs://myco-terraform-state \
    --location=us-central1 \
    --default-storage-class=STANDARD \
    --uniform-bucket-level-access

# Enable versioning
$ gcloud storage buckets update gs://myco-terraform-state \
    --versioning

# Set lifecycle — keep 50 non-current versions
$ cat > lifecycle.json << 'EOF'
{
  "lifecycle": {
    "rule": [
      {
        "action": {"type": "Delete"},
        "condition": {"isLive": false, "numNewerVersions": 50}
      }
    ]
  }
}
EOF

$ gcloud storage buckets update gs://myco-terraform-state \
    --lifecycle-file=lifecycle.json

Configure the GCS Backend

# backend.tf
terraform {
  backend "gcs" {
    bucket = "myco-terraform-state"
    prefix = "environments/production"
  }
}
# Initialize with the backend
$ terraform init

Initializing the backend...
Successfully configured the backend "gcs"!

State Per Environment

Use different prefixes for each environment.

gs://myco-terraform-state/
├── environments/
│   ├── dev/default.tfstate
│   ├── staging/default.tfstate
│   └── production/default.tfstate
├── shared/
│   └── networking/default.tfstate
└── bootstrap/
    └── default.tfstate
# dev/backend.tf
terraform {
  backend "gcs" {
    bucket = "myco-terraform-state"
    prefix = "environments/dev"
  }
}

# staging/backend.tf
terraform {
  backend "gcs" {
    bucket = "myco-terraform-state"
    prefix = "environments/staging"
  }
}

State Locking

GCS backend uses object generation numbers for locking. When someone runs terraform apply, a lock file prevents concurrent modifications.

# If a lock is stuck (e.g., CI/CD crashed)
$ terraform force-unlock LOCK_ID

# Always prefer: fix the cause rather than force-unlock

Remote State Data Source

Read outputs from another Terraform state.

# In the application Terraform, read networking state
data "terraform_remote_state" "networking" {
  backend = "gcs"
  config = {
    bucket = "myco-terraform-state"
    prefix = "shared/networking"
  }
}

# Use networking outputs
resource "google_compute_instance" "app" {
  name         = "app-server"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  network_interface {
    subnetwork = data.terraform_remote_state.networking.outputs.subnet_id
  }

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-12"
    }
  }
}

State Management Commands

# List resources in state
$ terraform state list

google_compute_network.vpc
google_compute_subnetwork.subnet
google_compute_instance.vm

# Show a specific resource
$ terraform state show google_compute_instance.vm

# Move a resource (rename in state)
$ terraform state mv google_compute_instance.vm google_compute_instance.app

# Remove from state (without destroying the resource)
$ terraform state rm google_compute_instance.legacy

# Import an existing resource into state
$ terraform import google_compute_instance.existing \
    projects/my-project/zones/us-central1-a/instances/existing-vm

Migrate Local State to GCS

# Add the backend configuration to backend.tf, then:
$ terraform init -migrate-state

Initializing the backend...
Do you want to copy existing state to the new backend? (yes/no): yes
Successfully configured the backend "gcs"!

Common Mistakes

Mistake Why It’s Wrong Fix
Local state in team projects Conflicts, data loss Use GCS backend
No versioning on state bucket Can’t recover from corruption Enable versioning
Single state for everything Blast radius too large Separate state per environment/service
Force-unlock without investigation May cause state corruption Find and fix the root cause
State bucket without IAM Anyone can read secrets in state Restrict access to state bucket

Quick Reference

Command Description
terraform init -migrate-state Migrate to remote backend
terraform state list List all managed resources
terraform state show RESOURCE Show resource details
terraform state mv OLD NEW Rename in state
terraform import TYPE.NAME ID Import existing resource
terraform force-unlock LOCK_ID Force remove stuck lock

Next → Terraform Modules

Updated: