Creates and manages a single Google Compute Engine VM instance (
google_compute_instance) with no external IP and full Shielded VM protections by default. Targetshashicorp/google ~> 7.0, Terraform>= 1.12.0.
- π₯οΈ 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 setsassign_external_ip = true; Shielded VM (secure boot, vTPM, integrity monitoring) is enabled by default. - π Consumes a subnetwork self_link from
terraform-google-vpc-networkand, optionally, a service account email fromterraform-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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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;
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.
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;
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.
| 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.imageis force-new for the entire instance. There is no in-place OS patch path β a base-image update means instance recreation. Only asizeincrease on an already-attached disk avoids replacement.name,zone, andhostnameare 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, andshielded_instance_configchanges on an existing instance all requirevar.allow_stopping_for_update = true(or aTERMINATEDdesired_status) β otherwise theapplyfails outright instead of silently recreating or stopping the instance.metadata_startup_scriptis force-new on change; thestartup-scriptkey insidevar.metadatais 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'svalidate/fmtgate cannot catch; an incompatible combination fails only atapply. - Shielded VM requires a Shielded-VM-capable boot image. An incompatible image fails only at
apply, never atplan. service_account {}is always rendered by this module, even whenvar.service_account_emailis null, so thatvar.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_protectiondefaults totrue. A destroy requires flipping it tofalsein an explicit priorapplyβ this is GCP API-enforced, not a Terraform-onlylifecycle.prevent_destroyguard.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.
roles/compute.instanceAdmin.v1on the target project β create, update, and delete Compute Engine instances and their attached disks.roles/iam.serviceAccountUseron the specific service account, only whenvar.service_account_emailis set β required to attach an existing identity to an instance. Not needed whenservice_account_emailis leftnull.
compute.googleapis.comenabled on the target project (viaterraform-google-project-services, applied before this module).- If
assign_external_ip = true: confirmconstraints/compute.vmExternalIpAccessdoes not deny external IPs at the org/folder level β undetectable atplantime, only atapply. - If any
kms_key_self_linkis set: the referenced Cloud KMS CryptoKey must already exist, and the Compute Engine service agent needsroles/cloudkms.cryptoKeyEncrypterDecrypteron it.
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
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.
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 |
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_configfield 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"
}
}
β οΈ Confirmconstraints/compute.vmExternalIpAccessis not set to deny at the org/folder level β this module'svalidate/plangate cannot detect that constraint; onlyapplysurfaces 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_scopesdefaults 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 requireon_host_maintenance = "TERMINATE"per the live GCP API β this module does not enforce the combination atplantime; an incompatible value fails only atapply.
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_restartmust befalsefor a preemptible/SPOT instance β GCP will not restart a preempted instance automatically regardless of this setting, but the API still expects it set tofalsefor 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 forassign_external_ipfrom 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.imageis force-new for the entire instance β resizingsizealone is the only in-place boot-disk change; changingimageortypeis 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_linkis 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_diskby 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 noadvanced_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_scriptis force-new on change (re-running the script re-creates the instance) β distinct from thestartup-scriptkey insidevar.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-instancein 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'sservice_account_emailandnetwork_interfaces[*].subnetworkboth come from sibling module outputs, never a hardcoded string.
β οΈ IAM role grants can take up to ~60 seconds to propagate; the explicitdepends_ononorders_api_iamorders the graph correctly, but a transient permission-denied error on first apply is still possible β see Troubleshooting.
| 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.
| 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.
boot_disk.initialize_params.imageis 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 asizeincrease on an already-attached disk avoids replacement;imageandtypechanges do not have an equivalent in-place path.- External IP applies only to
network_interfaces[0]. The nesteddynamic "access_config"block inmain.tfgates on bothvar.assign_external_ipand 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 wayvar.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-SPOTschedulingfields (node_affinities,min_node_cpus,availability_domain,local_ssd_recovery_timeout,max_run_duration,on_instance_stop_action), and severalnetwork_interfaceextras (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_updategates several in-place changes.machine_type,service_account_email/scopes,min_cpu_platform, andshielded_instance_configall require it (or aTERMINATEDdesired_status) to change without a failedapply.network_interfacesis a list, not afor_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.
| 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 |
cd terraform-google-compute-instance
terraform init -backend=false
terraform validate
terraform fmt -checkPin 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.
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.
$ 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"
| 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 |
google_compute_instanceprovider resource referenceterraform-google-vpc-network(suppliesnetwork_interfaces[*].subnetwork)terraform-google-service-account(suppliesservice_account_email)terraform-google-kms-keyring(supplies optional CMEKkms_key_self_link)terraform-google-firewall-policy(consumes this module'stagsviatarget_tags)terraform-google-project-iam-bindings(grants IAM roles to the attached service account)- This module's
SCOPE.md