Creates a single static custom route (
google_compute_route) in an existing VPC network — routed to a VPN tunnel, a specific instance, an internal load balancer, a literal next-hop IP, or the default internet gateway. Targetshashicorp/google ~> 7.0, Terraform>= 1.12.0.
- 🛣️ Creates one
google_compute_route— the GCP resource behind a "static route" entry in a VPC network's routing table. Terraform's resource name (google_compute_route) does not map 1:1 to a distinct GCP console product the way, say,google_container_clustermaps to "GKE" — a route is simply an entry inside the VPC network product's own routing table, per this module suite's guidance on naming mismatches between Terraform resources and GCP console concepts. - 🎯 A route tells traffic matching a destination CIDR where to go next: to a VPN tunnel, a specific instance, an internal load balancer, a literal next-hop IP, or the internet gateway. Exactly one next-hop target is required per route — never zero, never more than one.
- 🧱 Standalone, not composite: no natural
for_each-managed child collection — a single keystonegoogle_compute_route.this. - 🔀 Five mutually-exclusive next-hop variables (
next_hop_gateway,next_hop_instance,next_hop_ip,next_hop_vpn_tunnel,next_hop_ilb), enforced via avalidation {}block that catches zero-set or more-than-one-set atplantime — before an invalid combination ever reaches the GCP API. ⚠️ prioritydefaults to GCP's own default (1000) with a prominent tie-break-risk callout — there is no universally "safer" priority value; the right one always depends on the other routes already present in the target network.- 🚫 Confirmed schema gap: this resource has no
labelsargument at all —tags(instance tags controlling which VMs use this route) is a distinct, unrelated mechanism.
💡 Why it matters: a route with the wrong (or absent) next-hop configuration either fails outright at
applyor, worse, silently loses the tie-break against another route in the same network and never carries any traffic at all. Enforcing the five-way exclusivity atplantime, and surfacing the priority tie-break gotcha prominently, is what keeps a hybrid-connectivity or custom-routing composition from a routing failure thatterraform validatealone would never catch.
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
VPN["terraform-google-cloud-vpn"]:::sibling
CI["terraform-google-compute-instance"]:::sibling
THIS["terraform-google-compute-route"]:::thisModule
PS -. "enables compute.googleapis.com (informal prerequisite)".-> THIS
VPC -- "network self_link (recommended) or name consumed as var.network" --> THIS
VPN -- "tunnel_self_links map entry consumed as var.next_hop_vpn_tunnel (optional)" --> THIS
CI -- "self_link/name/internal_ip consumed as var.next_hop_instance / var.next_hop_ip (optional)" --> THIS
classDef thisModule fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:1px;
classDef keystoneSibling fill:#174EA6,color:#ffffff,stroke:#174EA6,stroke-width:1px;
classDef external fill:#E8EAED,color:#202124,stroke:#9AA0A6,stroke-width:1px,stroke-dasharray: 3 3;
classDef sibling fill:#E8EAED,color:#202124,stroke:#9AA0A6,stroke-width:1px;
Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) before embedding.
terraform-google-project-services (external, dashed) must have already enabled compute.googleapis.com
before this module applies. terraform-google-vpc-network is this module's keystone/target sibling — its
network self_link (recommended) or name output becomes var.network, the one required
cross-module input every call needs. terraform-google-cloud-vpn and terraform-google-compute-instance are
real, optional upstream siblings feeding this module's next-hop variables — this module is the
"future static-route module" terraform-google-cloud-vpn's own SCOPE.md already anticipated for its
tunnel_self_links output.
graph LR
subgraph Inputs
A["var.name / var.network / var.dest_range"]
B["var.description / var.priority / var.tags"]
C["exactly one of: var.next_hop_gateway / var.next_hop_instance (+ var.next_hop_instance_zone) / var.next_hop_ip / var.next_hop_vpn_tunnel / var.next_hop_ilb"]
D["var.params.resource_manager_tags (optional)"]
E["var.project / var.deletion_policy / var.timeouts"]
end
R["google_compute_route.this"]:::thisModule
A --> R
B --> R
C --> R
D --> R
E --> R
R --> O1["output: id"]
R --> O2["output: self_link"]
R --> O3["output: name"]
R --> O4["output: route_status"]
classDef thisModule fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:1px;
Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) before embedding.
Resource inventory:
| Resource | Cardinality | Notes |
|---|---|---|
google_compute_route.this |
Exactly 1 | Keystone; params is the only optional nested block, rendered via dynamic only when populated |
| 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, provider_doc_id 12683498,
cross-checked against the installed provider's own terraform providers schema -json output):
- Five-way next-hop exclusivity is "exactly one," not "at most one." Confirmed verbatim: "A Route resource must have exactly one specification of either nextHopGateway, nextHopInstance, nextHopIp, nextHopVpnTunnel, or nextHopIlb." A route with none set is just as invalid as one with two or more set.
priority's default (1000) is a silent tie-break risk. Confirmed verbatim: "the one with the lowest-numbered priority value wins" — a caller who accepts the default is only safe from conflicts if no other route in the network also uses 1000 (or a lower value) for an overlapping destination range, including GCP's own implicit subnet routes.networkargument's own live-docs example uses.name(bare name), not.self_link/.id— a deviation from this library's usual self_link-preferred convention, resolved (not silently "corrected") in this module's favor ofself_linkas the recommended, cross-project-safe form. See Architecture Notes.params.resource_manager_tagsis immutable/create-time-only. Confirmed verbatim: "The field is immutable and causes resource replacement when mutated... modifying this field after creation will trigger recreation."- No
labelsargument anywhere. Confirmed absent from the full live Argument Reference — a genuine Rule 6 escape-clause case, same precedent asterraform-google-vpc-network. timeoutsis create/delete only — confirmed against the live schema'sblock_types.timeouts, which lists only these two (noupdate).next_hop_ilbon the GA provider accepts only a partial/full URL, not a bare IP — confirmed: "With the GA provider you can only specify the forwarding rule as a partial or full URL" (the bare-IP form is agoogle-beta-only capability, out of scope for this GA-only library).
roles/compute.networkAdminon the target project — create, update, and delete routes.
compute.googleapis.comenabled on the target project (viaterraform-google-project-services, applied before this module).- The target network must already exist — typically via
terraform-google-vpc-network. - If
projectis supplied to target a Shared VPC host project, the applying principal needsroles/compute.networkAdminon that HOST project specifically.
terraform-google-compute-route/
├── providers.tf # required_providers (hashicorp/google ~> 7.0) + required_version — no provider {} block
├── variables.tf # name/network/dest_range, 5-way next-hop exclusivity, priority, tags, params, project, deletion_policy, timeouts
├── main.tf # google_compute_route.this
├── outputs.tf # id, self_link, name, route_status
├── README.md # this file
├── SCOPE.md # lightweight cross-module contract
└── examples/ # runnable example matching the Quick Start below
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-to-internet"
network = "https://www.googleapis.com/compute/v1/projects/casey-prod-project/global/networks/casey-prod-vpc"
dest_range = "0.0.0.0/0"
next_hop_gateway = "default-internet-gateway"
}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. The call above routes all otherwise-unmatched traffic to the
default internet gateway at GCP's own default priority (1000).
Consumes
| Input | Type | Source module |
|---|---|---|
network |
string (self_link recommended, bare name also accepted) |
terraform-google-vpc-network |
next_hop_vpn_tunnel (optional) |
string (VPN tunnel self_link) |
terraform-google-cloud-vpn |
next_hop_instance (optional) |
string (instance self_link or name) |
terraform-google-compute-instance |
next_hop_ip (optional) |
string (instance internal_ip) |
terraform-google-compute-instance |
params.resource_manager_tags (optional) |
map(string) |
terraform-google-resource-tags |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Route Terraform-internal resource id | Diagnostic/audit use |
self_link |
Route self_link (full GCP API URL form) | Diagnostic/audit use |
name |
Route name | Diagnostic/audit use |
route_status |
ACTIVE/INACTIVE — operational diagnostic only |
Diagnostic/troubleshooting use |
1 · Minimal route to the default internet gateway
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-to-internet"
network = module.vpc_network.self_link
dest_range = "0.0.0.0/0"
next_hop_gateway = "default-internet-gateway"
}💡
"default-internet-gateway"is the literal string GCP expects — there is no sibling module that produces this value; it is a caller-supplied literal.
2 · Route to a specific instance via next_hop_instance + zone
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-via-nva"
network = module.vpc_network.self_link
dest_range = "10.50.0.0/16"
next_hop_instance = module.nva_instance.name
next_hop_instance_zone = "us-east1-b"
}ℹ️
next_hop_instance_zoneis only meaningful whennext_hop_instanceis a bare instance NAME (not a URL) — this module'svalidation {}block requires it be unset whennext_hop_instanceitself is unset.
3 · Route to an instance via self_link — no zone needed
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-via-nva-selflink"
network = module.vpc_network.self_link
dest_range = "10.51.0.0/16"
next_hop_instance = module.nva_instance.self_link
}💡 A
self_linkalready embeds its own zone —next_hop_instance_zoneis correctly omitted here.
4 · Route with a literal next_hop_ip
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-static-ip"
network = module.vpc_network.self_link
dest_range = "10.60.0.0/16"
next_hop_ip = "10.0.1.25"
}
⚠️ next_hop_ipmust be an IPv4 dotted-quad address — enforced viavalidation {}, mirroringterraform-google-cloud-vpn's ownpeer_ipprecedent.
5 · Route to an internal load balancer via next_hop_ilb
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-to-ilb"
network = module.vpc_network.self_link
dest_range = "192.0.2.0/24"
next_hop_ilb = "regions/us-east1/forwardingRules/internal-lb-fr"
}ℹ️ No sibling module produces an internal forwarding-rule reference yet — this input has no producing module in the current catalog. On the GA provider (this library's target),
next_hop_ilbmust be a partial or full URL, never a bare IP.
6 · Non-default priority with the tie-break risk called out
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-high-priority-override"
network = module.vpc_network.self_link
dest_range = "10.70.0.0/16"
priority = 100
next_hop_vpn_tunnel = module.cloud_vpn.tunnel_self_links["to-onprem-primary"]
}
⚠️ priority = 100deliberately outranks GCP's default (1000) so this route wins any tie against the default-priority routes already present — but it can just as easily outrank a DIFFERENT custom route the caller does not realize exists. Review every route in the target network sorted by priority before relying on a specific outcome.
7 · Route scoped to specific instance tags
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-tagged-only"
network = module.vpc_network.self_link
dest_range = "10.80.0.0/16"
tags = ["restricted-egress"]
next_hop_vpn_tunnel = module.cloud_vpn.tunnel_self_links["to-onprem-primary"]
}ℹ️
tagsscopes which VMs' routing tables include this route (by their ownnetwork_tags) — a distinct mechanism from GCPlabels(this resource has none) and fromparams.resource_manager_tags(a third, different mechanism again).
8 · Route with params.resource_manager_tags populated
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-tagged-resource"
network = module.vpc_network.self_link
dest_range = "10.90.0.0/16"
next_hop_gateway = "default-internet-gateway"
params = {
resource_manager_tags = {
(module.resource_tags.id) = module.resource_tags.tag_value_ids["prod"]
}
}
}
⚠️ params.resource_manager_tagsis immutable — changing it after creation forces a full route replacement. Useterraform-google-tag-bindingsto attach a tag to an already-created route without replacing it.
9 · Cross-project route on a Shared VPC host network
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-shared-vpc-route"
network = module.shared_vpc_host_network.self_link
dest_range = "10.100.0.0/16"
project = "casey-prod-networking-host"
next_hop_gateway = "default-internet-gateway"
}🔑 The applying principal needs
roles/compute.networkAdminon the HOST project (casey-prod-networking-host) specifically — not just the service project the caller's default provider configuration targets.
10 · Route with a custom description and explicit deletion_policy
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-critical-vpn-path"
network = module.vpc_network.self_link
dest_range = "10.0.0.0/8"
description = "Sole route to the on-prem datacenter — do not delete without a migration plan"
next_hop_vpn_tunnel = module.cloud_vpn.tunnel_self_links["to-onprem-primary"]
deletion_policy = "PREVENT"
}
⚠️ deletion_policy = "PREVENT"blocks the Terraform command itself — it is a Terraform-state-level guard, not a GCP-API-enforced one. This module never defaults to"PREVENT"; opt in explicitly for operationally critical routes.
11 · Custom create/delete timeouts
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-route-to-internet"
network = module.vpc_network.self_link
dest_range = "0.0.0.0/0"
next_hop_gateway = "default-internet-gateway"
timeouts = {
create = "10m"
delete = "10m"
}
}ℹ️ This resource's
timeoutssupportscreate/deleteonly — there is noupdatetimeout, since every meaningful field change is force-new.
12 · Route using the bare network name (live-docs example form)
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-dev-route-bare-name"
network = module.vpc_network.name
dest_range = "10.110.0.0/16"
next_hop_gateway = "default-internet-gateway"
}ℹ️ The live
google_compute_routedocs' own executable examples use this bare-name form. This module accepts it unmodified — but see Architecture Notes for whyself_link(example 1) is the recommended form for any composition that might ever span projects.
13 · Validation failure — zero next-hop fields set
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-invalid-route-no-next-hop"
network = module.vpc_network.self_link
dest_range = "10.120.0.0/16"
# No next_hop_* field set at all.
}🔒 This fails
terraform planimmediately with this module'svalidation {}error message — "Exactly one of next_hop_gateway, next_hop_instance, next_hop_ip, next_hop_vpn_tunnel, or next_hop_ilb must be set" — rather than reaching the GCP API as anapply-time rejection.
14 · Validation failure — two next-hop fields set
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-invalid-route-two-next-hops"
network = module.vpc_network.self_link
dest_range = "10.130.0.0/16"
next_hop_gateway = "default-internet-gateway"
next_hop_ip = "10.0.1.25"
}🔒 Also fails
terraform planwith the samevalidation {}error — setting more than one next-hop field is exactly as invalid as setting none.
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)
# — applied before every other module in this composition.
}
module "vpc_network" {
source = "git::https://github.com/microsoftexpert/terraform-google-vpc-network.git?ref=v1.0.0"
network_name = "casey-prod-use1-vpc"
subnetworks = {
"app-subnet-use1" = {
ip_cidr_range = "10.0.1.0/24"
region = "us-east1"
}
}
depends_on = [module.project_services]
}
module "cloud_vpn" {
source = "git::https://github.com/microsoftexpert/terraform-google-cloud-vpn.git?ref=v1.0.0"
gateway_name = "casey-prod-use1-havpn-gw"
network = module.vpc_network.id
region = "us-east1"
tunnels = {
"to-onprem-primary" = {
vpn_gateway_interface = 0
peer_external_gateway = module.external_vpn_gateway.id
peer_ip = "203.0.113.1"
shared_secret_wo = var.vpn_shared_secret
shared_secret_wo_version = 1
}
}
depends_on = [module.vpc_network]
}
module "compute_route" {
source = "git::https://github.com/microsoftexpert/terraform-google-compute-route.git?ref=v1.0.0"
name = "casey-prod-use1-route-to-onprem"
network = module.vpc_network.self_link
dest_range = "192.168.0.0/16" # the on-prem CIDR reachable through this tunnel
next_hop_vpn_tunnel = module.cloud_vpn.tunnel_self_links["to-onprem-primary"]
priority = 900 # deliberately below GCP's 1000 default — see example 6's callout
depends_on = [module.cloud_vpn]
}💡 This wires
terraform-google-project-services→terraform-google-vpc-network→terraform-google-cloud-vpn→terraform-google-compute-routein dependency order: APIs enabled first, then the VPC network, then an HA VPN gateway and tunnel to an on-prem peer, then a static route that sends on-prem-destined traffic through that tunnel by consumingtunnel_self_links["to-onprem-primary"]asnext_hop_vpn_tunnel— the realistic hybrid-connectivity static-route scenario this module targets.
⚠️ module.external_vpn_gateway(the on-prem peer'sgoogle_compute_external_vpn_gateway) andvar.vpn_shared_secretare referenced above but not shown in full — seeterraform-google-cloud-vpn's own README for that resource's complete setup.
| Variable | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string |
Yes | — | Force-new; 1-63 chars, RFC1035 |
network |
string |
Yes | — | self_link recommended; bare name also accepted; force-new |
dest_range |
string |
Yes | — | IPv4 CIDR only; force-new |
description |
string |
No | null |
|
priority |
number |
No | 1000 |
|
tags |
list(string) |
No | [] |
Instance tags, NOT labels or Resource Manager tags |
next_hop_gateway |
string |
No* | null |
*Exactly one of the five next_hop_* fields required |
next_hop_instance |
string |
No* | null |
Pair with next_hop_instance_zone if a bare name |
next_hop_instance_zone |
string |
No | null |
Requires next_hop_instance to be set |
next_hop_ip |
string |
No* | null |
Validated IPv4 dotted-quad when set |
next_hop_vpn_tunnel |
string |
No* | null |
From terraform-google-cloud-vpn's tunnel_self_links |
next_hop_ilb |
string |
No* | null |
GA provider: partial/full URL only, no bare IP |
params |
object({ resource_manager_tags }) |
No | {} |
Immutable/create-time-only when populated |
project |
string |
No | null |
Cross-project override — Shared VPC host-project scenario |
deletion_policy |
string |
No | null |
Validated closed set; NOT defaulted to "PREVENT" |
timeouts |
object({ create, delete }) |
No | null |
Create/delete only — no update |
labels is deliberately NOT modeled — confirmed absent from the live schema (see Architecture
Notes).
Full variable schemas
variable "name" {
type = string
}
variable "network" {
type = string
}
variable "dest_range" {
type = string
}
variable "description" {
type = string
default = null
}
variable "priority" {
type = number
default = 1000
}
variable "tags" {
type = list(string)
default = []
}
variable "next_hop_gateway" {
type = string
default = null
# exactly one of next_hop_gateway/next_hop_instance/next_hop_ip/next_hop_vpn_tunnel/next_hop_ilb
}
variable "next_hop_instance" {
type = string
default = null
}
variable "next_hop_instance_zone" {
type = string
default = null
# requires next_hop_instance to also be set
}
variable "next_hop_ip" {
type = string
default = null
# validated IPv4 dotted-quad when set
}
variable "next_hop_vpn_tunnel" {
type = string
default = null
}
variable "next_hop_ilb" {
type = string
default = null
}
variable "params" {
type = object({
resource_manager_tags = optional(map(string), {})
})
default = {}
}
variable "project" {
type = string
default = null
}
variable "deletion_policy" {
type = string
default = null
# one of DELETE, PREVENT, ABANDON
}
variable "timeouts" {
type = object({
create = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Sensitive |
|---|---|---|
id |
Route Terraform-internal resource id (projects/{{project}}/global/routes/{{name}}) |
No |
self_link |
Route self_link (full GCP API URL form) — confirmed present, unlike this batch's terraform-google-folder/terraform-google-shared-vpc |
No |
name |
Route name | No |
route_status |
ACTIVE/INACTIVE — reflects state as of last apply/refresh, not predictable at plan time |
No |
No secret values exist on this resource; nothing is marked sensitive.
- Five-way next-hop exclusivity, enforced at
plantime, "exactly one," not "at most one." Modeled as five independentoptional(string)variables with a single cross-variablevalidation {}block (attached tonext_hop_gateway, Terraform 1.9+) checking the count of non-null entries across all five equals exactly 1 — zero set is exactly as invalid as two or more set. priority's default (1000) is not a "safe" default — it is GCP's own default, carried through unchanged, with a prominent risk callout instead of a house override. There is no universally safer priority value; the correct value always depends on the other routes already present in the target network, which this module cannot see or validate against.networkargument reference-form — resolved judgment call. The live docs' own two executable Example Usage blocks both use the bare network NAME (google_compute_network.default.name), a real deviation from this library's usual self_link-preferred convention. This module documents and recommendsself_link(this library's house convention, and the cross-project-unambiguous form relevant to the Shared VPC host-project scenario) while still accepting a bare name unmodified, since the live docs' own example proves the API accepts that form too. Not silently "corrected" back to self_link-only.params.resource_manager_tagsis create-time-only and immutable. Changing it after creation triggers a full route replacement, confirmed verbatim in the live schema. Useterraform-google-tag-bindings(google_tags_tag_binding) instead to apply a tag to an already-created route without replacing it.- No
labelsargument anywhere in the live schema. A genuine Rule 6 escape-clause case, same precedent asterraform-google-vpc-network.tags(instance tags controlling which VMs use this route) is a distinct, unrelated GCP primitive — do not conflate the two. timeoutssupports create/delete only — confirmed against the live schema; there is noupdatetimeout, since every meaningful field change on this resource is force-new.projectis a documented cross-project override, not a default addition. Motivated concretely by the Shared VPC host-project scenario — sourced from the caller's own composition, never derived.- IAM propagation lag (up to ~60 seconds) applies to
roles/compute.networkAdmingrants made immediately before this module applies, per this module suite's house-wide note on IAM propagation.
| Concern | Secure default | Opt-out (explicit) |
|---|---|---|
Route priority (priority) |
1000 — GCP's own default, left unchanged (there is no universally "safer" priority value); documented risk callout instead of a house override |
Caller supplies an explicit, network-aware value after reviewing all existing routes' priorities |
State-level destroy guard (deletion_policy) |
null (provider default "DELETE") — NOT defaulted to "PREVENT"; a route has materially lower blast radius than the resources this library does default to a protective posture for |
Caller sets "PREVENT" explicitly for an operationally critical route |
Next-hop selection (next_hop_*) |
No default — the caller MUST supply exactly one; enforced via validation {} before any API call |
N/A — this is a hard requirement, not an opt-out |
Resource Manager tags (params.resource_manager_tags) |
{} (omitted) — the params {} block is only rendered when populated |
Caller supplies tag key/value pairs sourced from terraform-google-resource-tags |
Network reference form (network) |
self_link recommended in documentation (not enforced by type) — cross-project-unambiguous |
Caller may still supply a bare name for a same-project network |
cd terraform-google-compute-route
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 (the five-way next-hop
validation {} block, the next_hop_instance_zone conditional check, the params. resource_manager_tags format check, correct resource/output wiring). terraform fmt -check
confirms canonical formatting. Neither can catch GCP API-level rejections — most importantly, a
route that plans cleanly but is shadowed by a higher-priority-number (i.e. lower-precedence) route
elsewhere in the network, or a next-hop target (an instance, a VPN tunnel, an internal load
balancer) that does not actually exist yet at apply time. 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
id = "projects/casey-prod-project/global/routes/casey-prod-use1-route-to-onprem"
name = "casey-prod-use1-route-to-onprem"
self_link = "https://www.googleapis.com/compute/v1/projects/casey-prod-project/global/routes/casey-prod-use1-route-to-onprem"
route_status = "ACTIVE"
| Symptom | Cause | Fix |
|---|---|---|
| Route created successfully but traffic isn't taking the expected path | A lower-priority-number route elsewhere in the network is winning the tie-break | Review all routes in the network sorted by priority, not just this module's own state |
plan fails with "Exactly one of next_hop_gateway, next_hop_instance,..." |
Two or more next-hop fields were set, or none at all | Set exactly one of the five next_hop_* fields |
apply unexpectedly replaces the route after changing params.resource_manager_tags |
This field is immutable/create-time-only per the live schema | Use terraform-google-tag-bindings to change a Resource Manager tag on an existing route without replacing it |
plan fails with "next_hop_instance_zone requires next_hop_instance..." |
next_hop_instance_zone was set without next_hop_instance |
Set next_hop_instance (as a bare name) alongside next_hop_instance_zone, or omit both |
apply fails with a permission-denied error on a Shared VPC host project |
The applying principal lacks roles/compute.networkAdmin on the HOST project referenced by project/network |
Grant roles/compute.networkAdmin on the host project, not just the service project |
terraform destroy fails outright |
deletion_policy = "PREVENT" is set |
Set deletion_policy to "DELETE" (or remove the argument) before destroying |
| Route to a VPN tunnel never becomes ACTIVE | The referenced next_hop_vpn_tunnel does not exist yet, or its own tunnel establishment failed |
Confirm terraform-google-cloud-vpn's tunnel is up before or via an explicit depends_on; check route_status |
google_compute_routeprovider resource referenceterraform-google-vpc-network(upstream — suppliesvar.network)terraform-google-cloud-vpn(upstream, optional — suppliesvar.next_hop_vpn_tunnelviatunnel_self_links)terraform-google-compute-instance(upstream, optional — suppliesvar.next_hop_instance/var.next_hop_ip)terraform-google-resource-tags,terraform-google-tag-bindings(upstream/downstream —params.resource_manager_tagslifecycle)terraform-google-shared-vpc(related — the Shared VPC host-project scenario motivatingvar.project)terraform-google-project-services(must enablecompute.googleapis.combefore this module applies)- This module's
SCOPE.md