Terraform for Google Cloud: Setup, Authentication, Workflow, and Examples

Terraform is a tool for managing infrastructure by writing configuration files instead of clicking through the Cloud Console. You describe the GCP resources you want, and Terraform figures out what needs to be created, changed, or deleted to reach that state. This page covers how Terraform works with GCP, how to set it up, how authentication works, what the daily workflow looks like, and what mistakes to avoid.

What Terraform actually does

Terraform is infrastructure as code. You write files in a language called HCL that describe the GCP resources you want: storage buckets, service accounts, Cloud Run services, VPC networks. Terraform reads those files, checks what already exists in your GCP project, and shows you a plan — here is what I will create, here is what I will change, here is what I will delete.

You review the plan, approve it, and Terraform makes the API calls. The resources exist when it finishes. The next time you run it, Terraform compares the files against real GCP again and only changes what differs. This cycle of describe, plan, apply is the core of how Terraform works.

Compared with clicking through the Cloud Console, Terraform is slower for a one-off experiment and much better for anything you want to repeat, review as a team, or recreate in another environment. The Console has no memory of what you did. Terraform files do.

Analogy

Think of HCL files as blueprints and Terraform as the contractor. You hand over the blueprints, the contractor surveys what already exists, then tells you exactly what work needs doing before touching anything. Nothing gets built until you sign off on the plan.

Why teams use Terraform with Google Cloud

The practical reasons come down to a few things that matter at work:

Repeatability

You can spin up a staging environment that looks identical to production by running the same code with different variable values. That takes minutes in Terraform. Doing it manually takes days and the result will differ in ways nobody can account for.

Reviewable changes

Infrastructure changes go through pull requests. A teammate can look at a diff, see that a storage bucket’s public access setting changed, and catch it before it reaches production. This is not possible with Console changes.

Consistency across environments

Modules let you define a Cloud Run service pattern once and deploy it identically across dev, staging, and prod. See Terraform project structure for how to organise this.

Drift detection

If someone manually resizes a Cloud SQL instance or deletes a firewall rule in the Console, Terraform notices. Running terraform plan shows the difference between what your code describes and what actually exists in GCP.

Team workflows

When state is stored remotely, multiple people can work on the same infrastructure. Locking prevents two people from applying at the same time. See Terraform state management for the setup.

How Terraform works with GCP

Understanding the moving parts makes the workflow make sense.

The Google provider

Terraform talks to GCP through a plugin called the hashicorp/google provider. The provider translates your HCL resource blocks into GCP API calls. You declare which provider you need and pin a version, then Terraform downloads it during init.

Authentication

Terraform needs credentials to call GCP APIs. It uses whichever credentials it finds in the environment: Application Default Credentials locally, or a short-lived token from Workload Identity Federation in CI/CD. You do not hardcode credentials in the provider block.

State

Terraform records every resource it manages in a state file. This file maps your HCL blocks to real GCP resources by their IDs. When you run terraform plan, Terraform reads the state file and calls GCP to check what currently exists, then computes the diff. Without state, Terraform would try to create everything from scratch every time.

Resource definitions

You describe GCP resources as resource blocks in .tf files. Each block specifies the resource type, a local name, and the configuration. Resources can reference each other by name.

Dependency graph

Terraform builds a graph of dependencies before doing anything. If resource B references resource A’s ID, Terraform creates A first. You never need to manually order resource creation.

Plan

Running terraform plan shows a preview of every change Terraform intends to make. A + means create, a - means destroy, a ~ means update in place, and a -/+ means destroy and recreate. Read the plan before every apply.

Apply

Running terraform apply executes the planned changes and updates the state file to reflect what now exists in GCP. The state file is the record of truth between runs.

Drift and reconciliation

If someone changes a resource outside Terraform (in the Console, via gcloud, or through another tool), the real state diverges from the state file. Running terraform plan surfaces this drift and shows what Terraform would do to bring things back in line with your code.

How state fits in

State is like a stockroom inventory list. Before Terraform orders or discards anything, it checks the list. Remote state with locking is a shared list with a sign-out sheet: only one person can update at a time, and everyone sees the same version.

Setting up the Google provider

Every Terraform project needs a versions.tf file that declares the required providers and Terraform version. Keep this separate from main.tf so version constraints are easy to find:

terraform {
  required_version = ">= 1.6"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 6.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

The version = ”~> 6.0” constraint means: use any 6.x version but do not automatically upgrade to 7.0. This protects you from silent breaking changes when a new major version ships. Always pin to a major version.

The provider block sets defaults for your project and region. Resources inherit these values unless they override them. Using var.project_id instead of a hardcoded string means the same code works across multiple GCP projects.

Note

The google-beta provider covers GCP features still in preview. You can declare both google and google-beta in required_providers and use whichever a resource requires. Most resources use google. Only reach for google-beta when a specific resource or feature is not yet available in the stable provider.

Before running anything, make sure the APIs you need are enabled in your GCP project. Terraform cannot enable APIs for you unless you explicitly include a google_project_service resource. See enabling APIs in GCP for details.

Authentication for Terraform on GCP

Authentication is one of the most important things to get right. The approach differs between local development and CI/CD pipelines.

Local development: Application Default Credentials

For local work, use Application Default Credentials. Run this once:

gcloud auth application-default login

This opens a browser and saves a credential file to your workstation. Terraform automatically detects ADC without any configuration in the provider block. You do not need to set environment variables or reference a key file. The credential is tied to your personal Google account and the standard GCP OAuth scopes.

ADC is the right default for local development because it uses your own identity (with your own IAM permissions), it does not require creating or managing any credential files, and it works across all Google Cloud client libraries and tools, not just Terraform.

Why service account key files are risky

Danger

A service account key file is a long-lived credential that stays valid until you manually delete it, often for months or years. If it is committed to git accidentally, leaked in a build log, or stored insecurely, an attacker has persistent access to your GCP project. Never set credentials = file(“key.json”) in the provider block. Never commit a key file. See why service account keys are dangerous for the full picture.

CI/CD pipelines: Workload Identity Federation

In a CI/CD pipeline, use Workload Identity Federation instead of key files. WIF lets your pipeline authenticate to GCP by exchanging a short-lived OIDC token from your CI system (GitHub Actions, GitLab, etc.) for a short-lived GCP access token. The token expires in one hour. There is no credential file to create, store, rotate, or accidentally expose.

For GitHub Actions specifically, see GitHub Actions for GCP for the exact setup. The principle applies to any CI system that issues OIDC tokens.

Tip

A useful mental model: ADC for humans working locally, WIF for machines running in pipelines. Neither approach requires a key file. Service account key files are a last resort, not a default.

A simple Terraform example for Google Cloud

Here is a realistic example that creates a Cloud Storage bucket, a service account for an application, and grants that service account read access to the bucket:

resource "google_storage_bucket" "assets" {
  name          = "${var.project_id}-assets"
  location      = "EU"
  force_destroy = false

  uniform_bucket_level_access = true
}

resource "google_service_account" "app" {
  account_id   = "api-service"
  display_name = "API Service Account"
  project      = var.project_id
}

resource "google_storage_bucket_iam_member" "app_reader" {
  bucket = google_storage_bucket.assets.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.app.email}"
}

A few things worth noticing here:

  • The IAM binding references google_storage_bucket.assets.name and google_service_account.app.email. These are not string literals; they are references to outputs of other resource blocks.
  • Because the IAM resource references both the bucket and the service account, Terraform knows to create those first. You did not have to specify an order.
  • The bucket name uses ${var.project_id}-assets to stay unique across GCP’s global namespace. Bucket names must be globally unique.
  • Using IAM resources in Terraform means permission changes are in code, reviewable, and not invisible Console changes.

The core workflow: init, plan, apply

These three commands are the daily rhythm of working with Terraform.

terraform init

terraform init

Run this first, in every new working directory, and again after any change to required_providers or a new module source. Init downloads the providers you declared, sets up the backend for remote state, and installs any modules. Nothing in your GCP project changes. If you skip init after adding a provider, your next plan will fail with an unhelpful error.

terraform plan

terraform plan

Plan reads your configuration and the current state, calls GCP to check what actually exists, and prints a diff. Read it carefully every time. Each symbol means something specific:

  • + will be created
  • ~ will be updated in place
  • - will be destroyed
  • -/+ will be destroyed and recreated
Watch out for -/+

-/+ means destroy and recreate, not update in place. On a database, a Cloud SQL instance, or anything that holds data, that is downtime or data loss. Scan every plan for -/+ before typing yes.

Plan makes no changes to GCP. It is safe to run at any time.

terraform apply

terraform apply

Apply runs the plan and prompts you to type yes before making any changes. After confirming, Terraform calls GCP APIs to create, update, or delete resources and writes the new state.

Saving the plan in CI/CD

# In CI/CD: save the plan to a file
terraform plan -out=tfplan

# Apply exactly that saved plan, no re-planning
terraform apply tfplan

In a pipeline, always save the plan to a file and apply that file. This prevents the apply step from generating a new plan that might differ from the one that was reviewed and approved. The plan you review in the PR check is exactly the plan that gets applied.

Note

Commit .terraform.lock.hcl to version control. This file locks exact provider versions so every teammate and every CI run downloads identical providers. Add .terraform/ to .gitignore, since it contains large binary provider files that each machine downloads fresh during init.

Variables, outputs, and tfvars

Hardcoding values like project IDs and regions inside resource blocks is a problem as soon as you have more than one environment. Variables let you declare inputs once and supply different values per environment.

Declaring variables

Declare variables in variables.tf:

variable "project_id" {
  description = "GCP project ID"
  type        = string
}

variable "region" {
  description = "Default GCP region"
  type        = string
  default     = "europe-west2"
}

Reference them in resource blocks with var.project_id and var.region.

Outputs

Outputs work in reverse. You define them in outputs.tf and Terraform prints them after apply. They are also how one module passes values to another:

output "bucket_url" {
  description = "GCS bucket URL"
  value       = google_storage_bucket.assets.url
}

Supplying values with tfvars

Supply variable values in a terraform.tfvars file. Terraform loads this automatically:

project_id = "my-app-prod"
region     = "europe-west2"

For multiple environments, use named var files. Pass them explicitly with -var-file:

terraform plan -var-file=prod.tfvars
terraform plan -var-file=staging.tfvars

Where secrets do not go

Never put database passwords, API keys, or sensitive credentials in terraform.tfvars files. Those files end up in git. Use environment variables in the form TF_VAR_variable_name for sensitive values at the terminal, or pull secrets at runtime from Secret Manager. See secrets in CI/CD pipelines for pipeline-specific guidance.

Warning

If a file containing a sensitive value has already been committed to git, removing it from the latest commit is not enough. It is still in the git history. Treat the credential as compromised and rotate it immediately.

When to use Terraform on Google Cloud

Terraform is the right tool for infrastructure that needs to last, be reproducible, or be managed by more than one person.

Good candidates for Terraform
  • Repeatable environments: dev, staging, and production that should be structurally identical
  • Shared infrastructure: VPC networks, Cloud SQL instances, Artifact Registry repositories used across teams
  • IAM and permissions: roles, bindings, and service account grants that need to be reviewable and auditable
  • Production environments where changes need review before they happen, not after
  • Platform resources: enabling APIs, creating projects, setting organisation policies
  • Any resource you would want to recreate from scratch in under five minutes if the project were deleted
When Terraform is probably overkill
  • A quick experiment you will delete in an hour
  • A one-off data migration that runs once and is never repeated
  • Exploring what a new GCP product does (use the Console for that)

The dividing line is whether the resource needs to exist consistently over time and whether changes to it should be reviewable. If yes, use Terraform.

Terraform vs manual Console changes

These are not competing approaches. They serve different purposes. Here is an honest comparison:

FactorConsoleTerraform
Speed for a one-off taskFasterSlower (write HCL, plan, apply)
RepeatabilityNone; you rely on memory or notesRun the same code, get the same result
ReviewabilityNo audit trail of intentPull requests with diffs
Consistency across envsDifficult; each env drifts over timeSame code, different variable values
Risk of driftHigh; every Console change is untrackedLow; plan detects drift
Team collaborationWho made that change and why?Git history and commit messages

The Console is the right tool for investigation and exploration. Terraform is the right tool for changes that need to stick. The mistake is using the Console to make persistent changes and never bringing them back into code.

When you mix manual Console changes with Terraform-managed resources, you create drift. The next terraform apply will either revert your Console change or show an unexpected diff. Decide upfront which resources Terraform owns, and do not change those through other means. See managing environments in CI/CD for how this plays out in a full pipeline.

Importing existing resources

If you created GCP resources manually before adopting Terraform, you can bring them under Terraform management with import. Import adds an existing resource to the Terraform state file without destroying and recreating it.

# Import an existing GCS bucket into the google_storage_bucket.assets resource
terraform import google_storage_bucket.assets my-app-prod-assets

After importing, run terraform plan. The plan will likely show differences between the existing resource and your HCL. Update your configuration until the plan shows no changes. That means your code accurately reflects reality.

Terraform 1.5 introduced import blocks as a reviewable alternative to the one-shot CLI command. You declare the import in code, which makes it visible in pull requests:

import {
  to = google_storage_bucket.assets
  id = "my-app-prod-assets"
}

Import blocks run during terraform apply and are removed from state after the resource is successfully imported. They are the preferred approach for new projects since the import intent is visible alongside the resource definition.

Tip

Import resources one at a time, not all at once. After each import, run terraform plan and verify it shows no changes before moving to the next resource. Bulk importing creates a large surface area for hard-to-diagnose mismatches.

Common beginner mistakes

  1. Hardcoding credentials in the provider block. Setting credentials = file(“key.json”) in the provider and checking that file into git is one of the most common ways GCP projects get compromised. Use ADC locally. Use Workload Identity Federation in CI/CD. See service account keys explained for why this matters.

  2. Applying without reading the plan. The plan output tells you exactly what will change. A -/+ entry means Terraform will destroy and recreate a resource, which can mean data loss or downtime. Never type yes without reading every line of the plan first.

  3. Not pinning provider versions. Without a version constraint like ~> 6.0, Terraform can download a new major version on the next terraform init and silently break your configuration. Pin to a major version and upgrade deliberately.

  4. Forgetting terraform init. Any change to required_providers, a new module source, or a backend configuration change requires re-running terraform init before planning. The error messages when you skip this are often confusing.

  5. Making Console changes to Terraform-managed resources. If Terraform owns a resource, all changes should go through Terraform. A Console change creates drift. On the next apply, Terraform either overwrites your change or shows an unexpected diff that confuses everyone.

  6. Storing secrets in tfvars files. A terraform.tfvars file committed to git with database passwords or API keys is a security incident waiting to happen. Use environment variables or Secret Manager for sensitive values.

  7. Using local state for team environments. The default state backend is a local file on your machine. If two people apply against the same infrastructure, they corrupt each other’s state. Configure the GCS remote backend from the start of any project that more than one person touches.

  8. Ignoring the lock file. Not committing .terraform.lock.hcl to git means your team can use different provider versions and get different behaviour. Commit the lock file so everyone runs the same providers.

Related concepts worth learning next

This page covers the core of how Terraform works with GCP. These topics go deeper on the parts that matter most for real projects:

Frequently asked questions

How does Terraform authenticate to Google Cloud?

For local development, run `gcloud auth application-default login`. Terraform automatically picks up Application Default Credentials — no configuration needed in the provider block. In CI/CD, use Workload Identity Federation instead of service account key files. WIF issues short-lived tokens that expire in one hour. Key files are long-lived and can be leaked.

What is the difference between the google and google-beta providers?

The google provider covers Generally Available GCP features. The google-beta provider includes features still in preview. You can use both in the same Terraform configuration — declare both in required_providers and use google-beta as the provider on resources that need preview capabilities.

Does Terraform replace the Google Cloud Console?

No. The Console is still useful for exploration, debugging, and one-off tasks. Terraform manages the infrastructure you want to keep repeatable, reviewable, and consistent. Think of them as complementary: the Console for looking, Terraform for changing.

What does -/+ mean in a Terraform plan output?

It means the resource will be destroyed and recreated rather than updated in place. This is different from ~ (update in place). A -/+ on a database or stateful resource means downtime or data loss. Always read the plan carefully before applying. Look specifically for -/+ on anything that holds data.

Can Terraform manage resources that already exist?

Yes, using terraform import. Import brings an existing GCP resource under Terraform state without recreating it. After importing, run terraform plan to see the diff between the existing resource and your HCL. Update your config until plan shows no changes.

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