Cloud/ Google Cloud / DevOps & Operations / Terraform on GCP: State, Modules, and the Patterns That Actually Scale

GCP Google Cloud Platform Guide 2 of 5 54 guides ยท updated 2026

Guides to BigQuery, Vertex AI, GKE, Dataflow, and the rest of Google's data- and AI-first cloud โ€” written for engineers shipping real workloads.

Terraform on GCP: State, Modules, and the Patterns That Actually Scale

Terraform has become the practical default for provisioning GCP infrastructure, including among teams running entirely on Google Cloud who could use the native Deployment Manager instead โ€” a signal worth taking seriously about which tool actually holds up as infrastructure complexity grows. Writing individual resource blocks is the easy part and well covered by the official documentation; the part that actually determines whether a Terraform setup stays maintainable at scale or becomes a source of dread is state management, module structure, and how you handle multiple environments โ€” decisions that are hard to retrofit cleanly once made poorly and lived with for a year.

This is where real, hard-won practice diverges most from a getting-started tutorial, so thatโ€™s the focus here.


Remote State: The Decision That Matters Most Early

Terraformโ€™s local state file (terraform.tfstate) works fine for a solo experiment and becomes a genuine liability the moment more than one person touches the same infrastructure โ€” two people applying changes against their own local state files independently is a recipe for drift and conflicting changes neither person can see coming.

terraform {
backend "gcs" {
bucket = "my-company-terraform-state"
prefix = "prod/networking"
}
}
Terminal window
# Create the state bucket with versioning enabled โ€” non-negotiable
gsutil mb -l us-central1 gs://my-company-terraform-state
gsutil versioning set on gs://my-company-terraform-state

Storing state in a Cloud Storage bucket, with versioning enabled, gives you both team-shared state (everyone applies against the same source of truth) and a recovery path if state ever gets corrupted or an apply goes badly wrong โ€” versioning means you can restore a previous state file version, which has saved real infrastructure teams from what would otherwise be a genuinely bad day. Skipping remote state, or skipping bucket versioning specifically, is one of the few Terraform decisions thatโ€™s actively dangerous rather than just suboptimal.


State Locking: Preventing Concurrent Apply Disasters

Engineer A runs `terraform apply`
โ”‚
โ–ผ
GCS backend acquires a lock (via Cloud Storage's
native object locking mechanism)
โ”‚
Engineer B runs `terraform apply` at the same time
โ”‚
โ–ผ
Lock held โ€” Engineer B's apply blocks
until Engineer A's completes

The GCS backend handles state locking automatically โ€” this isnโ€™t optional configuration, itโ€™s built into using the GCS backend, and itโ€™s what prevents the genuinely bad scenario of two concurrent applies racing against the same state and corrupting it. This is one of several reasons the GCS backend (over local state, even local state shared via a network drive) is the correct default for any team, not a nice-to-have for larger organizations specifically.


Module Structure: Reuse Without Chaos

modules/
โ”œโ”€โ”€ gke-cluster/
โ”‚ โ”œโ”€โ”€ main.tf
โ”‚ โ”œโ”€โ”€ variables.tf
โ”‚ โ””โ”€โ”€ outputs.tf
โ”œโ”€โ”€ vpc-network/
โ”‚ โ”œโ”€โ”€ main.tf
โ”‚ โ”œโ”€โ”€ variables.tf
โ”‚ โ””โ”€โ”€ outputs.tf
โ””โ”€โ”€ cloud-sql-instance/
โ”œโ”€โ”€ main.tf
โ”œโ”€โ”€ variables.tf
โ””โ”€โ”€ outputs.tf
environments/
โ”œโ”€โ”€ dev/
โ”‚ โ””โ”€โ”€ main.tf (calls modules with dev-specific variables)
โ”œโ”€โ”€ staging/
โ”‚ โ””โ”€โ”€ main.tf (calls modules with staging-specific variables)
โ””โ”€โ”€ prod/
โ””โ”€โ”€ main.tf (calls modules with prod-specific variables)
environments/prod/main.tf
module "networking" {
source = "../../modules/vpc-network"
network_name = "prod-vpc"
subnet_cidr = "10.10.0.0/16"
}
module "gke" {
source = "../../modules/gke-cluster"
cluster_name = "prod-cluster"
network = module.networking.network_self_link
node_count = 5
}

This structure โ€” reusable modules parameterized by variables, called from environment-specific configurations โ€” is what actually lets a team avoid writing the same VPC or GKE cluster configuration three times with copy-pasted, subtly-diverging HCL for dev, staging, and prod. The module encapsulates the โ€œhow,โ€ the environment configuration supplies the โ€œwhat specifically for this environment,โ€ and a change to the moduleโ€™s internal logic (say, a security hardening update to the GKE module) propagates to every environment consistently once each environmentโ€™s main.tf is re-applied.

Module outputs, like module.networking.network_self_link in the example above, are what let modules compose cleanly โ€” the GKE module doesnโ€™t need to know how the VPC module constructed its network internally, only that it can reference the networkโ€™s identifier through a stable, documented output. This composability is what makes larger Terraform codebases genuinely maintainable rather than an increasingly tangled web of resources referencing each otherโ€™s internal implementation details directly.


Workspaces vs Separate Directories: The Multi-Environment Decision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Approach โ”‚ Trade-off โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Terraform Workspaces โ”‚ One codebase, multiple named state โ”‚
โ”‚ โ”‚ instances (`terraform workspace select โ”‚
โ”‚ โ”‚ prod`) โ€” less code duplication, but โ”‚
โ”‚ โ”‚ genuinely easy to apply against the wrong โ”‚
โ”‚ โ”‚ workspace by mistake โ”‚
โ”‚ Separate directories โ”‚ Explicit directory per environment (as โ”‚
โ”‚ (shown above) โ”‚ in the module example) โ€” more files, but โ”‚
โ”‚ โ”‚ you can never accidentally run `apply` โ”‚
โ”‚ โ”‚ against the wrong environment, because the โ”‚
โ”‚ โ”‚ environment is the directory you're in โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Workspaces are appealing for their reduced duplication, but the โ€œwhich workspace am I currently inโ€ ambiguity is a genuinely common source of real incidents โ€” an engineer runs terraform apply believing theyโ€™re in the dev workspace, when a previous workspace select command actually left them in prod. Separate directories per environment trade some file duplication for an explicit, hard-to-mistake safety property: the environment youโ€™re operating on is literally the directory youโ€™re standing in, not an easily-forgotten prior commandโ€™s side effect. For anything genuinely production-critical, most practitioners consider this trade-off worth it.


The GCP Provider: Authentication in CI/CD

provider "google" {
project = var.project_id
region = "us-central1"
}

For CI/CD pipelines applying Terraform automatically, the authentication method matters for security, not just convenience:

Terminal window
# Workload Identity Federation โ€” no long-lived key needed
gcloud iam workload-identity-pools create github-pool \
--location=global \
--display-name="GitHub Actions pool"

Using Workload Identity Federation to let a CI/CD pipeline (GitHub Actions, GitLab CI) authenticate to GCP without a long-lived service account JSON key stored as a pipeline secret is the current best practice โ€” a leaked long-lived key is a standing risk for as long as it remains valid, while federated authentication issues short-lived tokens per pipeline run with no persistent credential to leak in the first place. This is a genuinely important security upgrade that a surprising number of Terraform-on-GCP setups still havenโ€™t made, often because the long-lived-key approach was what an early tutorial showed and nobodyโ€™s revisited it since.


A Safe CI/CD Apply Pipeline, End to End

Putting the pieces together โ€” remote state, plan review, and federated authentication โ€” into an actual pipeline flow clarifies how they combine in practice.

No

Yes, merged

Engineer opens PR with .tf changes

CI pipeline authenticates via

Workload Identity Federation

terraform init โ€” connects to

GCS remote state backend

terraform plan โ€” computes diff

Plan output posted as PR comment

for human review

Reviewer approves PR?

Changes requested โ€” cycle repeats

CI pipeline runs terraform apply

on merge to main

State lock acquired during apply

Resources created/updated/deleted

State lock released,

updated state written to GCS

The plan-as-PR-comment step is worth calling out specifically โ€” it turns an infrastructure change into something reviewable the same way application code is, with the actual computed diff (not just the source code diff) visible to whoeverโ€™s approving it. A reviewer can see โ€œthis will destroy and recreate a Cloud SQL instanceโ€ directly in the plan output before approving, which is a meaningfully stronger safety net than reviewing only the HCL source and trying to mentally simulate what Terraform will actually do with it.


Real-World Use Case: Onboarding a New Environment Safely

A team adding a new environment (say, a dedicated environment for a specific enterprise customer with data residency requirements) benefits directly from a well-structured module setup โ€” creating the new environment is largely copying an existing environmentโ€™s main.tf, adjusting variables (region, network CIDR, sizing), and running terraform plan to review exactly what will be created before applying. Because the underlying modules are already tested and used in production for other environments, this new environment inherits the same security hardening, tagging conventions, and architecture patterns automatically, rather than requiring a from-scratch design and review process each time a new environment is needed.


Best Practices


Common Mistakes

Using local state, or state shared via a network drive, for anything beyond a solo experiment. This is consistently the root cause of the worst Terraform incidents โ€” corrupted or conflicting state with no clean recovery path.

Applying changes without reviewing the plan output carefully. A plan showing an unexpected resource replacement (not just an update) is a critical signal worth stopping and investigating before applying โ€” Terraform replacing a resource often means downtime or data loss for that resource, not a routine change.

Copy-pasting configuration across environments instead of using modules. This guarantees the environments drift apart silently over time as each copy gets modified independently, defeating the entire โ€œconsistent infrastructure across environmentsโ€ value proposition Terraform is meant to provide.


Frequently Asked Questions

Is Terraform the same across every cloud provider, or does GCP need different knowledge? The core Terraform workflow (plan, apply, state, modules) is identical across providers โ€” what differs is the provider-specific resource types and their arguments, which for GCP means learning the google providerโ€™s resource schema specifically.

Can I import existing, manually-created GCP resources into Terraform management? Yes, via terraform import, which brings an existing resource under Terraformโ€™s state tracking without recreating it โ€” a common and necessary step when adopting Terraform for infrastructure that already exists.

How often should Terraform modules be updated or reviewed? Treat them like any shared library code โ€” version them explicitly (using Git tags or a module registry), and review changes with the understanding that a module update potentially affects every environment consuming it, not just the one being actively worked on.

Does using Terraform mean giving up the GCP Console for making changes? Not entirely, but manual console changes to Terraform-managed resources cause state drift โ€” the console remains useful for inspection and read-only investigation, but changes to Terraform-managed infrastructure should go through Terraform to keep state accurate.

Whatโ€™s the risk of running terraform apply locally instead of through CI/CD? Local applies bypass the review step a PR-based pipeline enforces, and depending on local authentication setup, may use a long-lived credential rather than short-lived federated tokens โ€” reserving local applies for genuine emergencies, with routine changes going through the reviewed pipeline, is the safer default.


Summary

The Terraform features that get the most tutorial attention โ€” resource syntax, basic provider configuration โ€” are genuinely the easy part. What actually determines whether a GCP Terraform setup remains trustworthy as it grows is remote state with versioning from day one, a module structure that avoids environment drift, a deliberate (and safety-conscious) choice between workspaces and separate directories, and modern, keyless CI/CD authentication. Getting these foundational decisions right early is meaningfully cheaper than retrofitting them onto a year of accumulated infrastructure and institutional habits built around a riskier initial setup.