Building a reliable Google Cloud foundation with Terraform
A practical starting point for remote state, reusable modules, and a safe plan, review, and apply workflow on GCP.
terraform {
backend "gcs" {
bucket = "terraform-state"
}
}
Infrastructure as code starts showing its value when a second environment appears, a review must explain what changed, or configuration needs to be reproduced without relying on someone’s memory. This is the foundation I use to start Google Cloud projects with Terraform.
Remote state comes first — after bootstrap
Terraform maintains a state file that maps the resources it manages. Keeping that file only on a local machine creates risks around conflict, loss, and drift.
There is, however, a circular dependency: a GCS backend needs a bucket that does not exist yet. The first step is therefore to create that bucket through a small bootstrap module using local state. Once it exists, configure the remote backend and migrate state with terraform init -migrate-state.
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 7.39"
}
}
}
resource "google_storage_bucket" "terraform_state" {
name = "my-project-terraform-state"
location = "US"
uniform_bucket_level_access = true
versioning {
enabled = true
}
}
With the bucket available, the main configuration can use the GCS backend:
terraform {
backend "gcs" {
bucket = "my-project-terraform-state"
prefix = "env/prod"
}
}
Bucket versioning helps recover accidental changes, but it does not replace least-privilege access, retention, and a backup strategy appropriate for the project.
A structure that grows without hiding everything
I keep the root small and extract modules when a reusable boundary exists — not simply to make one file shorter.
infra/
├── main.tf
├── providers.tf
├── variables.tf
├── outputs.tf
└── modules/
├── network/
└── cloud-run/
A service call remains explicit:
module "api" {
source = "./modules/cloud-run"
name = "api"
image = var.api_image
region = var.region
project = var.project_id
}
Two rules prevent unnecessary coupling: every variable has a description, and modules never hardcode project or environment IDs.
Plan, review, apply
The local loop stays short and predictable:
terraform fmt -checkto standardize configuration;terraform validateto validate structure and types;terraform plan -out=tfplanto materialize the expected change;- review the plan before running
terraform apply tfplan.
In CI, the plan can be attached to the pull request. Apply remains restricted to the main branch and protected environments. For pipelines outside Google Cloud, Workload Identity Federation avoids permanent JSON keys and provides short-lived credentials.
A Terraform plan deserves the same attention as a code diff: if the change is not clear, it is not ready to be applied.
Where to go next
After this foundation, the next gains usually come from separate state per environment, scheduled drift detection, CI policy checks, and module tests. But there is no need to begin with a complex platform.
An explicit bootstrap, protected remote state, one useful module, and a reviewed plan already create a reliable foundation.