Compute and Storage Blocks

Quote

“The best infrastructure is the infrastructure you don’t have to think about.”

Werner Vogels, AWS re:Invent keynote

Assumed variables

All blocks in this file reference shared input variables (var.project_id, var.env, var.region, var.zone, var.vpc_network, var.subnetwork). See the Variables Reference section at the bottom for the full list and types. Define them in your variables.tf.

GCP provider documentation

For the canonical argument reference of each resource, see the Google Cloud Terraform provider registry. The blocks here track provider version 5.x syntax.

Compute Engine Blocks

Terraform resources for GCP Compute Engine — virtual machines, persistent disks, snapshots, snapshot scheduling policies, and VM start/stop schedules. Cross-reference the gcloud equivalents in VM lifecycle and Disks and snapshots.

google_compute_instance

The google_compute_instance resource provisions a single GCP Compute Engine virtual machine. Changing zone or machine_type (on certain families) forces resource replacement — Terraform destroys the existing VM and creates a new one, which means data loss on any attached ephemeral disks. The Terraform service account needs roles/compute.instanceAdmin.v1 on the project.

Force-replacement arguments

Changing zone, boot_disk.initialize_params.image, or switching machine_type between incompatible families triggers a destroy-then-create cycle. For stateful VMs (databases, orchestrators), this means downtime and potential data loss on the boot disk.

Protect stateful VMs

Use lifecycle { prevent_destroy = true } on any VM with persistent state. Use deletion_protection = true at the GCP level as a second safety net. Always store data on a separately managed google_compute_disk.

google_compute_instance | Database server

A long-lived, single-node database VM with private-only networking, a dedicated SSD data disk, OS-level hardening (Shielded VM), and a service account scoped to minimum permissions. Typical for PostgreSQL, MySQL, or SQL Server on Linux instances that should never have a public IP.

The data disk is created as a separate google_compute_disk resource so it survives terraform destroy on the VM. The startup script formats and mounts the data disk on first boot. OS Login with IAP replaces static SSH keys. The ignore_changes on startup-script prevents Terraform from detecting drift caused by manual debugging sessions.

Create a standalone SSD data disk for the database server.

resource "google_compute_disk" "db_data" {
  name    = "db-data-disk"
  type    = "pd-ssd"
  zone    = var.zone
  size    = 500
 
  labels = {
    env  = var.env
    role = "database"
  }
}

Provision a private database VM with SSD data disk, Shielded VM, and OS Login.

resource "google_compute_instance" "db_server" {
  name         = "${var.env}-db-server"
  machine_type = "e2-standard-4"
  zone         = var.zone
  tags         = ["db-server", "internal"]
 
  boot_disk {
    auto_delete = true
    initialize_params {
      image = "ubuntu-os-cloud/ubuntu-2204-lts"
      size  = 50
      type  = "pd-balanced"
    }
  }
 
  attached_disk {
    source      = google_compute_disk.db_data.self_link
    device_name = "db-data"
    mode        = "READ_WRITE"
  }
 
  network_interface {
    network    = var.vpc_network
    subnetwork = var.subnetwork
  }
 
  metadata = {
    startup-script = <<-SCRIPT
      #!/bin/bash
      set -euo pipefail
      if ! blkid /dev/disk/by-id/google-db-data; then
        mkfs.ext4 -F /dev/disk/by-id/google-db-data
      fi
      mkdir -p /mnt/data
      mount /dev/disk/by-id/google-db-data /mnt/data
      echo "/dev/disk/by-id/google-db-data /mnt/data ext4 defaults,nofail 0 2" >> /etc/fstab
    SCRIPT
 
    enable-oslogin = "TRUE"
  }
 
  service_account {
    email  = var.db_service_account_email
    scopes = ["cloud-platform"]
  }
 
  shielded_instance_config {
    enable_secure_boot          = true
    enable_vtpm                 = true
    enable_integrity_monitoring = true
  }
 
  scheduling {
    on_host_maintenance = "MIGRATE"
    automatic_restart   = true
    preemptible         = false
  }
 
  deletion_protection = true
 
  labels = {
    env  = var.env
    role = "database"
  }
 
  lifecycle {
    ignore_changes = [metadata["startup-script"]]
  }
}
ArgumentRequiredDescription
nameYesVM name, must be unique within the project
machine_typeYesVM size — e2-standard-4 gives 4 vCPU, 16 GB RAM (cost-effective E2 family)
zoneYesGCP zone — must match the attached data disk zone
tagsNoNetwork tags used by VPC firewall rules to control traffic
boot_disk.auto_deleteNotrue (default) deletes the boot disk when the VM is deleted
boot_disk.initialize_params.imageYesOS image family — changing this forces VM replacement
boot_disk.initialize_params.sizeNoBoot disk size in GiB (default: image default, usually 10)
boot_disk.initialize_params.typeNoDisk type — pd-balanced is a good default for OS disks
attached_disk.sourceYesself_link of the google_compute_disk to attach
attached_disk.device_nameNoDevice path inside the guest OS (/dev/disk/by-id/google-<name>)
attached_disk.modeNoREAD_WRITE (default) or READ_ONLY for multi-reader
network_interface.networkYesVPC network — omitting access_config block means no public IP
metadata.startup-scriptNoShell script executed as root on every boot
metadata.enable-osloginNoTRUE enables IAP + OS Login instead of static SSH keys
service_account.emailNoDedicated service account email — use a per-VM SA for least privilege
service_account.scopesNo["cloud-platform"] delegates all permissions to IAM roles on the SA
shielded_instance_config.*NoEnables Secure Boot, vTPM, and integrity monitoring
scheduling.on_host_maintenanceNoMIGRATE (default) live-migrates during maintenance; TERMINATE stops the VM
scheduling.automatic_restartNotrue (default) restarts after host failure
scheduling.preemptibleNofalse (default) for persistent VMs
deletion_protectionNotrue blocks terraform destroy — must be disabled manually first
lifecycle.ignore_changesNoPrevents Terraform from detecting drift on specified attributes

terraform plan output

Run terraform plan with this block in your project to generate the plan output. Not fabricated here.

google_compute_instance | Orchestration server (Airflow)

A single VM running Apache Airflow (or a similar orchestrator) via Docker Compose. Container-Optimized OS (COS) handles Docker out of the box. An ephemeral public IP is included so the scheduler can reach external APIs — lock it down with firewall tags. The pd-balanced boot disk is sufficient since the OS carries only container layers and logs; all DAG state lives in the metadata database and GCS.

The user-data metadata key is COS-specific — it uses cloud-init syntax to pull and run the Airflow Docker image on first boot. The access_config block without a nat_ip lets GCP assign an ephemeral public IP automatically. Remove the entire access_config block for private-only networking with Cloud NAT.

Provision an Airflow orchestrator on Container-Optimized OS with ephemeral public IP.

resource "google_compute_instance" "airflow" {
  name         = "${var.env}-airflow"
  machine_type = "e2-standard-2"
  zone         = var.zone
  tags         = ["airflow", "allow-iap"]
 
  boot_disk {
    auto_delete = true
    initialize_params {
      image = "cos-cloud/cos-stable"
      size  = 50
      type  = "pd-balanced"
    }
  }
 
  network_interface {
    network    = var.vpc_network
    subnetwork = var.subnetwork
 
    access_config {}
  }
 
  metadata = {
    user-data = <<-CLOUDINIT
      #cloud-config
      runcmd:
        - docker-credential-gcr configure-docker
        - docker pull ${var.airflow_image}
        - docker run -d --name airflow --restart=unless-stopped \
            -p 8080:8080 \
            -e AIRFLOW__CORE__EXECUTOR=LocalExecutor \
            -e AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=${var.airflow_db_conn} \
            ${var.airflow_image}
    CLOUDINIT
 
    enable-oslogin = "TRUE"
  }
 
  service_account {
    email  = var.airflow_service_account_email
    scopes = ["cloud-platform"]
  }
 
  scheduling {
    on_host_maintenance = "MIGRATE"
    automatic_restart   = true
    preemptible         = false
  }
 
  labels = {
    env  = var.env
    role = "orchestration"
  }
}
ArgumentRequiredDescription
machine_typeYese2-standard-2 gives 2 vCPU, 8 GB RAM — scale up if DAG count grows
boot_disk.initialize_params.imageYescos-cloud/cos-stable — Container-Optimized OS with Docker pre-installed
access_configNoEmpty block assigns an ephemeral public IP; omit entirely for private-only
metadata.user-dataNoCOS cloud-init config — runs Docker commands on boot
service_account.emailNoSA needs GCS, BigQuery, and Pub/Sub access for DAG operations
scheduling.preemptibleNofalse — DAGs must not be interrupted by preemption

Sensitive connection string

The airflow_db_conn variable contains a database connection string with credentials. It is marked sensitive = true in the variable definition, but it will still appear in the VM metadata in the GCP console.

Use Secret Manager

Store the connection string in Secret Manager and fetch it at runtime inside the container instead of baking it into metadata.

google_compute_instance | Spot / preemptible worker

Batch processing, CI runners, ML training jobs, or any fault-tolerant workload where cost matters more than uptime. Spot VMs cost 60–91% less than standard VMs but can be reclaimed by GCP at any time with a 30-second notice. Always checkpoint work to GCS. Avoid for databases, stateful services, or anything behind a synchronous API.

The provisioning_model = "SPOT" is the current-generation API (provider 4.x+). Legacy PREEMPTIBLE still works but is deprecated. Spot VMs cannot live-migrate, so on_host_maintenance must be TERMINATE. The ignore_changes = [scheduling] prevents Terraform from detecting drift when GCP modifies scheduling metadata on preempted VMs.

Private-only networking is shown here — use Cloud NAT for outbound internet access.

Provision a cost-optimized Spot VM for fault-tolerant batch workloads.

resource "google_compute_instance" "spot_worker" {
  name         = "${var.env}-spot-worker"
  machine_type = "n2-standard-8"
  zone         = var.zone
  tags         = ["spot-worker", "allow-iap"]
 
  boot_disk {
    auto_delete = true
    initialize_params {
      image = "debian-cloud/debian-12"
      size  = 100
      type  = "pd-balanced"
    }
  }
 
  network_interface {
    network    = var.vpc_network
    subnetwork = var.subnetwork
  }
 
  metadata = {
    startup-script = var.worker_startup_script
    enable-oslogin = "TRUE"
  }
 
  service_account {
    email  = var.worker_service_account_email
    scopes = ["cloud-platform"]
  }
 
  scheduling {
    preemptible                 = true
    on_host_maintenance         = "TERMINATE"
    automatic_restart           = false
    provisioning_model          = "SPOT"
  }
 
  labels = {
    env  = var.env
    role = "worker"
    type = "spot"
  }
 
  lifecycle {
    ignore_changes = [scheduling]
  }
}
ArgumentRequiredDescription
machine_typeYesn2-standard-8 gives 8 vCPU, 32 GB RAM — N2 is good for compute-heavy workloads
boot_disk.initialize_params.imageYesdebian-cloud/debian-12 — small and fast to boot
boot_disk.initialize_params.sizeNo100 GiB — enough for job artifacts; checkpoint results to GCS
scheduling.preemptibleNotrue enables spot pricing
scheduling.on_host_maintenanceNoMust be TERMINATE for spot VMs — they cannot live-migrate
scheduling.automatic_restartNofalse — do not auto-restart preemptible VMs
scheduling.provisioning_modelNoSPOT (current) or PREEMPTIBLE (legacy, deprecated)
lifecycle.ignore_changesNo[scheduling] prevents drift detection from GCP-managed scheduling changes

Spot VM vs standard VM

Use spot VMs for fault-tolerant, stateless workloads (batch ETL, CI/CD runners, ML training). Use standard VMs for databases, orchestrators, and anything that cannot tolerate interruption. For horizontally scalable services, consider a MIG with a mix of spot and standard instances.

google_compute_instance_template

The google_compute_instance_template resource creates a reusable VM blueprint for managed instance groups. Templates are immutable — any change creates a new template. Use create_before_destroy in the lifecycle block to ensure the new template exists before the old one is removed, preventing downtime during updates. Templates are regional (not zonal) and cannot reference external disks — all disk configuration is inline.

Use case: stateless, horizontally scalable services — web frontends, API backends, data transformation workers.

Create a reusable VM blueprint for managed instance groups.

resource "google_compute_instance_template" "app" {
  name_prefix  = "${var.env}-app-template-"
  machine_type = "e2-standard-2"
  region       = var.region
  tags         = ["app-server", "allow-lb"]
 
  disk {
    source_image = "ubuntu-os-cloud/ubuntu-2204-lts"
    auto_delete  = true
    boot         = true
    disk_type    = "pd-balanced"
    disk_size_gb = 30
  }
 
  network_interface {
    network    = var.vpc_network
    subnetwork = var.subnetwork
  }
 
  metadata = {
    startup-script = var.app_startup_script
    enable-oslogin = "TRUE"
  }
 
  service_account {
    email  = var.app_service_account_email
    scopes = ["cloud-platform"]
  }
 
  scheduling {
    on_host_maintenance = "MIGRATE"
    automatic_restart   = true
    preemptible         = false
  }
 
  labels = {
    env  = var.env
    role = "app"
  }
 
  lifecycle {
    create_before_destroy = true
  }
}
ArgumentRequiredDescription
name_prefixYesPrefix for the template name — Terraform appends a unique hash suffix
machine_typeYesVM size applied to every instance created from this template
regionYesTemplates are regional, not zonal
tagsNoNetwork tags — used by load balancer and firewall rules
disk.source_imageYesBase OS image for the boot disk
disk.disk_typeNopd-balanced (default), pd-ssd, or pd-standard
disk.disk_size_gbNoBoot disk size in GiB — 30 GiB is usually enough for app + logs
metadata.startup-scriptNoBootstraps the app on each new instance (install deps, pull binary, start)
lifecycle.create_before_destroyNoCreates the new template before destroying the old one — prevents downtime

Instances behind a load balancer

Omit the access_config block in network_interface — instances behind a load balancer do not need public IPs. Outbound traffic can route through Cloud NAT.

google_compute_instance_group_manager

The google_compute_instance_group_manager creates and manages a fleet of identical VMs from an instance template. The MIG handles auto-healing (replacing unhealthy instances) and rolling updates (replacing instances when the template changes). The target_size sets the initial fleet size but is overridden by an autoscaler when attached.

Create a managed fleet of identical VMs with auto-healing and rolling updates.

resource "google_compute_instance_group_manager" "app" {
  name               = "${var.env}-app-mig"
  base_instance_name = "${var.env}-app"
  zone               = var.zone
  target_size        = 2
 
  version {
    instance_template = google_compute_instance_template.app.id
    name              = "primary"
  }
 
  auto_healing_policies {
    health_check      = google_compute_health_check.app.id
    initial_delay_sec = 120
  }
 
  update_policy {
    type                         = "PROACTIVE"
    minimal_action               = "REPLACE"
    max_surge_fixed              = 1
    max_unavailable_fixed        = 0
    replacement_method           = "SUBSTITUTE"
  }
 
  named_port {
    name = "http"
    port = 8080
  }
}
ArgumentRequiredDescription
nameYesMIG name
base_instance_nameYesPrefix for individual VM names — GCP appends a random suffix (e.g., app-abc123)
zoneYesSingle-zone MIG — use google_compute_region_instance_group_manager for HA across zones
target_sizeNoInitial instance count — overridden by autoscaler when attached
version.instance_templateYesID of the active template — changing this triggers a rolling update
version.nameNoVersion label — useful for canary deployments with two version blocks
auto_healing_policies.health_checkNoHealth check ID — unhealthy instances are automatically replaced
auto_healing_policies.initial_delay_secNoSeconds to wait before health-checking a new instance (default: 300)
update_policy.typeNoPROACTIVE updates all instances immediately; OPPORTUNISTIC only on scale events
update_policy.minimal_actionNoREPLACE recreates instances; RESTART just reboots them
update_policy.max_surge_fixedNoExtra instances created during rolling update (set to 1 for zero-downtime)
update_policy.max_unavailable_fixedNoMax instances unavailable during update (set to 0 for zero-downtime)
named_portNoNamed port mapping used by the load balancer backend service

Regional MIG for high availability

A single-zone MIG has all instances in one zone. For production, use google_compute_region_instance_group_manager to spread instances across multiple zones in a region — surviving a zone outage without manual intervention.

google_compute_health_check

The google_compute_health_check defines how GCP determines whether a VM instance is healthy. Used by both the MIG (for auto-healing) and the load balancer (for traffic routing). An instance that fails unhealthy_threshold consecutive checks is marked unhealthy — the MIG replaces it, and the load balancer stops routing traffic to it.

Define an HTTP health check for MIG auto-healing and load balancer routing.

resource "google_compute_health_check" "app" {
  name                = "${var.env}-app-health-check"
  check_interval_sec  = 10
  timeout_sec         = 5
  healthy_threshold   = 2
  unhealthy_threshold = 3
 
  http_health_check {
    port         = 8080
    request_path = "/health"
  }
}
ArgumentRequiredDescription
nameYesHealth check name
check_interval_secNoSeconds between probes (default: 5)
timeout_secNoSeconds to wait for a response before marking as failed (default: 5)
healthy_thresholdNoConsecutive successes needed to mark healthy (default: 2)
unhealthy_thresholdNoConsecutive failures needed to mark unhealthy (default: 2)
http_health_check.portYesPort to probe on each instance
http_health_check.request_pathNoEndpoint path — must return HTTP 200 when the app is ready

google_compute_autoscaler

The google_compute_autoscaler adjusts the number of instances in a MIG based on a scaling metric. The most common metric is CPU utilization — the autoscaler adds instances when average CPU exceeds the target and removes instances when it drops below. The cooldown_period prevents rapid scale oscillations.

Scale the MIG between 2 and 10 instances based on 70% CPU target.

resource "google_compute_autoscaler" "app" {
  name   = "${var.env}-app-autoscaler"
  zone   = var.zone
  target = google_compute_instance_group_manager.app.id
 
  autoscaling_policy {
    min_replicas    = 2
    max_replicas    = 10
    cooldown_period = 60
 
    cpu_utilization {
      target = 0.7
    }
  }
}
ArgumentRequiredDescription
nameYesAutoscaler name
zoneYesMust match the MIG zone
targetYesID of the MIG to scale
autoscaling_policy.min_replicasYesMinimum instance count — never scale below this
autoscaling_policy.max_replicasYesMaximum instance count — never scale above this
autoscaling_policy.cooldown_periodNoSeconds to wait after a scaling event before the next decision (default: 60)
cpu_utilization.targetNoTarget average CPU — scale out above 0.7 (70%), scale in below

MIG architecture

How the instance template, MIG, health check, and autoscaler resources relate:

Visualize how instance template, MIG, health check, and autoscaler wire together.


flowchart TD
    TPL["google_compute_instance_template<br/>VM blueprint: image, machine_type, SA"]
    MIG["google_compute_instance_group_manager<br/>Fleet: target_size, update_policy"]
    HC["google_compute_health_check<br/>HTTP /health probe"]
    AS["google_compute_autoscaler<br/>CPU target: 70%"]
    VM1["VM instance 1"]
    VM2["VM instance 2"]
    VMN["VM instance N"]

    TPL -->|"version.instance_template"| MIG
    HC -->|"auto_healing_policies"| MIG
    AS -->|"target = MIG.id"| MIG
    MIG --> VM1
    MIG --> VM2
    MIG --> VMN
    HC -.->|"probe :8080/health"| VM1
    HC -.->|"probe :8080/health"| VM2
    HC -.->|"probe :8080/health"| VMN

google_compute_disk

A standalone persistent disk that outlives its VM. Use when you need a data volume (database data directory, shared NFS-style volume) that survives terraform destroy on the instance. Always create data disks as separate resources rather than inline in the VM block. Disk size can be increased but never decreased — GCP does not support shrinking persistent disks. See Disks and snapshots for gcloud equivalents.

Disk size cannot be reduced

Once a persistent disk is created, you can increase its size in-place (non-destructive) but never decrease it. Plan capacity carefully — oversizing wastes money, undersizing requires migration.

Right-size with monitoring

Use Cloud Monitoring disk utilization metrics to right-size before provisioning. Start conservative and grow as needed.

Create a persistent SSD disk that outlives its VM.

resource "google_compute_disk" "data" {
  name                      = "${var.env}-data-disk"
  type                      = "pd-ssd"
  zone                      = var.zone
  size                      = 200
  physical_block_size_bytes = 4096
 
  labels = {
    env  = var.env
    role = "data"
  }
 
  lifecycle {
    prevent_destroy = true
  }
}
ArgumentRequiredDescription
nameYesDisk name visible in the GCP console
typeNopd-ssd, pd-balanced (default), pd-standard, or pd-extreme
zoneYesMust match the zone of the VM it will be attached to
sizeNoSize in GiB — can be increased but never decreased
physical_block_size_bytesNo4096 (default) or 16384 — match the OS block size
lifecycle.prevent_destroyNotrue blocks accidental deletion — Terraform plan will fail if destroy is attempted

google_compute_attached_disk

Attaches a standalone google_compute_disk to an existing VM. Use this resource when the disk and the VM are managed in separate Terraform modules or when a disk needs to be detached and reattached to different VMs.

In READ_ONLY mode, a disk can be attached to multiple VMs simultaneously — useful for sharing static datasets.

Attach a standalone disk to a VM in read-write mode.

resource "google_compute_attached_disk" "data_attach" {
  disk     = google_compute_disk.data.id
  instance = google_compute_instance.db_server.id
  zone     = var.zone
  mode     = "READ_WRITE"
 
  device_name = "data"
}
ArgumentRequiredDescription
diskYesID of the google_compute_disk to attach
instanceYesID of the google_compute_instance target
zoneYesMust match both the disk and VM zone
modeNoREAD_WRITE (default) or READ_ONLY for multi-reader scenarios
device_nameNoDevice path inside the guest OS: /dev/disk/by-id/google-<name>

google_compute_snapshot

A point-in-time backup of a persistent disk. Snapshots are incremental after the first — only changed blocks are stored, reducing cost and time. Stored in Cloud Storage automatically and charged at GCS rates. Use before risky operations (OS upgrades, schema migrations). For recurring backups, use google_compute_resource_policy instead.

The storage_locations argument controls where snapshot data is stored. Use a multi-region value ("us", "eu", "asia") for resilience or a single region ("us-central1") for locality. Omit to use the GCP default (same region as the source disk).

Take a manual point-in-time backup of the data disk.

resource "google_compute_snapshot" "db_backup" {
  name        = "${var.env}-db-backup-manual"
  source_disk = google_compute_disk.data.id
  zone        = var.zone
  description = "Manual pre-migration snapshot"
 
  storage_locations = ["us"]
 
  labels = {
    env    = var.env
    type   = "manual"
    source = "db-data-disk"
  }
}
ArgumentRequiredDescription
nameYesSnapshot name
source_diskYesID of the disk to snapshot (boot or data disk)
zoneYesZone of the source disk
descriptionNoHuman-readable description for the GCP console
storage_locationsNoWhere to store — ["us"] (multi-region) or ["us-central1"] (regional)
snapshot_encryption_key.raw_keyNoBase64-encoded AES-256 key for CMEK encryption

CMEK encryption

For compliance-sensitive workloads, encrypt snapshots with a Customer-Managed Encryption Key (CMEK) using the snapshot_encryption_key block. The key must be in the same region as the snapshot storage location.

google_compute_resource_policy

A resource policy defines scheduled operations on Compute Engine resources — automated snapshots or VM start/stop schedules. Policies are regional and are attached to disks or VMs via dedicated attachment resources.

google_compute_resource_policy | Scheduled snapshot

Automated daily snapshots with retention so you don’t need manual snapshots. Attach the policy to any disk. GCP handles the schedule, incremental snapshots, and deletion of expired snapshots. The on_source_disk_delete argument controls whether auto-snapshots are preserved or deleted when the source disk is destroyed.

Setting guest_flush = true flushes OS write buffers before the snapshot for application-consistent backups — this requires the QEMU guest agent installed on the VM.

Define daily automated snapshots with 7-day retention.

resource "google_compute_resource_policy" "daily_snapshot" {
  name   = "${var.env}-daily-snapshot-policy"
  region = var.region
 
  snapshot_schedule_policy {
    schedule {
      daily_schedule {
        days_in_cycle = 1
        start_time    = "04:00"
      }
    }
 
    retention_policy {
      max_retention_days    = 7
      on_source_disk_delete = "KEEP_AUTO_SNAPSHOTS"
    }
 
    snapshot_properties {
      labels = {
        policy     = "daily-snapshot"
        managed_by = "terraform"
      }
      storage_locations = ["us"]
      guest_flush       = false
    }
  }
}

Attach the snapshot policy to the data disk.

resource "google_compute_disk_resource_policy_attachment" "data_snapshot" {
  name = google_compute_resource_policy.daily_snapshot.name
  disk = google_compute_disk.data.name
  zone = var.zone
}
ArgumentRequiredDescription
nameYesPolicy name
regionYesResource policies are regional
daily_schedule.days_in_cycleYesFrequency in days — 1 means every day (minimum)
daily_schedule.start_timeYesUTC time to start — pick off-peak hours for your region
max_retention_daysNoDelete snapshots older than N days (default: no expiration)
on_source_disk_deleteNoKEEP_AUTO_SNAPSHOTS (default) or APPLY_RETENTION_POLICY
storage_locationsNoWhere to store snapshots — multi-region or single region
guest_flushNotrue flushes OS buffers before snapshot (requires QEMU guest agent)

google_compute_resource_policy | VM start/stop (business hours)

Dev/test VMs and non-critical batch VMs that only need to run during business hours. Start/stop scheduling can cut compute costs by ~65% for a VM that runs 9 hours/day on weekdays instead of 24/7. Schedules use IANA timezone names — use "UTC" to avoid daylight saving time surprises.

The Compute Engine service agent (<project-number>@cloudservices.gserviceaccount.com) needs roles/compute.instanceAdmin.v1 on the VM to start and stop it. The IAM binding must be created before attaching the schedule.

Schedule business-hours start/stop for dev VMs.

resource "google_compute_resource_policy" "business_hours" {
  name   = "${var.env}-business-hours-schedule"
  region = var.region
 
  instance_schedule_policy {
    time_zone = "America/New_York"
 
    vm_start_schedule {
      schedule = "0 8 * * MON-FRI"
    }
 
    vm_stop_schedule {
      schedule = "0 19 * * MON-FRI"
    }
  }
}

Grant the Compute Engine service agent permission to start/stop the VM.

resource "google_compute_instance_iam_member" "schedule_actor" {
  instance_name = google_compute_instance.db_server.name
  zone          = var.zone
  role          = "roles/compute.instanceAdmin.v1"
  member        = "serviceAccount:${var.project_number}@cloudservices.gserviceaccount.com"
}

Attach the schedule policy to the database server.

resource "google_compute_resource_policy_attachment" "biz_hours_attach" {
  name     = google_compute_resource_policy.business_hours.name
  instance = google_compute_instance.db_server.name
  zone     = var.zone
}
ArgumentRequiredDescription
nameYesPolicy name
regionYesMust match the VM’s region
time_zoneYesIANA timezone for cron schedules — "America/New_York", "UTC", etc.
vm_start_schedule.scheduleYesCron expression for start time
vm_stop_schedule.scheduleYesCron expression for stop time

Cost savings estimate

A VM running 9 hours/day × 5 days/week instead of 24/7 saves approximately 65% on compute costs. Apply this pattern to all non-production VMs that don’t need to run overnight or on weekends.

Cloud Storage Blocks

Terraform resources for GCS buckets, IAM bindings, object uploads, and Pub/Sub notifications. Cross-reference the gsutil and gcloud equivalents in GCS buckets and lifecycle and GCS object operations.

google_storage_bucket

The google_storage_bucket resource provisions a GCS bucket. Bucket names are globally unique across all of GCP. Changing location forces resource replacement — Terraform destroys the bucket and creates a new one, which means all objects are deleted. The Terraform service account needs roles/storage.admin on the project.

Force-replacement on location change

Changing the location argument on a google_storage_bucket triggers a destroy-then-create cycle. Terraform deletes all objects in the bucket before recreating it in the new location. This is unrecoverable without external backups.

Protect production buckets

Set force_destroy = false (default) so Terraform refuses to destroy a bucket that contains objects. Add lifecycle { prevent_destroy = true } for stateful buckets (data lake, state backend).

force_destroy = true

When force_destroy = true, terraform destroy silently deletes every object in the bucket before destroying the bucket itself. This is irreversible.

Reserve for ephemeral buckets only

Only set force_destroy = true on temporary, CI/CD, or test buckets that contain no valuable data.

ArgumentRequiredDescription
nameYesGlobally unique bucket name — convention: <project-id>-<env>-<purpose>
locationYesRegion (us-central1), dual-region, or multi-region (US, EU, ASIA) — forces replacement if changed
storage_classNoDefault class for new objects: STANDARD, NEARLINE, COLDLINE, ARCHIVE (default: STANDARD)
uniform_bucket_level_accessNotrue disables ACLs and enforces IAM-only access control (recommended)
force_destroyNofalse (default) — set true only for ephemeral buckets
public_access_preventionNo"enforced" blocks all public access at the bucket level
versioning.enabledNotrue enables object versioning — keeps history of every object version
lifecycle_ruleNoAuto-tiering and cleanup rules — see the Storage Class Decision Guide below

google_storage_bucket | General purpose (versioning + lifecycle)

A versatile bucket for application artifacts, exports, or backups. Versioning enabled to recover accidentally overwritten objects. Lifecycle rules automatically tier objects through STANDARD → NEARLINE (30 days) → COLDLINE (90 days) → ARCHIVE (365 days) and prune old non-current versions to keep only the 3 most recent.

Provision a versioned bucket with STANDARD → NEARLINE → COLDLINE → ARCHIVE tiering.

resource "google_storage_bucket" "general" {
  name                        = "${var.project_id}-${var.env}-general"
  location                    = var.region
  storage_class               = "STANDARD"
  uniform_bucket_level_access = true
  force_destroy               = false
 
  versioning {
    enabled = true
  }
 
  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "NEARLINE"
    }
    condition {
      age = 30
    }
  }
 
  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "COLDLINE"
    }
    condition {
      age = 90
    }
  }
 
  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "ARCHIVE"
    }
    condition {
      age = 365
    }
  }
 
  lifecycle_rule {
    action {
      type = "Delete"
    }
    condition {
      num_newer_versions = 3
      with_state         = "ARCHIVED"
    }
  }
 
  labels = {
    env         = var.env
    managed_by  = "terraform"
  }
}

google_storage_bucket | Landing zone (data ingestion)

Entry point for raw, untransformed data from external producers (APIs, SFTP uploads, IoT devices, event streams). Versioning disabled — raw files are append-only. Objects stay as STANDARD for 30 days (frequent access during initial processing), transition to NEARLINE, then are deleted after 180 days (adjust to your compliance window). Incomplete multipart uploads are aborted after 1 day to avoid paying for partial data. Co-locate this bucket in the same region as your Dataflow or Dataproc cluster.

Provision a raw data ingestion bucket with 180-day retention.

resource "google_storage_bucket" "landing_zone" {
  name                        = "${var.project_id}-${var.env}-landing"
  location                    = var.region
  storage_class               = "STANDARD"
  uniform_bucket_level_access = true
  force_destroy               = false
 
  versioning {
    enabled = false
  }
 
  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "NEARLINE"
    }
    condition {
      age = 30
    }
  }
 
  lifecycle_rule {
    action {
      type = "AbortIncompleteMultipartUpload"
    }
    condition {
      age = 1
    }
  }
 
  lifecycle_rule {
    action {
      type = "Delete"
    }
    condition {
      age = 180
    }
  }
 
  labels = {
    env         = var.env
    role        = "landing-zone"
    managed_by  = "terraform"
  }
}

google_storage_bucket | Terraform state backend

Stores Terraform remote state. This bucket must exist before the backend "gcs" configuration references it — bootstrap it with a separate root module or create it manually once. Key requirements: versioning on (recover corrupted state), public access prevention enforced, uniform IAM, lifecycle rule to prune old state versions.

Provision a state backend bucket with versioning and public access prevention.

resource "google_storage_bucket" "tf_state" {
  name                        = "${var.project_id}-terraform-state"
  location                    = "US"
  storage_class               = "STANDARD"
  uniform_bucket_level_access = true
  force_destroy               = false
 
  versioning {
    enabled = true
  }
 
  public_access_prevention = "enforced"
 
  lifecycle_rule {
    action {
      type = "Delete"
    }
    condition {
      num_newer_versions = 10
      with_state         = "ARCHIVED"
    }
  }
 
  lifecycle_rule {
    action {
      type = "AbortIncompleteMultipartUpload"
    }
    condition {
      age = 1
    }
  }
 
  labels = {
    purpose    = "terraform-state"
    managed_by = "terraform"
  }
}

Bootstrap workflow

  1. Create the state bucket manually or with a separate Terraform root module that uses local state
  2. Add the backend "gcs" block to your main project’s backend.tf
  3. Run terraform init — Terraform migrates the local state to GCS
  4. Enable state locking (automatic with GCS backend) to prevent concurrent modifications

Reference in backend.tf after the bucket is created:

Reference the state bucket in the backend configuration.

terraform {
  backend "gcs" {
    bucket = "<project-id>-terraform-state"
    prefix = "terraform/state"
  }
}

State bucket deletion

Losing or corrupting the state file means Terraform no longer knows what resources it manages. Recovery requires manual terraform import of every resource.

Always use remote state with versioning

GCS backend with versioning.enabled = true lets you roll back to a previous state version. Combined with public_access_prevention = "enforced", this is the safest state storage pattern.

google_storage_bucket | Data lake

Central repository for all analytical data — raw, curated, and aggregated layers. Versioning enabled to protect curated objects against accidental overwrites. Lifecycle rules tier objects through STANDARD → NEARLINE → COLDLINE → ARCHIVE using matches_storage_class conditions to ensure objects follow the full tiering path. Co-locate with your BigQuery dataset for zero-cost data loading.

GCS is a flat namespace — the / separator is a convention enforced by tools. Placeholder objects create the expected folder hierarchy (raw/, curated/, aggregated/).

Provision a multi-tiered data lake bucket with folder placeholders.

resource "google_storage_bucket" "data_lake" {
  name                        = "${var.project_id}-${var.env}-data-lake"
  location                    = var.region
  storage_class               = "STANDARD"
  uniform_bucket_level_access = true
  force_destroy               = false
 
  versioning {
    enabled = true
  }
 
  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "NEARLINE"
    }
    condition {
      age                        = 30
      matches_storage_class      = ["STANDARD"]
    }
  }
 
  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "COLDLINE"
    }
    condition {
      age                   = 90
      matches_storage_class = ["NEARLINE"]
    }
  }
 
  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "ARCHIVE"
    }
    condition {
      age                   = 365
      matches_storage_class = ["COLDLINE"]
    }
  }
 
  lifecycle_rule {
    action {
      type = "Delete"
    }
    condition {
      num_newer_versions = 5
      with_state         = "ARCHIVED"
    }
  }
 
  lifecycle_rule {
    action {
      type = "AbortIncompleteMultipartUpload"
    }
    condition {
      age = 2
    }
  }
 
  labels = {
    env        = var.env
    role       = "data-lake"
    managed_by = "terraform"
  }
}

Folder structure placeholders — create .keep objects to materialize the expected directory hierarchy:

Create .keep objects to materialize the data lake folder hierarchy.

resource "google_storage_bucket_object" "raw_placeholder" {
  name    = "raw/.keep"
  bucket  = google_storage_bucket.data_lake.name
  content = "placeholder"
}
 
resource "google_storage_bucket_object" "curated_placeholder" {
  name    = "curated/.keep"
  bucket  = google_storage_bucket.data_lake.name
  content = "placeholder"
}
 
resource "google_storage_bucket_object" "aggregated_placeholder" {
  name    = "aggregated/.keep"
  bucket  = google_storage_bucket.data_lake.name
  content = "placeholder"
}

google_storage_bucket_iam_member

Grants a single role to a single member on a bucket. This is additive — it does not affect other bindings on the same bucket. Use when different teams or Terraform modules manage different roles independently. This is the preferred IAM pattern in Terraform because it avoids unintentional permission revocation. See Service accounts and IAM for IAM concepts.

The member argument uses the format serviceAccount:, user:, group:, or domain: followed by the identity.

Grant read-only access to the data lake.

resource "google_storage_bucket_iam_member" "reader" {
  bucket = google_storage_bucket.data_lake.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${var.reader_service_account}"
}

Grant read-write access.

resource "google_storage_bucket_iam_member" "writer" {
  bucket = google_storage_bucket.data_lake.name
  role   = "roles/storage.objectAdmin"
  member = "serviceAccount:${var.writer_service_account}"
}

Grant bucket listing to the data team group.

resource "google_storage_bucket_iam_member" "group_list" {
  bucket = google_storage_bucket.data_lake.name
  role   = "roles/storage.legacyBucketReader"
  member = "group:${var.data_team_group}"
}
ArgumentRequiredDescription
bucketYesTarget bucket name
roleYesIAM role to grant — e.g., roles/storage.objectViewer, roles/storage.objectAdmin
memberYesIdentity — serviceAccount:email, user:email, group:email, or domain:example.com
RolePermissions
------
roles/storage.objectViewerList and get objects — cannot write or delete
roles/storage.objectAdminFull object CRUD — create, read, update, delete objects
roles/storage.legacyBucketReaderList bucket contents (gsutil ls) — does NOT allow object reads
roles/storage.adminFull bucket and object admin — use sparingly

google_storage_bucket_iam_binding

Manages the entire list of members for a given role on a bucket. This is authoritative for that role — any member with the specified role who is not listed in the members array will be removed by Terraform. Use when the team that owns the bucket also owns all access grants for a given role.

Authoritative binding removes unlisted members

iam_binding removes any member with the specified role that is not in the members list. If other Terraform modules or manual grants have added members for this role, they will be revoked on the next terraform apply.

Use iam_member when multiple teams manage access

If different teams or modules grant different roles on the same bucket, use google_storage_bucket_iam_member (additive) instead. Reserve iam_binding for cases where a single Terraform module is the authoritative source for a role.

Authoritatively set object viewers on the landing zone bucket.

resource "google_storage_bucket_iam_binding" "landing_readers" {
  bucket = google_storage_bucket.landing_zone.name
  role   = "roles/storage.objectViewer"
 
  members = [
    "serviceAccount:${var.etl_service_account}",
    "serviceAccount:${var.dataflow_service_account}",
    "group:${var.analytics_group}",
  ]
}
ArgumentRequiredDescription
bucketYesTarget bucket name
roleYesIAM role — authoritative for this role on this bucket
membersYesComplete list of identities that should have this role — all others are removed

iam_member vs iam_binding vs iam_policy

Use iam_member (additive) when multiple modules manage different roles independently. Use iam_binding (authoritative per role) when one module owns all grants for a specific role. Avoid iam_policy (authoritative for all roles) unless you are managing the bucket’s entire IAM policy in a single Terraform module — it will remove all roles not explicitly declared.

google_storage_bucket_object

Uploads a file or inline content to a GCS bucket. Use to seed buckets with configuration files, SQL scripts, requirements files, or any small artifact that Terraform should manage alongside the infrastructure. Avoid for large binary files — use a null_resource with gsutil cp for those. See GCS object operations for gsutil equivalents.

Use source for local files or content for inline strings — only one can be specified per resource.

google_storage_bucket_object | Upload a local file

Upload a file from the local filesystem (relative to the module root) to a GCS bucket. Set content_type correctly for browser-based downloads.

Upload a local config file to GCS.

resource "google_storage_bucket_object" "config" {
  name         = "config/app-config.json"
  bucket       = google_storage_bucket.general.name
  source       = "${path.module}/files/app-config.json"
  content_type = "application/json"
}

google_storage_bucket_object | Upload inline content

Create an object directly from a string — no local file needed. Useful for seed data, SQL schemas, or small configuration snippets.

Create a SQL seed file directly in GCS from inline content.

resource "google_storage_bucket_object" "seed_sql" {
  name         = "seeds/initial-schema.sql"
  bucket       = google_storage_bucket.general.name
  content      = <<-SQL
    CREATE TABLE IF NOT EXISTS events (
      id         SERIAL PRIMARY KEY,
      event_type VARCHAR(64) NOT NULL,
      created_at TIMESTAMP  NOT NULL DEFAULT NOW()
    );
  SQL
  content_type = "text/plain"
}
ArgumentRequiredDescription
nameYesObject name (path within the bucket) — / separators create virtual folders
bucketYesTarget bucket name
sourceNoLocal file path relative to the module root — mutually exclusive with content
contentNoInline string content — mutually exclusive with source
content_typeNoMIME type — set correctly for browser downloads (application/json, text/plain, etc.)
metadataNoCustom HTTP headers — e.g., { "Cache-Control" = "no-cache" }

google_storage_notification

Creates a notification configuration on a GCS bucket that publishes messages to a Pub/Sub topic when objects are created, deleted, or modified. This is the foundation for event-driven data ingestion pipelines — when a file lands in the landing zone, a Cloud Function or Dataflow job processes it immediately with no polling required. See Pub/Sub messaging for Pub/Sub concepts and Pub/Sub topics and subscriptions for gcloud equivalents.

GCS uses a project-level service account to publish notifications. This account must have roles/pubsub.publisher on the destination topic — the data source and IAM binding below handle this.

The depends_on is critical: if the notification is created before the IAM binding, GCS will fail to publish and events will be silently dropped.

First, create the Pub/Sub topic and grant GCS permission to publish:

Create a Pub/Sub topic for bucket event notifications.

resource "google_pubsub_topic" "bucket_events" {
  name    = "${var.env}-bucket-events"
  project = var.project_id
 
  message_retention_duration = "86600s"
}

Grant GCS permission to publish to the events topic.

data "google_storage_project_service_account" "gcs_account" {
  project = var.project_id
}
 
resource "google_pubsub_topic_iam_member" "gcs_publisher" {
  topic  = google_pubsub_topic.bucket_events.id
  role   = "roles/pubsub.publisher"
  member = "serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}"
}

Then create the notification:

Trigger a Pub/Sub message when objects are finalized in the landing zone.

resource "google_storage_notification" "landing_notify" {
  bucket         = google_storage_bucket.landing_zone.name
  payload_format = "JSON_API_V1"
  topic          = google_pubsub_topic.bucket_events.id
 
  event_types = ["OBJECT_FINALIZE"]
 
  object_name_prefix = "raw/"
 
  custom_attributes = {
    source      = "landing-zone"
    environment = var.env
  }
 
  depends_on = [google_pubsub_topic_iam_member.gcs_publisher]
}
ArgumentRequiredDescription
bucketYesBucket to watch for events
payload_formatYesJSON_API_V1 (full object metadata) or NONE (no payload)
topicYesDestination Pub/Sub topic ID
event_typesNoOBJECT_FINALIZE (upload complete), OBJECT_DELETE, OBJECT_ARCHIVE, OBJECT_METADATA_UPDATE
object_name_prefixNoFilter — only notify for objects matching this prefix (e.g., raw/)
custom_attributesNoKey-value pairs added to every Pub/Sub message — helps consumers route events
depends_onNoMust reference the IAM binding to ensure GCS can publish before the notification is created

Finally, create a pull subscription for consumers:

Create a pull subscription for landing zone events.

resource "google_pubsub_subscription" "landing_events_sub" {
  name    = "${var.env}-landing-events-sub"
  topic   = google_pubsub_topic.bucket_events.id
  project = var.project_id
 
  ack_deadline_seconds       = 60
  message_retention_duration = "3600s"
  retain_acked_messages      = false
 
  expiration_policy {
    ttl = "86400s"
  }
 
  retry_policy {
    minimum_backoff = "10s"
    maximum_backoff = "300s"
  }
}
ArgumentRequiredDescription
ack_deadline_secondsNoSeconds a consumer has to ACK before message is redelivered (default: 10)
message_retention_durationNoHow long unACKed messages are retained (default: 604800s / 7 days)
retain_acked_messagesNofalse (default) discards ACKed messages immediately
expiration_policy.ttlNoSubscription expires after this duration of inactivity — set "" for no expiration
retry_policy.minimum_backoffNoMinimum wait before redelivering a NACKed message
retry_policy.maximum_backoffNoMaximum wait before redelivering (exponential backoff)

Event-driven ingestion pipeline

How the bucket notification, Pub/Sub topic, subscription, and consumer fit together:

Visualize the event flow from GCS object upload through Pub/Sub to consumer processing.


sequenceDiagram
    participant P as Producer<br/>(API, SFTP, IoT)
    participant B as GCS Bucket<br/>(landing zone)
    participant N as Notification<br/>(OBJECT_FINALIZE)
    participant T as Pub/Sub Topic
    participant S as Subscription
    participant C as Consumer<br/>(Cloud Function,<br/>Dataflow)

    P->>B: Upload file to raw/
    B->>N: Object finalized event
    N->>T: Publish message (JSON_API_V1)
    T->>S: Deliver to subscription
    S->>C: Pull / push message
    C->>C: Process file
    C->>S: ACK message

Variables Reference

The blocks above assume the following input variables. Define them in your variables.tf:

Declare all input variables referenced by the blocks in this file.

variable "project_id" {
  description = "GCP project ID"
  type        = string
}
 
variable "project_number" {
  description = "GCP project number (numeric)"
  type        = string
}
 
variable "env" {
  description = "Environment name: dev, staging, or prod"
  type        = string
}
 
variable "region" {
  description = "GCP region (e.g. us-central1)"
  type        = string
  default     = "us-central1"
}
 
variable "zone" {
  description = "GCP zone (e.g. us-central1-a)"
  type        = string
  default     = "us-central1-a"
}
 
variable "vpc_network" {
  description = "VPC network self_link or name"
  type        = string
}
 
variable "subnetwork" {
  description = "Subnetwork self_link or name"
  type        = string
}
 
variable "db_service_account_email" {
  description = "Service account email for the database VM"
  type        = string
}
 
variable "airflow_service_account_email" {
  description = "Service account email for the Airflow VM"
  type        = string
}
 
variable "airflow_image" {
  description = "Docker image for Airflow (full registry path)"
  type        = string
  default     = "apache/airflow:2.9.0"
}
 
variable "airflow_db_conn" {
  description = "SQLAlchemy connection string for Airflow metadata DB"
  type        = string
  sensitive   = true
}
 
variable "worker_service_account_email" {
  description = "Service account email for spot worker VMs"
  type        = string
}
 
variable "worker_startup_script" {
  description = "Startup script for the generic worker VM"
  type        = string
  default     = "#!/bin/bash\necho 'worker ready'"
}
 
variable "app_service_account_email" {
  description = "Service account email for app servers in the MIG"
  type        = string
}
 
variable "app_startup_script" {
  description = "Startup script for app server VMs in the MIG"
  type        = string
}
 
variable "reader_service_account" {
  description = "Service account email for read-only bucket access"
  type        = string
}
 
variable "writer_service_account" {
  description = "Service account email for read-write bucket access"
  type        = string
}
 
variable "data_team_group" {
  description = "Google group email for the data team"
  type        = string
}
 
variable "etl_service_account" {
  description = "Service account email for the ETL pipeline"
  type        = string
}
 
variable "dataflow_service_account" {
  description = "Service account email for Dataflow workers"
  type        = string
}
 
variable "analytics_group" {
  description = "Google group email for analytics users"
  type        = string
}
VariableTypeDefaultUsed by
project_idstringResource names, bucket names
project_numberstringService agent email for VM scheduling
envstringPrefix/label on all resources (dev, staging, prod)
regionstringus-central1Regional resources (templates, policies, buckets)
zonestringus-central1-aZonal resources (VMs, disks, MIGs)
vpc_networkstringnetwork_interface blocks
subnetworkstringnetwork_interface blocks
airflow_db_connstring (sensitive)Airflow metadata DB connection string

Quick Reference

Summary of all Terraform resource types covered in this file, their primary use cases, and the key arguments that differentiate each configuration.

Resource Cheat Sheet

ResourceUse CaseKey Arguments
google_compute_instanceSingle VMmachine_type, boot_disk, network_interface, scheduling
google_compute_diskStandalone data disktype, size, zone
google_compute_attached_diskAttach disk to VMdisk, instance, mode
google_compute_snapshotManual point-in-time backupsource_disk, storage_locations
google_compute_resource_policyScheduled snapshots or VM start/stopsnapshot_schedule_policy or instance_schedule_policy
google_compute_instance_templateReusable VM blueprint for MIGsdisk, network_interface, metadata
google_compute_instance_group_managerManaged fleet of identical VMsversion, auto_healing_policies, update_policy
google_compute_autoscalerCPU-based scaling for a MIGautoscaling_policy.cpu_utilization.target
google_storage_bucketAny GCS bucketlocation, uniform_bucket_level_access, lifecycle_rule
google_storage_bucket_iam_memberAdditive IAM grantbucket, role, member
google_storage_bucket_iam_bindingAuthoritative IAM for a rolebucket, role, members
google_storage_bucket_objectUpload file or inline contentname, bucket, source or content
google_storage_notificationPub/Sub trigger on bucket eventsevent_types, topic, object_name_prefix

Storage Class Decision Guide

GCS offers four storage classes with different cost profiles. Objects transition between classes via lifecycle_rule blocks. Minimum storage duration means you are charged for at least that duration even if the object is deleted or moved sooner.

ClassMin Storage DurationRetrieval CostUse When
STANDARDNoneFreeAccessed daily or hourly
NEARLINE30 daysLowAccessed < once per month
COLDLINE90 daysMediumAccessed < once per quarter
ARCHIVE365 daysHighAccessed < once per year (compliance, DR)

Match lifecycle rules to access patterns

Lifecycle rules should align to your actual access patterns. Over-tiering (moving to ARCHIVE too early) causes high retrieval costs when data is accessed. Under-tiering (keeping everything in STANDARD) means paying premium storage rates unnecessarily. Use Cloud Monitoring storage metrics to validate your tiering assumptions.

Illustrate the GCS storage class tiering path and minimum storage duration thresholds.


flowchart LR
    S["STANDARD<br/>no min duration<br/>free retrieval"]
    N["NEARLINE<br/>30-day min<br/>low retrieval cost"]
    C["COLDLINE<br/>90-day min<br/>medium retrieval cost"]
    A["ARCHIVE<br/>365-day min<br/>high retrieval cost"]
    D["Delete"]

    S -->|"age ≥ 30d"| N
    N -->|"age ≥ 90d"| C
    C -->|"age ≥ 365d"| A
    A -->|"retention expired"| D