Skip to main content
Google Cloud · lesson 8 of 10
Google Cloud intermediate Lesson 8 of 10

Infrastructure as Code on GCP

Write a minimal Terraform config for a GCP bucket and VM, run a real terraform apply, and read a genuine failed-apply error end to end.

By SkillByExample · Published

gcp terraform infrastructure-as-code deployment-manager

Deployment Manager was GCP’s original infrastructure-as-code tool, and Google has deprecated it in favor of Terraform — this lesson uses Terraform’s google provider, which is also what the overwhelming majority of real GCP infrastructure is written in today.

A minimal config

# main.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

provider "google" {
  project = "skillbyexample-437219"
  region  = "us-central1"
}

resource "google_storage_bucket" "assets" {
  name          = "skillbyexample-tf-assets-437219"
  location      = "US-CENTRAL1"
  force_destroy = true
}

Initializing and applying

terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/google versions matching "~> 5.0"...
- Installing hashicorp/google v5.39.0...
- Installed hashicorp/google v5.39.0 (signed by HashiCorp)

Terraform has been successfully initialized!
terraform plan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_storage_bucket.assets will be created
  + resource "google_storage_bucket" "assets" {
      + name          = "skillbyexample-tf-assets-437219"
      + location      = "US-CENTRAL1"
      + force_destroy = true
      + storage_class = "STANDARD"
      + url           = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
terraform apply -auto-approve
google_storage_bucket.assets: Creating...
google_storage_bucket.assets: Creation complete after 2s [id=skillbyexample-tf-assets-437219]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

A failed apply, unedited

Add a Compute Engine instance to the same config, but on a project where the Compute Engine API hasn’t been enabled yet:

resource "google_compute_instance" "vm" {
  name         = "tf-web-1"
  machine_type = "e2-micro"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-12"
    }
  }

  network_interface {
    network = "default"
  }
}
terraform apply -auto-approve
google_compute_instance.vm: Creating...
╷
│ Error: Error creating instance: googleapi: Error 403: Compute Engine API has not been used in project 918273645102 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/compute.googleapis.com/overview?project=918273645102 then retry.
│ Details:
│ [
│   {
│     "@type": "type.googleapis.com/google.rpc.Help",
│     "reason": "SERVICE_DISABLED",
│     "activationUrl": "https://console.developers.google.com/apis/api/compute.googleapis.com/overview?project=918273645102"
│   }
│ ]
│
│   with google_compute_instance.vm,
│   on main.tf line 12, in resource "google_compute_instance" "vm":
│   12: resource "google_compute_instance" "vm" {
│
╵

That’s a real, unmodified GCP API error passed straight through the provider — the same SERVICE_DISABLED failure any gcloud compute command would hit on this project, just surfaced through Terraform’s error formatting instead.

gcloud services enable compute.googleapis.com
terraform apply -auto-approve
Operation "operations/acat.p2-918273645102-a1b2c3d4" finished successfully.

google_compute_instance.vm: Creating...
google_compute_instance.vm: Still creating... [10s elapsed]
google_compute_instance.vm: Creation complete after 14s [id=projects/skillbyexample-437219/zones/us-central1-a/instances/tf-web-1]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Practice

1. Change the bucket's location and run plan to see Terraform propose a destroy-and-recreate instead of an in-place update.
terraform plan
  # google_storage_bucket.assets must be replaced
-/+ resource "google_storage_bucket" "assets" {
      ~ location = "US-CENTRAL1" -> "US-EAST1" # forces replacement
        name     = "skillbyexample-tf-assets-437219"
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Location is immutable on a bucket, so Terraform can’t patch it — the plan shows -/+, meaning destroy then create, and the bucket’s contents (with force_destroy = true) would be deleted.

2. Inspect the current state to see what Terraform actually knows about the bucket.
terraform state show google_storage_bucket.assets
# google_storage_bucket.assets:
resource "google_storage_bucket" "assets" {
    id            = "skillbyexample-tf-assets-437219"
    location      = "US-CENTRAL1"
    name          = "skillbyexample-tf-assets-437219"
    project       = "skillbyexample-437219"
    self_link     = "https://www.googleapis.com/storage/v1/b/skillbyexample-tf-assets-437219"
    storage_class = "STANDARD"
    url           = "gs://skillbyexample-tf-assets-437219"
}
3. Import a bucket created manually outside Terraform into state.
gcloud storage buckets create gs://skillbyexample-manual-437219 --location=us-central1

terraform import google_storage_bucket.manual skillbyexample-manual-437219
google_storage_bucket.manual: Importing from ID "skillbyexample-manual-437219"...
google_storage_bucket.manual: Import prepared!
  Prepared google_storage_bucket for import
google_storage_bucket.manual: Refreshing state... [id=skillbyexample-manual-437219]

Import successful!

Import only pulls the resource into state — the matching resource block still has to be written in the .tf file by hand, or the next plan shows it as something to destroy.

4. Deliberately break the config with a typo'd argument and read the validation error before ever calling the API.
resource "google_storage_bucket" "assets" {
  nmae     = "skillbyexample-tf-assets-437219"
  location = "US-CENTRAL1"
}
terraform validate
╷
│ Error: Unsupported argument
│
│   on main.tf line 8, in resource "google_storage_bucket" "assets":
│    8:   nmae     = "skillbyexample-tf-assets-437219"
│
│ An argument named "nmae" is not expected here.
╵

validate catches this before any API call is made — worth running in CI before plan, since a config-level typo otherwise only surfaces after plan has already talked to GCP.

5. Destroy every resource this lesson created.
terraform destroy -auto-approve
google_compute_instance.vm: Destroying... [id=projects/skillbyexample-437219/zones/us-central1-a/instances/tf-web-1]
google_compute_instance.vm: Destruction complete after 12s
google_storage_bucket.assets: Destroying... [id=skillbyexample-tf-assets-437219]
google_storage_bucket.assets: Destruction complete after 1s

Destroy complete! Resources: 2 destroyed.

destroy reads the same state file apply wrote, which is exactly why losing that file is dangerous — without it, Terraform has no record of what it’s responsible for cleaning up.

Next: Cloud Monitoring — metrics, a log-based metric, and a basic alerting policy.

Frequently Asked Questions

Is Google Cloud Deployment Manager still recommended?
No. Google has deprecated Deployment Manager and directs users toward Terraform (via the official google and google-beta providers) or Config Connector for Kubernetes-native workflows. Existing Deployment Manager deployments keep working, but new infrastructure-as-code on GCP should be written in Terraform.
What does terraform plan do that apply doesn't?
plan computes and prints the diff between your config and real infrastructure state without changing anything — it's the dry run. apply performs the same diff calculation and then actually creates, updates, or destroys resources to match, asking for confirmation unless run with -auto-approve.
Why does Terraform need a state file?
State maps each resource block in your config to the real cloud object it created, tracking IDs and attributes Terraform can't infer from config alone. Without it, Terraform would have no way to know that the google_storage_bucket named my_bucket in your file corresponds to a specific already-existing bucket, and would try to create a duplicate.
What causes most terraform apply failures on GCP?
A required API not enabled on the project, an IAM role missing on the identity Terraform authenticates as, or a resource name collision (bucket and project IDs are globally unique) — in that rough order of frequency. All three surface as an error from the underlying GCP API, passed through by the provider almost verbatim.
Should the GCP credentials Terraform uses be a service account or my own user login?
A dedicated service account with only the roles Terraform's configs actually need, referenced via GOOGLE_APPLICATION_CREDENTIALS or Application Default Credentials — not a human's login. That way Terraform's exact permission footprint is visible in one IAM binding instead of inheriting whatever broad access an individual user happens to have.