Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Google Cloud Compute Instance Terraform Module

Creates and manages a single Google Compute Engine VM instance (google_compute_instance) with no external IP and full Shielded VM protections by default. Targets hashicorp/google ~> 7.0, Terraform >= 1.12.0.

Terraform Google Provider Module Version Module Type Resources Posture


🧩 Overview

  • πŸ–₯️ Creates one google_compute_instance β€” a Google Compute Engine (GCE) VM instance β€” the keystone compute primitive most other GCP workload patterns (GKE nodes aside) ultimately run on top of.
  • πŸ”’ No access_config {} block (external IP) is ever rendered unless the caller explicitly sets assign_external_ip = true; Shielded VM (secure boot, vTPM, integrity monitoring) is enabled by default.
  • πŸ”Œ Consumes a subnetwork self_link from terraform-google-vpc-network and, optionally, a service account email from terraform-google-service-account β€” both as plain cross-module identity strings, never invented inside this module.
  • 🧱 Standalone by design β€” a VM instance's boot disk and network interfaces are nested configuration blocks on the instance itself, not a separate for_each-managed child collection.

πŸ’‘ Why it matters: a misconfigured default here β€” an accidental public IP, a disabled Shielded VM protection, or a boot-image change that silently recreates a production instance β€” is the kind of mistake that is expensive to notice after the fact. This module makes the safe path the default path and documents every force-new field before a caller hits it in apply.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


πŸ—ΊοΈ Where this fits

graph LR
 PS["terraform-google-project-services"]:::external
 VPC["terraform-google-vpc-network"]:::keystoneSibling
 SA["terraform-google-service-account"]:::keystoneSibling
 KMS["terraform-google-kms-keyring"]:::external
 CI["terraform-google-compute-instance"]:::thisModule
 FW["terraform-google-firewall-policy"]:::sibling
 MON["terraform-google-monitoring-alert-policy"]:::sibling

 PS -- "enables compute.googleapis.com" --> CI
 VPC -- "subnetwork self_link consumed by network_interfaces[*].subnetwork" --> CI
 SA -- "email consumed by service_account_email (optional)" --> CI
 KMS -- "crypto key self_link consumed by boot_disk.kms_key_self_link (optional CMEK)" --> CI
 CI -- "tags consumed as target_tags" --> FW
 CI -- "instance id / self_link referenced by alert scope (future)" --> MON

 classDef thisModule fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:1px;
 classDef keystoneSibling fill:#174EA6,color:#ffffff,stroke:#174EA6,stroke-width:1px;
 classDef sibling fill:#E8EAED,color:#202124,stroke:#9AA0A6,stroke-width:1px;
 classDef external fill:#E8EAED,color:#202124,stroke:#9AA0A6,stroke-width:1px,stroke-dasharray: 3 3;
Loading

terraform-google-project-services (external, dashed) must have already enabled compute.googleapis.com before this module applies. terraform-google-vpc-network and terraform-google-service-account are this module's two direct upstream identity suppliers (subnetwork and, optionally, service account email). terraform-google-kms-keyring is an optional upstream supplier for CMEK boot/attached-disk encryption. Downstream, terraform-google-firewall-policy targets this instance via network tags (not a direct resource reference), and a future terraform-google-monitoring-alert-policy composition could scope an alert to this instance's id/self_link.

Validated via the Mermaid Chart MCP before embedding.


🧬 What this builds

graph LR
 subgraph Inputs
 A["var.name / machine_type / zone"]
 B["var.boot_disk"]
 C["var.network_interfaces"]
 D["var.assign_external_ip / external_access_config"]
 E["var.service_account_email / scopes"]
 F["var.scheduling"]
 G["var.shielded_instance_config"]
 H["var.guest_accelerators"]
 I["var.labels / timeouts"]
 end

 R["google_compute_instance.this"]:::thisModule

 A --> R
 B --> R
 C --> R
 D --> R
 E --> R
 F --> R
 G --> R
 H --> R
 I --> R

 R --> O1["output: id, self_link, name"]
 R --> O2["output: instance_id"]
 R --> O3["output: internal_ip"]
 R --> O4["output: external_ip (conditional)"]

 classDef thisModule fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:1px;
Loading

Resource inventory (1 resource): google_compute_instance.this, with a boot_disk nested block (incl. initialize_params), one-or-more network_interface nested blocks (each optionally carrying an access_config and alias_ip_range), a service_account block (always rendered), scheduling, shielded_instance_config, zero-or-more guest_accelerator and attached_disk blocks, an optional advanced_machine_features block, and an optional timeouts block. No child resources, no for_each-managed collection β€” a single keystone resource with rich nested-block configuration.

Validated via the Mermaid Chart MCP before embedding.


βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/google provider ~> 7.0
Provider block None β€” the caller configures google (project, region/zone, auth)

Schema notes that bite (verified against hashicorp/google v7.39.0, cross-checked via the schema-JSON fallback at C:\tmp\gcp_schema\schema.json β€” the live provider documentation's chunked docs mixed in google_compute_instance_template content for several nested blocks sharing identical names with this resource):

  • ⚠️ boot_disk.initialize_params.image is force-new for the entire instance. There is no in-place OS patch path β€” a base-image update means instance recreation. Only a size increase on an already-attached disk avoids replacement.
  • name, zone, and hostname are force-new, per the house-wide note that these fields are force-new on nearly every GCP resource.
  • machine_type, service_account_email/scopes, min_cpu_platform, and shielded_instance_config changes on an existing instance all require var.allow_stopping_for_update = true (or a TERMINATED desired_status) β€” otherwise the apply fails outright instead of silently recreating or stopping the instance.
  • metadata_startup_script is force-new on change; the startup-script key inside var.metadata is not β€” the two mechanisms are mutually exclusive and behave differently on update.
  • GPU accelerators require scheduling.on_host_maintenance = "TERMINATE" β€” a live API constraint this module's validate/fmt gate cannot catch; an incompatible combination fails only at apply.
  • Shielded VM requires a Shielded-VM-capable boot image. An incompatible image fails only at apply, never at plan.
  • service_account {} is always rendered by this module, even when var.service_account_email is null, so that var.service_account_scopes's empty-list secure default actually takes effect β€” omitting the block entirely lets GCP attach its own broader historical default scope set instead.
  • deletion_protection defaults to true. A destroy requires flipping it to false in an explicit prior apply β€” this is GCP API-enforced, not a Terraform-only lifecycle.prevent_destroy guard.
  • initialize_params.type (disk type) is deliberately not validated against a closed list. GCP adds new disk-type families (Hyperdisk generations) over time; a hardcoded list would reject a legitimately new value until this module is updated.

πŸ”‘ Required IAM Roles

  • roles/compute.instanceAdmin.v1 on the target project β€” create, update, and delete Compute Engine instances and their attached disks.
  • roles/iam.serviceAccountUser on the specific service account, only when var.service_account_email is set β€” required to attach an existing identity to an instance. Not needed when service_account_email is left null.

☁️ GCP Prerequisites

  • compute.googleapis.com enabled on the target project (via terraform-google-project-services, applied before this module).
  • If assign_external_ip = true: confirm constraints/compute.vmExternalIpAccess does not deny external IPs at the org/folder level β€” undetectable at plan time, only at apply.
  • If any kms_key_self_link is set: the referenced Cloud KMS CryptoKey must already exist, and the Compute Engine service agent needs roles/cloudkms.cryptoKeyEncrypterDecrypter on it.

πŸ“ Module Structure

terraform-google-compute-instance/
β”œβ”€β”€ providers.tf # required_providers (hashicorp/google ~> 7.0) + required_version β€” no provider {} block
β”œβ”€β”€ variables.tf # name, machine_type, zone, boot_disk, network_interfaces, assign_external_ip,
β”‚ # external_access_config, service_account_email/scopes, scheduling,
β”‚ # shielded_instance_config, guest_accelerators, advanced_machine_features,
β”‚ # attached_disks, labels, timeouts, and remaining scalar arguments
β”œβ”€β”€ main.tf # google_compute_instance.this β€” the sole resource
β”œβ”€β”€ outputs.tf # id, self_link, name, instance_id, internal_ip, external_ip
β”œβ”€β”€ README.md # this file
β”œβ”€β”€ SCOPE.md # lightweight cross-module contract
└── examples/ # runnable example matching the Quick Start below

βš™οΈ Quick Start

module "app_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "app-instance-01"
  machine_type = "e2-medium"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
  ]
}

The caller's root module configures the google provider (project, region/zone, and authentication via ADC, Workload Identity Federation, or a service account key supplied out-of-band) β€” this module accepts none of those as variables.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
network_interfaces[*].subnetwork string (self_link) terraform-google-vpc-network
service_account_email string (optional) terraform-google-service-account
boot_disk.kms_key_self_link / attached_disks[*].kms_key_self_link string (optional, self_link) terraform-google-kms-keyring
attached_disks[*].source string (self_link) A pre-existing google_compute_disk

Emits

Output Description Consumed by
id Terraform-internal resource identifier Any module needing the Terraform reference
self_link The instance's URI (self_link) Modules needing the full API URL form
name Instance name Diagnostic/reference use
instance_id GCP's server-assigned numeric instance identifier Diagnostic/reference use
internal_ip Private IP of network_interfaces[0] DNS records, firewall composition, service discovery
external_ip Public IP of network_interfaces[0] (or null) DNS records, allowlisting, external monitoring

πŸ“š Example Library

1 Β· Minimal call
module "app_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "app-instance-01"
  machine_type = "e2-medium"

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

  network_interfaces = [
    { subnetwork = "https://www.googleapis.com/compute/v1/projects/casey-prod-networking/regions/us-east1/subnetworks/app-subnet-use1" }
  ]
}

πŸ’‘ The minimal call is the safe, complete path: no external IP, Shielded VM fully enabled, empty service-account scopes, deletion_protection = true β€” every secure default takes effect without the caller typing anything extra.

2 Β· Shielded VM defaults left in place
module "app_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "shielded-app-instance"
  machine_type = "e2-medium"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
  ]

  # shielded_instance_config intentionally omitted β€” secure boot, vTPM, and
  # integrity monitoring are all enabled by this module's own default.
}

πŸ”’ This example shows the default explicitly by omission β€” do not disable any shielded_instance_config field without a documented compatibility reason (e.g. a legacy image that predates Shielded VM support).

3 Β· Assigning an external IP
module "bastion_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "bastion-01"
  machine_type = "e2-small"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["mgmt-subnet-use1"] }
  ]

  assign_external_ip = true

  external_access_config = {
    network_tier = "PREMIUM"
  }
}

⚠️ Confirm constraints/compute.vmExternalIpAccess is not set to deny at the org/folder level β€” this module's validate/plan gate cannot detect that constraint; only apply surfaces it.

4 Β· Attaching a service account with least-privilege scopes
module "app_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "orders-api-instance"
  machine_type = "e2-medium"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
  ]

  service_account_email = module.orders_api_sa.email
  # service_account_scopes intentionally left at its default `[]` β€” access is
  # granted via terraform-google-project-iam-bindings, not legacy instance scopes.
}

πŸ’‘ service_account_scopes defaults to [] specifically so IAM role bindings (additive, scoped to this identity) are the actual access-control mechanism β€” not broad legacy OAuth scopes.

5 Β· Legacy workload requiring an explicit OAuth scope
module "legacy_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "legacy-batch-job"
  machine_type = "e2-standard-4"

  boot_disk = {
    initialize_params = {
      image = "debian-cloud/debian-11"
      size  = 50
    }
  }

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["batch-subnet-use1"] }
  ]

  service_account_email  = module.legacy_batch_sa.email
  service_account_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
}

⚠️ Explicit opt-out from this module's secure default. Only use broad legacy scopes for a workload that genuinely cannot be migrated to IAM-role-based access in the near term.

6 Β· GPU-attached instance
module "ml_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "ml-training-01"
  machine_type = "n1-standard-8"
  zone         = "us-east1-b"

  boot_disk = {
    initialize_params = {
      image = "debian-cloud/debian-12"
      size  = 100
      type  = "pd-ssd"
    }
  }

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["ml-subnet-use1"] }
  ]

  guest_accelerators = [
    { type = "nvidia-tesla-t4", count = 1 }
  ]

  scheduling = {
    on_host_maintenance = "TERMINATE"
  }
}

⚠️ GPU accelerators require on_host_maintenance = "TERMINATE" per the live GCP API β€” this module does not enforce the combination at plan time; an incompatible value fails only at apply.

7 Β· Preemptible (SPOT) scheduling configuration
module "batch_worker" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "batch-worker-01"
  machine_type = "e2-standard-4"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["batch-subnet-use1"] }
  ]

  scheduling = {
    preemptible                 = true
    automatic_restart           = false
    provisioning_model          = "SPOT"
    instance_termination_action = "STOP"
  }
}

ℹ️ automatic_restart must be false for a preemptible/SPOT instance β€” GCP will not restart a preempted instance automatically regardless of this setting, but the API still expects it set to false for consistency.

8 Β· Multiple network_interface entries
module "dual_homed_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "dual-homed-appliance"
  machine_type = "n2-standard-4"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] },
    { subnetwork = module.vpc_network.subnetwork_self_links["data-subnet-use1"] }
  ]
}

ℹ️ Only network_interfaces[0] (nic0) is ever eligible for assign_external_ip from this module β€” network_interfaces[1] (nic1) and beyond are always private-only.

9 Β· Boot disk sizing and disk type
module "database_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "db-instance-01"
  machine_type = "n2-standard-8"

  boot_disk = {
    initialize_params = {
      image = "debian-cloud/debian-12"
      size  = 200
      type  = "pd-ssd"
    }
  }

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["data-subnet-use1"] }
  ]
}

⚠️ initialize_params.image is force-new for the entire instance β€” resizing size alone is the only in-place boot-disk change; changing image or type is not.

10 Β· CMEK-encrypted boot disk
module "regulated_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "regulated-workload-01"
  machine_type = "n2-standard-4"

  boot_disk = {
    kms_key_self_link = module.kms_keyring.crypto_key_ids["compute-instance-key"]
    initialize_params = {
      image = "debian-cloud/debian-12"
      size  = 50
    }
  }

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["regulated-subnet-use1"] }
  ]
}

πŸ”’ kms_key_self_link is accepted as an optional variable and never defaulted to a specific key, per this module suite's CMEK rule extended to this module's boot disk encryption.

11 Β· Additional attached data disk
resource "google_compute_disk" "data" {
  name = "app-instance-data"
  zone = "us-east1-b"
  size = 100
  type = "pd-balanced"
}

module "app_instance_with_data_disk" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "app-instance-with-data"
  machine_type = "e2-standard-4"
  zone         = "us-east1-b"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
  ]

  attached_disks = [
    { source = google_compute_disk.data.self_link, device_name = "data" }
  ]
}

ℹ️ This module attaches an existing google_compute_disk by reference β€” it does not create the disk resource itself; that is a separate concern outside this module's scope.

12 Β· Advanced machine features
module "nested_virt_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "ci-runner-nested-virt"
  machine_type = "n2-standard-8"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["ci-subnet-use1"] }
  ]

  advanced_machine_features = {
    enable_nested_virtualization = true
    threads_per_core             = 1 # disable SMT
  }
}

ℹ️ Leaving advanced_machine_features = null (the default) renders no advanced_machine_features {} block at all β€” this example shows the opt-in path.

13 Β· Custom labels and timeouts
module "app_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "app-instance-labeled"
  machine_type = "e2-medium"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
  ]

  labels = {
    environment = "prod"
    owning_team = "platform"
  }

  timeouts = {
    create = "15m"
  }
}

ℹ️ Label keys/values must be lowercase letters, numbers, underscores, or hyphens (63 char max) β€” enforced at plan time via this module's validation {} block.

14 Β· Static internal IP and startup script
module "app_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "app-instance-static-ip"
  machine_type = "e2-medium"
  hostname     = "app01.internal.example.com"

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

  network_interfaces = [
    {
      subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"]
      network_ip = "10.0.1.25"
    }
  ]

  metadata_startup_script = <<-EOT
 #!/bin/bash
 apt-get update && apt-get install -y nginx
 EOT
}

⚠️ metadata_startup_script is force-new on change (re-running the script re-creates the instance) β€” distinct from the startup-script key inside var.metadata, which re-runs in place.

15 Β· πŸ—οΈ End-to-end composition
module "project_services" {
  source = "git::https://github.com/microsoftexpert/terraform-google-project-services.git?ref=v1.0.0"

  # Enables compute.googleapis.com (and any other APIs the composition needs).
}

module "vpc_network" {
  source = "git::https://github.com/microsoftexpert/terraform-google-vpc-network.git?ref=v1.0.0"

  network_name = "casey-prod-networking"
  routing_mode = "REGIONAL"

  subnetworks = {
    "app-subnet-use1" = {
      ip_cidr_range = "10.0.1.0/24"
      region        = "us-east1"
    }
  }

  depends_on = [module.project_services]
}

module "orders_api_sa" {
  source = "git::https://github.com/microsoftexpert/terraform-google-service-account.git?ref=v1.0.0"

  account_id   = "orders-api"
  display_name = "Orders API runtime identity"

  depends_on = [module.project_services]
}

module "orders_api_iam" {
  source = "git::https://github.com/microsoftexpert/terraform-google-project-iam-bindings.git?ref=v1.0.0"

  bindings = {
    "orders-api-secret-accessor" = {
      role   = "roles/secretmanager.secretAccessor"
      member = module.orders_api_sa.member
    }
  }
}

module "firewall_policy" {
  source = "git::https://github.com/microsoftexpert/terraform-google-firewall-policy.git?ref=v1.0.0"

  network = module.vpc_network.self_link

  firewall_rules = {
    "allow-orders-api-http" = {
      direction     = "INGRESS"
      priority      = 1000
      source_ranges = ["10.0.0.0/8"]
      target_tags   = ["orders-api"]
      allow = [
        { protocol = "tcp", ports = ["8080"] }
      ]
    }
  }
}

module "orders_api_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-google-compute-instance.git?ref=v1.0.0"

  name         = "orders-api-instance"
  machine_type = "e2-medium"

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

  network_interfaces = [
    { subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
  ]

  service_account_email = module.orders_api_sa.email
  tags                  = ["orders-api"]

  labels = {
    environment = "prod"
    owning_team = "platform"
  }

  depends_on = [module.orders_api_iam]
}

πŸ’‘ This wires terraform-google-project-services β†’ terraform-google-vpc-network + terraform-google-service-account β†’ terraform-google-project-iam-bindings β†’ terraform-google-firewall-policy β†’ terraform-google-compute-instance in dependency order: APIs enabled first, then networking and identity created in parallel, then IAM roles granted, then the firewall rule and the instance itself β€” the instance's service_account_email and network_interfaces[*].subnetwork both come from sibling module outputs, never a hardcoded string.

⚠️ IAM role grants can take up to ~60 seconds to propagate; the explicit depends_on on orders_api_iam orders the graph correctly, but a transient permission-denied error on first apply is still possible β€” see Troubleshooting.


πŸ“₯ Inputs

Variable Type Required Default Notes
name string Yes β€” Force-new
machine_type string Yes β€” Requires allow_stopping_for_update to resize in place
zone string No null Force-new; inherits provider zone if unset
description string No null
boot_disk object({...}) Yes β€” initialize_params.image is force-new for the whole instance
network_interfaces list(object({...})) Yes β€” At least one entry; entry 0 is the primary interface
assign_external_ip bool No false Secure default β€” gates access_config on entry 0 only
external_access_config object({...}) No {} Ignored unless assign_external_ip = true
service_account_email string No null Source from terraform-google-service-account
service_account_scopes list(string) No [] Secure default β€” use IAM roles, not legacy scopes
tags list(string) No [] Network tags, consumed by firewall target_tags
metadata map(string) No {}
metadata_startup_script string No null Force-new on change
can_ip_forward bool No false
allow_stopping_for_update bool No false Opt-in to auto-stop for certain in-place updates
desired_status string No null RUNNING | SUSPENDED | TERMINATED
deletion_protection bool No true Secure default
deletion_policy string No null DELETE | PREVENT | ABANDON
min_cpu_platform string No null Requires allow_stopping_for_update to change in place
resource_policies list(string) No [] Max 1 supported by the GCP API
key_revocation_action_type string No null STOP | NONE
enable_display bool No false
scheduling object({...}) No {}
shielded_instance_config object({...}) No {} (all enabled) Secure default
guest_accelerators list(object({...})) No [] Requires scheduling.on_host_maintenance = "TERMINATE"
advanced_machine_features object({...}) No null No block rendered when null
attached_disks list(object({...})) No [] References pre-existing google_compute_disk resources
labels map(string) No {} GCP label-format validated
timeouts object({...}) No null create/update/delete all supported
Full variable schemas
variable "boot_disk" {
  type = object({
    auto_delete             = optional(bool, true)
    device_name             = optional(string)
    mode                    = optional(string, "READ_WRITE")
    source                  = optional(string)
    disk_encryption_key_raw = optional(string)
    kms_key_self_link       = optional(string)
    initialize_params = optional(object({
      image  = optional(string)
      size   = optional(number)
      type   = optional(string)
      labels = optional(map(string), {})
    }), {})
  })
}

variable "network_interfaces" {
  type = list(object({
    subnetwork         = string
    network            = optional(string)
    subnetwork_project = optional(string)
    network_ip         = optional(string)
    nic_type           = optional(string)
    stack_type         = optional(string)
    queue_count        = optional(number)
    alias_ip_ranges = optional(list(object({
      ip_cidr_range         = string
      subnetwork_range_name = optional(string)
    })), [])
  }))
}

variable "external_access_config" {
  type = object({
    nat_ip                 = optional(string)
    network_tier           = optional(string, "PREMIUM")
    public_ptr_domain_name = optional(string)
  })
  default = {}
}

variable "scheduling" {
  type = object({
    preemptible                 = optional(bool, false)
    automatic_restart           = optional(bool, true)
    on_host_maintenance         = optional(string)
    provisioning_model          = optional(string)
    instance_termination_action = optional(string)
  })
  default = {}
}

variable "shielded_instance_config" {
  type = object({
    enable_secure_boot          = optional(bool, true)
    enable_vtpm                 = optional(bool, true)
    enable_integrity_monitoring = optional(bool, true)
  })
  default = {}
}

variable "guest_accelerators" {
  type = list(object({
    type  = string
    count = number
  }))
  default = []
}

variable "advanced_machine_features" {
  type = object({
    enable_nested_virtualization = optional(bool, false)
    threads_per_core             = optional(number)
    visible_core_count           = optional(number)
    turbo_mode                   = optional(string)
    performance_monitoring_unit  = optional(string)
    enable_uefi_networking       = optional(bool, false)
  })
  default = null
}

variable "attached_disks" {
  type = list(object({
    source                  = string
    device_name             = optional(string)
    mode                    = optional(string, "READ_WRITE")
    disk_encryption_key_raw = optional(string)
    kms_key_self_link       = optional(string)
  }))
  default = []
}

variable "labels" {
  type    = map(string)
  default = {}
  # keys/values: ^[a-z][a-z0-9_-]{0,62}$ / ^[a-z0-9_-]{0,63}$ enforced via validation {}
}

variable "timeouts" {
  type = object({
    create = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

See variables.tf for every remaining scalar variable (name, machine_type, zone, description, service_account_email, service_account_scopes, tags, metadata, metadata_startup_script, can_ip_forward, allow_stopping_for_update, desired_status, deletion_protection, deletion_policy, min_cpu_platform, resource_policies, key_revocation_action_type, enable_display) and every validation {} block's exact condition.


🧾 Outputs

Output Description
id Terraform-internal resource identifier
self_link The instance's URI (self_link)
name Instance name
instance_id GCP's server-assigned numeric instance identifier (distinct from id)
internal_ip Private IP of network_interfaces[0]
external_ip Public IP of network_interfaces[0] when assign_external_ip = true; null otherwise

None of these outputs are secret-bearing; no sensitive = true is applied to any of them.


🧠 Architecture Notes

  • boot_disk.initialize_params.image is force-new for the whole instance (the mandatory gotcha this module was built around documenting). A change to the boot image is not an in-place OS patch β€” GCP recreates the entire instance. Only a size increase on an already-attached disk avoids replacement; image and type changes do not have an equivalent in-place path.
  • External IP applies only to network_interfaces[0]. The nested dynamic "access_config" block in main.tf gates on both var.assign_external_ip and the outer dynamic block's own iterator index (network_interface.key == 0) β€” additional interfaces are always private-only from this module.
  • service_account {} is always rendered, never omitted. This is deliberate: it's the only way var.service_account_scopes's empty-list secure default actually takes effect, since omitting the block entirely lets GCP attach its own broader historical default scope set to the default Compute Engine service account instead.
  • Several nested blocks the live schema exposes are deliberately out of scope for v1.0.0 β€” reservation_affinity, confidential_instance_config, network_performance_config, scratch_disk, params.resource_manager_tags, sole-tenant/advanced-SPOT scheduling fields (node_affinities, min_node_cpus, availability_domain, local_ssd_recovery_timeout, max_run_duration, on_instance_stop_action), and several network_interface extras (ipv6_access_config, mac_address, vlan, security_policy, network_attachment). This mirrors this module suite's "scope a first slice and expand deliberately" principle applied at the nested-block level rather than the module-catalog level β€” see variables.tf's file header for the full list. None of these are silently accepted and dropped; there is simply no variable exposing them yet.
  • allow_stopping_for_update gates several in-place changes. machine_type, service_account_email/scopes, min_cpu_platform, and shielded_instance_config all require it (or a TERMINATED desired_status) to change without a failed apply.
  • network_interfaces is a list, not a for_each-keyed map. Ordering is semantically meaningful here (GCE assigns device index by list position; this module's IP outputs and external IP gate both key off index 0), which is why this is the one repeating structure in the module that intentionally departs from the "keyed map" convention used for genuinely order-independent child collections elsewhere in this library.

🧱 Design Principles

Concern Secure default Opt-out (explicit)
VM instance external IP No access_config {} block emitted unless assign_external_ip = true; applies to network_interfaces[0] only Caller sets assign_external_ip = true
Shielded VM (secure boot / vTPM / integrity monitoring) All three enabled by default via shielded_instance_config Caller disables per-flag, with a compatibility reason documented in the calling code
Service account OAuth scopes (this module's extension) service_account_scopes = [] β€” access granted via IAM role bindings on the attached identity, not legacy instance scopes Caller supplies an explicit scopes list (e.g. cloud-platform) for legacy workloads
Deletion protection deletion_protection = true Caller sets false explicitly; a destroy still requires a prior apply with the flag flipped
Boot / attached-disk encryption (CMEK) kms_key_self_link accepted as an optional variable, never defaulted to a specific key Caller supplies a terraform-google-kms-keyring crypto key self_link
In-place update via auto-stop (this module's extension) allow_stopping_for_update = false β€” Terraform never auto-stops a running instance without explicit opt-in Caller sets true to permit machine_type/service-account/shielded-config changes in place
IAM grants for the attached identity Not managed by this module β€” grant roles additively via terraform-google-project-iam-bindings, never an authoritative _iam_policy N/A β€” authoritative IAM policy is intentionally excluded from this library

πŸš€ Runbook

cd terraform-google-compute-instance
terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module source to ?ref=v1.0.0 β€” never a branch. This library is plan-only from an authoring session; a human applies from CI with valid Workload Identity Federation or ADC credentials.


πŸ§ͺ Testing

terraform validate confirms internal type and reference consistency β€” every validation {} block (disk mode, network-interface nic_type/stack_type, scheduling enums, label format, and more) fires at plan time, before any GCP API call. terraform fmt -check confirms canonical formatting. Neither can catch GCP API-level rejections: quota limits, the constraints/compute.vmExternalIpAccess org policy, a boot image incompatible with Shielded VM, a GPU/on_host_maintenance mismatch, or an in-place update attempted without allow_stopping_for_update. Only a real terraform plan/apply against a live project, with valid credentials, exercises those paths β€” that step belongs to the consuming CI pipeline, not this authoring session.


πŸ’¬ Example Output

$ terraform output

external_ip = null
id = "projects/casey-prod-workloads/zones/us-east1-b/instances/orders-api-instance"
instance_id = "4820193746582910234"
internal_ip = "10.0.1.14"
name = "orders-api-instance"
self_link = "https://www.googleapis.com/compute/v1/projects/casey-prod-workloads/zones/us-east1-b/instances/orders-api-instance"

πŸ” Troubleshooting

Symptom Cause Fix
apply recreates the instance after only changing the OS image boot_disk.initialize_params.image is force-new β€” there is no in-place OS patch path Plan for instance recreation (and any data-disk migration it implies); use attached_disks for data that must survive a boot-disk replacement
apply fails with an error about stopping the instance to update a property machine_type, service_account_email/scopes, min_cpu_platform, or shielded_instance_config changed without allow_stopping_for_update = true Set allow_stopping_for_update = true, or set desired_status = "TERMINATED" before the change
GPU-attached instance fails to create at apply (not caught by validate) scheduling.on_host_maintenance was left at MIGRATE (or unset) with guest_accelerators populated Set scheduling.on_host_maintenance = "TERMINATE"
Instance creation fails with a Shielded VM error Boot image is not on GCP's Shielded-VM-capable image list Choose a Shielded-VM-compatible image, or explicitly disable the specific unsupported shielded_instance_config field with a documented reason
assign_external_ip = true but the instance never gets a public IP at apply Org policy constraints/compute.vmExternalIpAccess denies external IPs at the org/folder level Confirm the org policy with your GCP organization administrator before relying on this flag
plan fails with a labels validation error A label key/value contains uppercase letters, starts with a number, or exceeds 63 characters Adjust to lowercase letters, numbers, underscores, or hyphens only, 63 chars max
An IAM-role-dependent workload on this instance fails immediately after apply IAM role grants (e.g. via terraform-google-project-iam-bindings) can take up to ~60 seconds to propagate Retry, or add a short delay between the IAM-binding apply and workload startup
A second network_interfaces entry unexpectedly has no external IP even with assign_external_ip = true By design β€” external IP assignment applies only to network_interfaces[0] Reorder the list so the interface needing a public IP is entry 0, or use a NAT/load-balancer pattern for the secondary interface

πŸ”— Related Docs

  • google_compute_instance provider resource reference
  • terraform-google-vpc-network (supplies network_interfaces[*].subnetwork)
  • terraform-google-service-account (supplies service_account_email)
  • terraform-google-kms-keyring (supplies optional CMEK kms_key_self_link)
  • terraform-google-firewall-policy (consumes this module's tags via target_tags)
  • terraform-google-project-iam-bindings (grants IAM roles to the attached service account)
  • This module's SCOPE.md

About

Terraform module: terraform-google-compute-instance

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages