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.
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.
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.
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 loginThis 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
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.
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.nameandgoogle_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}-assetsto 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 initRun 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 planPlan 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
-/+ 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 applyApply 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 tfplanIn 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.
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.tfvarsWhere 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.
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.
- 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
- 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:
| Factor | Console | Terraform |
|---|---|---|
| Speed for a one-off task | Faster | Slower (write HCL, plan, apply) |
| Repeatability | None; you rely on memory or notes | Run the same code, get the same result |
| Reviewability | No audit trail of intent | Pull requests with diffs |
| Consistency across envs | Difficult; each env drifts over time | Same code, different variable values |
| Risk of drift | High; every Console change is untracked | Low; plan detects drift |
| Team collaboration | Who 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-assetsAfter 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.
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
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.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 typeyeswithout reading every line of the plan first.Not pinning provider versions. Without a version constraint like
~> 6.0, Terraform can download a new major version on the nextterraform initand silently break your configuration. Pin to a major version and upgrade deliberately.Forgetting
terraform init. Any change torequired_providers, a new module source, or a backend configuration change requires re-runningterraform initbefore planning. The error messages when you skip this are often confusing.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.
Storing secrets in tfvars files. A
terraform.tfvarsfile 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.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.
Ignoring the lock file. Not committing
.terraform.lock.hclto 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:
- Terraform Project Structure: how to organise environments, modules, and files as your codebase grows
- Terraform State Management: GCS remote backend, locking, and recovering from corrupted state
- Managing IAM with Terraform: defining roles, bindings, and service account grants in code
- Workload Identity Federation: the right way to authenticate pipelines to GCP
- Secret Manager: storing and accessing secrets safely from Terraform and pipelines
- GitHub Actions for GCP: setting up WIF for GitHub Actions workflows that run Terraform
- Secure CI/CD Pipelines: hardening your pipeline against credential exposure
- Terraform Permission Errors: diagnosing IAM and API errors during plan and apply
Summary
- Terraform describes desired GCP infrastructure in HCL files and makes API calls to reach that state
- Authenticate locally with
gcloud auth application-default login; Terraform picks up ADC automatically - Use Workload Identity Federation in CI/CD; never use service account key files
- Core commands:
terraform init(setup),terraform plan(preview),terraform apply(execute) - In CI/CD, save the plan with
-out=tfplanand apply exactly that file - Pin provider versions with
~> 6.0; commit.terraform.lock.hcl; gitignore.terraform/ - Resources reference each other with dot notation; Terraform resolves dependency order automatically
- Use remote state in Cloud Storage for any project more than one person touches
- Never commit secrets to tfvars files; never manually edit resources Terraform manages
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.