Terraform Reference: GCP Basic Infrastructure
Terraform reference: GCP basic infrastructure
TL;DR
- Bottom line: Terraform with the Google provider (
hashicorp/google ~> 6.0) lets you define VPC networks, Compute Engine VMs, Cloud Storage, Cloud SQL, and IAM as code. - Key tool/command:
terraform init && terraform plan && terraform apply - Watch out for: GCP APIs must be enabled before Terraform can create resources — use
google_project_serviceto enable APIs declaratively. - Works with: Terraform 1.6+, Google provider ~> 6.0, OpenTofu 1.6+.
Constraints
- Always use GCS backend for remote state — never commit terraform.tfstate.
- Pin provider versions with
~>constraint. - Enable versioning and encryption on the GCS state bucket.
- Use service accounts with least-privilege IAM — never use roles/owner for Terraform.
- GCP requires enabling APIs before creating resources.
- GCS backend supports native state locking (no DynamoDB needed).
Quick Reference
| Resource | Terraform Type | Key Arguments | Notes |
|---|---|---|---|
| VPC Network | google_compute_network | name, auto_create_subnetworks | Set false for custom |
| Subnet | google_compute_subnetwork | network, ip_cidr_range, region | One per region |
| Firewall Rule | google_compute_firewall | network, allow, source_ranges | Network-level |
| Compute Instance | google_compute_instance | machine_type, zone, boot_disk | Use data source for image |
| Cloud Storage | google_storage_bucket | name, location, storage_class | Globally unique name |
| Cloud SQL | google_sql_database_instance | database_version, settings.tier | Private IP with VPC peering |
| IAM Binding | google_project_iam_member | role, member | Use member for additive |
| Service Account | google_service_account | account_id | One per service |
| Cloud NAT | google_compute_router_nat | router, nat_ip_allocate_option | Private subnet egress |
| API Enablement | google_project_service | service | Enable APIs declaratively |
| GCS Backend | backend "gcs" | bucket, prefix | Native locking |
Decision Tree
START
├── First time with Terraform on GCP?
│ ├── YES → Enable APIs + GCS backend (Step 1-2)
│ └── NO ↓
├── Need full VPC with private/public subnets?
│ ├── YES → Custom VPC + Cloud NAT (Step 3)
│ └── NO ↓
├── Need VMs or containers?
│ ├── VMs → Compute Engine (Step 4)
│ ├── Containers → Cloud Run or GKE
│ └── NO ↓
├── Need managed database?
│ ├── YES → Cloud SQL with private IP
│ └── NO ↓
└── DEFAULT → VPC + Compute Engine + Cloud Storage
Step-by-Step Guide
1. Configure provider and GCS backend
Set up the Google provider and enable APIs. [src1]
terraform {
required_version = ">= 1.6.0"
required_providers {
google = { source = "hashicorp/google", version = "~> 6.0" }
}
backend "gcs" {
bucket = "my-project-tf-state"
prefix = "prod"
}
}
provider "google" {
project = var.project_id
region = var.region
}
resource "google_project_service" "apis" {
for_each = toset(["compute.googleapis.com", "sqladmin.googleapis.com", "storage.googleapis.com"])
service = each.value
disable_on_destroy = false
}
Verify: terraform init → backend configured
2. Create VPC with subnets
Custom-mode VPC. [src5]
resource "google_compute_network" "main" {
name = "${var.project_name}-vpc"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "public" {
name = "${var.project_name}-public"
ip_cidr_range = "10.0.1.0/24"
region = var.region
network = google_compute_network.main.id
}
Verify: terraform plan → shows network resources
3. Launch Compute Engine instance
Deploy a VM. [src2]
data "google_compute_image" "debian" {
family = "debian-12"
project = "debian-cloud"
}
resource "google_compute_instance" "web" {
name = "${var.project_name}-web"
machine_type = "e2-micro"
zone = "${var.region}-a"
boot_disk {
initialize_params { image = data.google_compute_image.debian.self_link }
}
network_interface {
subnetwork = google_compute_subnetwork.public.id
access_config {}
}
tags = ["web", "ssh"]
}
Verify: terraform apply → instance running
Code Examples
HCL: VPC + Cloud SQL with private IP
# Input: Terraform configuration
# Output: Cloud SQL PostgreSQL with private VPC peering
resource "google_compute_global_address" "private_ip" {
name = "private-ip-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 16
network = google_compute_network.main.id
}
resource "google_sql_database_instance" "main" {
name = "${var.project_name}-db"
database_version = "POSTGRES_16"
region = var.region
settings {
tier = "db-f1-micro"
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.main.id
}
backup_configuration { enabled = true; start_time = "03:00" }
}
deletion_protection = var.environment == "prod"
}
HCL: Cloud Storage static site with CDN
# Input: Domain name
# Output: Cloud Storage + HTTP(S) Load Balancer with CDN
resource "google_storage_bucket" "website" {
name = var.domain_name
location = "US"
website {
main_page_suffix = "index.html"
not_found_page = "404.html"
}
uniform_bucket_level_access = true
}
resource "google_compute_backend_bucket" "website" {
name = "${var.project_name}-backend"
bucket_name = google_storage_bucket.website.name
enable_cdn = true
}
Anti-Patterns
Wrong: Using default network
# ❌ BAD — permissive firewall rules
resource "google_compute_instance" "web" {
network_interface { network = "default" }
}
Correct: Custom VPC with explicit rules
# ✅ GOOD — isolated network
resource "google_compute_network" "main" {
name = "my-vpc"
auto_create_subnetworks = false
}
resource "google_compute_instance" "web" {
network_interface { subnetwork = google_compute_subnetwork.public.id }
}
Wrong: Forgetting to enable APIs
# ❌ BAD — "API not enabled" error
resource "google_compute_instance" "web" { ... }
Correct: Declare API enablement
# ✅ GOOD — API enabled first
resource "google_project_service" "compute" {
service = "compute.googleapis.com"
disable_on_destroy = false
}
resource "google_compute_instance" "web" {
depends_on = [google_project_service.compute]
}
Wrong: Owner role for Terraform SA
# ❌ BAD — excessive permissions
role = "roles/owner"
Correct: Least-privilege roles
# ✅ GOOD — specific roles
for_each = toset(["roles/compute.admin", "roles/storage.admin"])
Common Pitfalls
- API not enabled errors: Fix: add
google_project_servicewithdisable_on_destroy = false. [src6] - Cloud SQL deletion protection: Blocks destroy. Fix: set to false in dev, true in prod. [src1]
- Globally unique bucket names: Fix: include project ID in name. [src1]
- auto_create_subnetworks default: Creates subnet in every region. Fix: set to
false. [src2] - SSH via public IP: Insecure. Fix: use IAP tunneling (35.235.240.0/20). [src7]
- Using iam_binding instead of iam_member: Authoritative, removes other members. Fix: use
google_project_iam_member. [src7]
Diagnostic Commands
# Initialize providers
terraform init
# Preview changes
terraform plan -out=tfplan
# Apply plan
terraform apply tfplan
# List state
terraform state list
# Check enabled APIs
gcloud services list --enabled --project=PROJECT_ID
# List VMs
gcloud compute instances list --project=PROJECT_ID
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Terraform 1.9+ | Current | None | HCL improvements |
| Google Provider 6.x | Current | Removed deprecated resources | Check migration guide |
| Google Provider 5.x | Supported | Default labels | — |
| OpenTofu 1.6+ | Active | Fork of TF 1.6 | Drop-in compatible |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Reproducible GCP infrastructure | One-off gcloud commands | gcloud CLI |
| Team collaboration with reviews | Rapid prototyping | GCP Console |
| Multi-cloud deployments | GCP-only with DM expertise | Deployment Manager |
| Long-lived infra (VPCs, Cloud SQL) | K8s resource management | Config Connector, Helm |
Important Caveats
- The
googleandgoogle-betaproviders are separate — beta features require the beta provider. - GCS backend locks state natively, but a crashed apply can leave a stale lock — use
terraform force-unlock. - Cloud SQL instances take 5-10 minutes to create.
google_project_iam_bindingis authoritative — usegoogle_project_iam_memberfor additive bindings.