GCP for DevOps — Terraform State in GCS
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