Creates and manages a single, wholly immutable Google Compute Engine instance template (
google_compute_instance_template) with no external IP and full Shielded VM protections by default. Targetshashicorp/google ~> 7.0, Terraform>= 1.12.0.
- π₯οΈ Creates one
google_compute_instance_templateβ a versioned, disposable blueprint for GCE instances, most commonly consumed by aterraform-google-managed-instance-groupfor rolling, autoscaled workloads. - π§ Wholly immutable: every argument on this resource is force-new at the GCE API level. There is no in-place update path at all β a configuration change always means "destroy this template, create a new one."
- π House pattern:
name_prefix(never a fixedname) pluslifecycle { create_before_destroy = true }, so callers can roll template versions without a window where no template exists. - π 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 β matchingterraform-google-compute-instance's posture. - π 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 β disks and network interfaces are nested configuration blocks on the
template itself, not a separate
for_each-managed child collection.
π‘ Why it matters: because this resource cannot be updated in place, a caller who does not understand the immutability gotcha will be surprised the first time a routine change (a machine type bump, a new boot image) replaces the entire template rather than patching it. This module makes that behavior explicit up front and ships the
name_prefix+lifecycle { create_before_destroy = true }pattern as the house answer to "how do I roll this without downtime."
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
IT["terraform-google-instance-template"]:::thisModule
FW["terraform-google-firewall-policy"]:::sibling
MIG["terraform-google-managed-instance-group"]:::keystoneSibling
PS -- "enables compute.googleapis.com" --> IT
VPC -- "subnetwork self_link consumed by network_interfaces[*].subnetwork" --> IT
SA -- "email consumed by service_account_email (optional)" --> IT
KMS -- "crypto key self_link consumed by disks[*].disk_encryption_key.kms_key_self_link (optional CMEK)" --> IT
IT -- "tags consumed as target_tags" --> FW
IT -- "self_link / self_link_unique consumed by version.instance_template" --> MIG
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 disk encryption.
Downstream, terraform-google-firewall-policy targets instances created from this template via network
tags (not a direct resource reference), and terraform-google-managed-instance-group is the primary
known consumer of this module's self_link/self_link_unique outputs, feeding them into its own
version.instance_template argument for rolling deployment.
Validated via the Mermaid Chart MCP before embedding.
graph LR
subgraph Inputs
A["var.name / name_prefix / machine_type / region"]
B["var.disks"]
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_template.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, self_link_unique"]
R --> O2["output: name"]
R --> O3["output: numeric_id"]
classDef thisModule fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:1px;
Resource inventory (1 resource): google_compute_instance_template.this, with one-or-more
disk nested blocks (unified boot+data disk β exactly one boot = true), 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 blocks, an optional
advanced_machine_features block, an optional timeouts block (create/delete only), and a
lifecycle { create_before_destroy = true } guard. 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 because the live provider documentation's
chunked reference mixed this resource's content with its own data-source variant
(name/filter/self_link_unique lookup arguments) and with
google_compute_region_instance_template, so schema-JSON ground truth was used to disambiguate the
real resource argument set field-for-field):
β οΈ β οΈ The entire resource is immutable β every argument is force-new. Unlikegoogle_compute_instance(which allows some in-place changes viaallow_stopping_for_update), there is no such escape hatch here. Any change destroys the existing template and creates a new one under a new identity.diskis a single unified block, not separateboot_disk/attached_diskresources. The live schema has no nestedinitialize_paramssub-block either βdisk_size_gb,disk_type,source_image, etc. sit directly on eachdiskentry, with the boot disk distinguished only by its ownboot = trueflag. This genuinely diverges fromgoogle_compute_instance's shape β do not assume parity between the two resources.network_interface.access_config.public_ptr_domain_nameis computed-only on this resource β unlikegoogle_compute_instance, where the same field is settable. This module does not expose it as a configurable field; setting it would be rejected by the provider.timeoutssupports onlycreateanddeleteβ there is noupdatetimeout in the live schema, consistent with the resource's wholesale immutability.nameandname_prefixare mutually exclusive and both optional+computed; leaving both null lets the provider auto-generate a name.name_prefixlonger than 37 characters falls back to a shortened, more collision-prone UUID suffix (54-character hard max).- This resource exposes both
self_linkandself_link_uniqueβ the latter has no equivalent ongoogle_compute_instance. Google's own docs recommendself_link_uniquefor a managed instance group'sversion.instance_templateargument specifically to avoid a rare eventual-consistency staleness issue with the bareself_linkafter acreate_before_destroyreplacement. - 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.disks[*].disk_typeis 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 instance templates.
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 for instances created from this template β undetectable atplantime, only atapply. - If any
disk_encryption_key.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-instance-template/
βββ providers.tf # required_providers (hashicorp/google ~> 7.0) + required_version β no provider {} block
βββ variables.tf # name/name_prefix, description, machine_type, region, disks, network_interfaces,
β # assign_external_ip, external_access_config, service_account_email/scopes,
β # scheduling, shielded_instance_config, guest_accelerators,
β # advanced_machine_features, labels, timeouts, and remaining scalar arguments
βββ main.tf # google_compute_instance_template.this β the sole resource, with
β # lifecycle { create_before_destroy = true }
βββ outputs.tf # id, self_link, self_link_unique, name, numeric_id
βββ README.md # this file
βββ SCOPE.md # lightweight cross-module contract
βββ examples/ # runnable example matching the Quick Start below
module "app_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "app-template-"
machine_type = "e2-medium"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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 |
disks[*].disk_encryption_key.kms_key_self_link |
string (optional, self_link) |
terraform-google-kms-keyring |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Terraform-internal resource identifier | Any module needing the Terraform reference |
self_link |
The template's URI (self_link) | terraform-google-managed-instance-group's instance_template_self_link input β the output that module actually consumes |
self_link_unique |
A uniquely-identifying URI for this specific template incarnation | A MIG version.instance_template argument wanting the stronger eventual-consistency guarantee |
name |
Realized template name (whichever of name/name_prefix/auto-generated took effect) |
Diagnostic/reference use |
numeric_id |
GCE's server-assigned numeric template identifier | Diagnostic/reference use |
1 Β· Minimal call
module "app_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "app-template-"
machine_type = "e2-medium"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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 β every secure default takes effect without the caller typing anything extra. Note
name_prefix, notnameβ the house pattern from the very first example.
2 Β· Shielded VM defaults left in place
module "app_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "shielded-app-template-"
machine_type = "e2-medium"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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 Β· Opting into assign_external_ip = true
module "bastion_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "bastion-template-"
machine_type = "e2-small"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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. Noteexternal_access_confighere has nopublic_ptr_domain_namefield β that attribute is computed-only on this resource (see Schema notes that bite).
4 Β· Attaching a service account with least-privilege scopes
module "orders_api_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "orders-api-template-"
machine_type = "e2-medium"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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 Β· guest_accelerator usage (GPU-attached template)
module "ml_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "ml-training-template-"
machine_type = "n1-standard-8"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 100
disk_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.
6 Β· name_prefix + create_before_destroy rollout pattern
module "app_template_v2" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
# Bumping machine_type from "e2-medium" (v1) to "e2-standard-2" (v2) forces
# an entirely new template. Because name_prefix (not a fixed name) is used
# and main.tf carries lifecycle { create_before_destroy = true }, Terraform
# creates the NEW template (under a freshly generated name) BEFORE
# destroying the old one β there is never a moment with zero templates.
name_prefix = "app-template-"
machine_type = "e2-standard-2"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 20
}
]
network_interfaces = [
{ subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
]
}
# A consuming terraform-google-managed-instance-group picks up the new self_link on
# its own rolling-update schedule β this module only guarantees the new
# template exists alongside the old one during the transition, not that
# running instances are updated.π‘ This is the house answer to "how do I change something on an immutable resource without downtime":
name_prefix, never a fixedname, pluscreate_before_destroy. A fixednamecombined withcreate_before_destroywould fail β Terraform would try to create the replacement under the same name before the original is destroyed, and GCE rejects that as a duplicate.
7 Β· Why changing this field replaces the template
module "app_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "app-template-"
machine_type = "e2-medium"
disks = [
{
boot = true
# Changing source_image here (e.g. a new Debian point release) does
# NOT patch an existing template's boot image in place β there is no
# such operation for a template. It forces a brand-new template
# resource, with a brand-new self_link/self_link_unique, exactly like
# changing machine_type, network_interfaces, or ANY other argument in
# this file would.
source_image = "debian-cloud/debian-12"
disk_size_gb = 20
}
]
network_interfaces = [
{ subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
]
}
β οΈ Unlikegoogle_compute_instance(where only a handful of fields are truly force-new and several allow an in-place update viaallow_stopping_for_update), every argument ongoogle_compute_instance_templateis force-new. Plan every change here as "this creates a new template," never as "this updates the existing template."
8 Β· Legacy workload requiring an explicit OAuth scope
module "legacy_batch_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "legacy-batch-template-"
machine_type = "e2-standard-4"
disks = [
{
boot = true
source_image = "debian-cloud/debian-11"
disk_size_gb = 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.
9 Β· Preemptible (SPOT) scheduling configuration
module "batch_worker_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "batch-worker-template-"
machine_type = "e2-standard-4"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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 β the API expects it set tofalsefor consistency even though GCP will not auto-restart a preempted instance regardless.
10 Β· Multiple network_interface entries
module "dual_homed_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "dual-homed-template-"
machine_type = "n2-standard-4"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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.
11 Β· Additional data disk alongside the boot disk
module "database_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "db-template-"
machine_type = "n2-standard-8"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 50
disk_type = "pd-ssd"
},
{
boot = false
disk_name = "data"
disk_size_gb = 200
disk_type = "pd-balanced"
auto_delete = false
}
]
network_interfaces = [
{ subnetwork = module.vpc_network.subnetwork_self_links["data-subnet-use1"] }
]
}βΉοΈ Unlike
terraform-google-compute-instance(separateboot_disk+attached_disks), this module'sdiskslist carries both the boot AND data disks together β exactly one entry must setboot = true.auto_delete = falseon the data disk keeps it from being deleted if the template itself is later destroyed.
12 Β· CMEK-encrypted disk
module "regulated_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "regulated-workload-template-"
machine_type = "n2-standard-4"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 50
disk_encryption_key = {
kms_key_self_link = module.kms_keyring.crypto_key_ids["instance-template-key"]
}
}
]
network_interfaces = [
{ subnetwork = module.vpc_network.subnetwork_self_links["regulated-subnet-use1"] }
]
}π
disk_encryption_key.kms_key_self_linkis accepted as an optional variable and never defaulted to a specific key, per this suite's CMEK convention extended to this module's disk encryption.
13 Β· Advanced machine features
module "nested_virt_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "ci-runner-template-"
machine_type = "n2-standard-8"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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.
14 Β· Custom labels and create/delete timeouts
module "app_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "app-template-labeled-"
machine_type = "e2-medium"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 20
}
]
network_interfaces = [
{ subnetwork = module.vpc_network.subnetwork_self_links["app-subnet-use1"] }
]
labels = {
environment = "prod"
owning_team = "platform"
}
timeouts = {
create = "10m"
delete = "10m"
}
}βΉοΈ Only
create/deleteare meaningful here β this resource's live schema has noupdatetimeout at all, since there is nothing to update in place. Label keys/values must be lowercase letters, numbers, underscores, or hyphens (63 char max) β enforced at plan time.
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 "orders_api_template" {
source = "git::https://github.com/microsoftexpert/terraform-google-instance-template.git?ref=v1.0.0"
name_prefix = "orders-api-template-"
machine_type = "e2-medium"
disks = [
{
boot = true
source_image = "debian-cloud/debian-12"
disk_size_gb = 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]
}
module "orders_api_mig" {
source = "git::https://github.com/microsoftexpert/terraform-google-managed-instance-group.git?ref=v1.0.0"
name = "orders-api-mig"
region = "us-east1"
instance_template_self_link = module.orders_api_template.self_link
target_size = 3
}π‘ This wires
terraform-google-project-servicesβterraform-google-vpc-network+terraform-google-service-accountβterraform-google-project-iam-bindingsβterraform-google-instance-templateβterraform-google-managed-instance-groupin dependency order: APIs enabled first, then networking and identity created in parallel, then IAM roles granted, then the template itself (itsservice_account_emailandnetwork_interfaces[*].subnetworkboth come from sibling module outputs, never a hardcoded string), and finally the managed instance group that actually runs instances from this template'sself_link.
β οΈ 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 of an instance created from this template is still possible β see Troubleshooting.
| Variable | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string |
No | null |
Force-new; mutually exclusive with name_prefix; not recommended with create_before_destroy |
name_prefix |
string |
No | null |
House-recommended naming pattern; max 54 chars |
description |
string |
No | null |
|
instance_description |
string |
No | null |
Describes instances created from this template, not the template itself |
machine_type |
string |
Yes | β | Force-new (whole-resource immutability) |
region |
string |
No | null |
Restricts the template to a region only if a regional resource (e.g. custom subnetwork) is referenced β this is a global resource |
disks |
list(object({...})) |
Yes | β | At least one entry; exactly one boot = true; force-new |
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; no public_ptr_domain_name field (computed-only on this resource) |
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 |
|
can_ip_forward |
bool |
No | false |
|
min_cpu_platform |
string |
No | null |
|
resource_policies |
list(string) |
No | [] |
Max 1 supported by the GCP API; distinct from per-disk resource_policies |
key_revocation_action_type |
string |
No | null |
STOP | NONE |
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 |
labels |
map(string) |
No | {} |
GCP label-format validated |
timeouts |
object({...}) |
No | null |
create/delete only β no update timeout exists on this resource |
Full variable schemas
variable "disks" {
type = list(object({
boot = optional(bool, false)
auto_delete = optional(bool, true)
device_name = optional(string)
disk_name = optional(string)
disk_size_gb = optional(number)
disk_type = optional(string)
source_image = optional(string)
source = optional(string)
source_snapshot = optional(string)
interface = optional(string, "SCSI")
mode = optional(string, "READ_WRITE")
type = optional(string, "PERSISTENT")
labels = optional(map(string), {})
resource_policies = optional(list(string), [])
disk_encryption_key = optional(object({
kms_key_self_link = optional(string)
kms_key_service_account = optional(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")
})
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 "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)
delete = optional(string)
})
default = null
}See variables.tf for every remaining scalar variable (name, name_prefix, description,
instance_description, machine_type, region, service_account_email,
service_account_scopes, tags, metadata, metadata_startup_script, can_ip_forward,
min_cpu_platform, resource_policies, key_revocation_action_type) and every validation {}
block's exact condition.
| Output | Description |
|---|---|
id |
Terraform-internal resource identifier |
self_link |
The template's URI (self_link) β the output terraform-google-managed-instance-group actually consumes |
self_link_unique |
A uniquely-identifying URI for this specific template incarnation (no equivalent on google_compute_instance) |
name |
Realized template name, whichever naming input actually took effect |
numeric_id |
GCE's server-assigned numeric template identifier (distinct from id) |
None of these outputs are secret-bearing; no sensitive = true is applied to any of them.
- Every argument in this module is force-new β there is no in-place update path. This is the
single fact this entire module is built around documenting. Unlike
terraform-google-compute-instance(which allows several in-place changes viaallow_stopping_for_update), there is no equivalent escape hatch for a template β any configuration change destroys the existing template and creates a new one under a new identity. lifecycle { create_before_destroy = true }on the keystone resource was added during initial authoring (not part of any standard boilerplate) specifically to make that replace-on-any-change behavior survivable: the new template is created before the old one is destroyed, so a consuming managed instance group's own version reference is never left pointing at nothing. Confirmed this does not conflict with this suite's "record ordering via implicit references" convention, which concerns ordering between different resources, not lifecycle behavior on a single resource.name_prefix, never a fixedname, is required forcreate_before_destroyto actually work. A fixednamewould make Terraform attempt to create the replacement template under the SAME name before destroying the original β the GCE API rejects that as a duplicate.variables.tfdocuments this pairing explicitly and recommendsname_prefixin its description.disksunifies boot and data disks into one ordered list β a genuine structural divergence fromgoogle_compute_instance's separateboot_disk/attached_diskresources. Exactly one entry must setboot = true, enforced by this module's ownvalidation {}block (the live schema only enforcesmin_items = 1on the block itself, not the boot-disk cardinality).external_access_confighere has nopublic_ptr_domain_namefield. That attribute is computed-only ongoogle_compute_instance_template(confirmed against schema.json for both resources) β a genuine schema divergence fromterraform-google-compute-instance's equivalent variable, which does expose it.self_link_uniqueexists specifically because of the replace-on-any-change behavior. Google recommends it over the bareself_linkfor a managed instance group'sversionblock to avoid a rare eventual-consistency staleness issue right after acreate_before_destroyreplacement.- Several nested blocks/fields the live schema exposes are deliberately out of scope for
v1.0.0 β
confidential_instance_config,network_performance_config,reservation_affinity,disk.source_image_encryption_key/disk.source_snapshot_encryption_key,resource_manager_tags(both instance- and disk-level), Hyperdisk-specific disk fields (provisioned_iops/provisioned_throughput/storage_pool),disk.architecture,disk.guest_os_features,network_interface's IPv6/dynamic-NIC extras, andscheduling's sole-tenant/advanced-SPOT fields. This mirrors this suite's "scope a first slice and expand deliberately" convention 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. service_account {}is always rendered, never omitted β 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 instead.network_interfacesanddisksare lists, notfor_each-keyed maps. Ordering is semantically meaningful for both (GCE assigns device/interface index by list position; this module's external-IP gate keys offnetwork_interfaces[0], and exactly onedisksentry must be the boot disk), which is why these are the two repeating structures in the module that intentionally depart from the "keyed map" convention used for genuinely order-independent child collections elsewhere in this library.
| Concern | Secure default | Opt-out (explicit) |
|---|---|---|
| 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 |
| Disk encryption (CMEK) | disk_encryption_key.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 |
| Template rollout safety (this module's extension) | lifecycle { create_before_destroy = true } on the keystone resource, paired with the name_prefix house recommendation |
Caller may still supply a fixed name, at the documented risk of a duplicate-name collision on the next change |
| 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-instance-template
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 (exactly-one-boot-disk, disk mode/interface/type enums, network-interface
nic_type/stack_type, scheduling enums, name/name_prefix mutual exclusivity, 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 a duplicate-name collision from a fixed name combined with
create_before_destroy. 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. Because this resource is wholly immutable, plan against a live project is also
the only way to confirm a given configuration change produces the replacement a caller expects,
rather than an unexpected diff on a field assumed to be updatable in place.
$ terraform output
id = "projects/casey-prod-workloads/global/instanceTemplates/orders-api-template-a1b2"
name = "orders-api-template-a1b2"
numeric_id = "5821093746582910234"
self_link = "https://www.googleapis.com/compute/v1/projects/casey-prod-workloads/global/instanceTemplates/orders-api-template-a1b2"
self_link_unique = "https://www.googleapis.com/compute/v1/projects/casey-prod-workloads/global/instanceTemplates/orders-api-template-a1b2?uniqueId=5821093746582910234"
| Symptom | Cause | Fix |
|---|---|---|
apply replaces the template after only changing the boot image |
Every argument on this resource is force-new β there is no in-place OS patch path, unlike google_compute_instance |
Expect and plan for template replacement; a consuming MIG needs its own rolling-update strategy to roll the change out to running instances |
| Changed a field and got a brand-new template instead of an update | This is expected β the resource has no update API at all | Confirm the change is intentional; use name_prefix + the default create_before_destroy lifecycle so the replacement does not create a downtime window |
apply fails with a duplicate-name error when rolling a template |
A fixed var.name was used instead of var.name_prefix, colliding with create_before_destroy's create-then-destroy ordering |
Switch to name_prefix β never a fixed name β for any template that will be rolled |
plan fails with "at most one of name, name_prefix" |
Both var.name and var.name_prefix were set |
Set at most one β supply name_prefix for the house rollout pattern |
plan fails with "disks must contain exactly one entry with boot = true" |
Zero or more than one disks entry set boot = true |
Set boot = true on exactly one entry β the boot disk |
GPU-attached template 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 from this template 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 instances from this template never get 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 an instance from this template 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 |
google_compute_instance_templateprovider 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-managed-instance-group(consumes this module'sself_link/self_link_unique)terraform-google-project-iam-bindings(grants IAM roles to the attached service account)- This module's
SCOPE.md