Google Cloud · 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
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.