Provisions the "front door" of an external HTTP(S) Application Load Balancer as one coherent unit: a global forwarding rule (
google_compute_global_forwarding_rule), its reserved global IP address (google_compute_global_address), a Google-managed SSL certificate (google_compute_managed_ssl_certificate), a target HTTPS proxy (google_compute_target_https_proxy), and a URL map (google_compute_url_map). Targetshashicorp/google ~> 7.0, Terraform>= 1.12.0.
- 🎯 Creates five top-level resources as a single coherent unit: the global forwarding rule
(keystone,
this), the reserved global address (address), the managed SSL certificate (ssl_certificate), the target HTTPS proxy (https_proxy), and the URL map (url_map). None is individually useful without the other four — a forwarding rule with no target is inert, a target HTTPS proxy with no URL map cannot be created, and a certificate not attached to a proxy never serves traffic. - 🧱 Composite, but with no
for_eachchild collection at all: the four supporting resources are plain, unconditional, role-named resources — neverfor_each, nevercount— because exactly one of each exists per load balancer front door instance. See 🧬 What this builds and SCOPE.md's Design decisions for why this is still composite, not standalone or aggregation. - 🔗 Consumes a backend service by reference (
var.backend_service_self_link, sourced fromterraform-google-backend-service), wired straight through to the URL map'sdefault_service. - 🌐 Reserves its own static IP (
google_compute_global_address.address) rather than accepting one from a sibling module — theip_addressoutput is the single most operationally important value this module emits. - 🔒 Secure by default:
load_balancing_scheme = "EXTERNAL_MANAGED"(a deliberate departure from the provider's ownEXTERNALdefault, matchingterraform-google-backend-service),address_type = "EXTERNAL", network tier implicitlyPREMIUM(the only usable tier for a global forwarding rule — see Architecture Notes for a real grounding correction on where this argument actually lives). ⚠️ Third and final module in a deliberate three-module split of the external HTTP(S) load balancer family:terraform-google-health-check->terraform-google-backend-service->terraform-google-http-load-balancer(this module).- ⏳ Managed SSL certificate provisioning is asynchronous and can take 15-60+ minutes after
applyreturns — see 🔍 Troubleshooting, restated prominently because this is the single most common "is my load balancer broken?" support question this module generates.
💡 Why it matters: this is the resource assembly a browser or client actually connects to. Its reserved IP is what DNS points at, its certificate is what makes HTTPS work, and its URL map is what decides which backend service serves which request. Getting the five-resource wiring right — and documenting the certificate's asynchronous provisioning window — is the difference between a caller correctly waiting out a normal provisioning delay and one wrongly debugging a misconfiguration that doesn't exist.
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
HC["terraform-google-health-check"]:::neutral
BS["terraform-google-backend-service"]:::keystoneSibling
THIS["terraform-google-http-load-balancer"]:::thisModule
DNS["terraform-google-dns-zone"]:::neutral
HC -. "self_link -> health_check_self_link (upstream of backend-service)".-> BS
BS -- "self_link consumed as var.backend_service_self_link (required)" --> THIS
THIS -. "ip_address -> A/AAAA record value (informational - DNS record points at this IP)".-> DNS
classDef thisModule fill:#4285F4,color:#ffffff,stroke:#174EA6,stroke-width:1px;
classDef keystoneSibling fill:#174EA6,color:#ffffff,stroke:#174EA6,stroke-width:1px;
classDef neutral fill:#E8EAED,color:#202124,stroke:#9AA0A6,stroke-width:1px;
Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) before embedding.
terraform-google-backend-service's self_link output is this module's only required cross-module input
(var.backend_service_self_link, shown as the keystone/target sibling in the darker blue).
terraform-google-health-check is one hop further upstream (feeds terraform-google-backend-service, not this
module directly — shown here only for family context, dashed since it is not this module's own
edge). This module's ip_address output feeds terraform-google-dns-zone informationally only — the
dashed edge is deliberate: a composing root module reads ip_address and supplies it as a literal
value to a DNS record, not a live Terraform cross-module reference, since DNS zone ownership and
load-balancer front-door ownership are independent concerns that may not even live in the same
Terraform state (see SCOPE.md's Out of scope section).
flowchart TB
subgraph Module["terraform-google-http-load-balancer"]
direction TB
addr["google_compute_global_address.address (singular)"]:::child
cert["google_compute_managed_ssl_certificate.ssl_certificate (singular)"]:::child
urlmap["google_compute_url_map.url_map (singular)"]:::child
proxy["google_compute_target_https_proxy.https_proxy (singular)"]:::child
fr["google_compute_global_forwarding_rule.this (keystone)"]:::keystone
end
urlmap -->|"url_map = url_map.self_link"| proxy
cert -->|"ssl_certificates = [cert.self_link]"| proxy
proxy -->|"target = proxy.self_link"| fr
addr -->|"ip_address = address.address (attribute)"| fr
classDef keystone fill:#174EA6,color:#FFFFFF,stroke:#0D47A1,stroke-width:2px;
classDef child fill:#4285F4,color:#FFFFFF,stroke:#174EA6,stroke-width:2px;
Validated via the Mermaid Chart MCP before embedding.
Every one of the four supporting resources is singular and unconditional — not a for_each
collection. Contrast with a typical composite module like terraform-google-vpc-network (a keystone
network plus a for_each map of subnetworks): this module has no repeating child collection at all,
because there is exactly one address, one certificate, one proxy, and one URL map per load balancer
front door. SCOPE.md's Design decisions section explains why this is still classified composite
rather than standalone — the defining composite trait is "a tightly-coupled multi-resource unit with
no independent lifecycle for its parts," not "must contain a for_each block," and this module
must create five separate top-level resources for the bundle to do anything at all (a forwarding
rule alone has no target).
Resource inventory (5 resources):
| Resource | Cardinality | Role |
|---|---|---|
google_compute_global_forwarding_rule.this |
Exactly 1 | Keystone — the "front door" identity; its reserved IP is what DNS points at and browsers connect to |
google_compute_global_address.address |
Exactly 1 | Reserves the static global IP the forwarding rule binds to |
google_compute_managed_ssl_certificate.ssl_certificate |
Exactly 1 | Google-managed certificate covering the domain(s) this front door serves |
google_compute_target_https_proxy.https_proxy |
Exactly 1 | Binds the certificate to the URL map |
google_compute_url_map.url_map |
Exactly 1 | Routes incoming requests to a backend service by host/path |
| 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 via terraform-registry,
providerDocIDs 12683401 / 12683400 / 12683429 / 12683522 / 12683527):
google_compute_global_addresshas nonetwork_tierargument at all in the live schema — a real correction made during this authoring session against the prior scaffolding pass's assumption. A global address is implicitly alwaysPREMIUMtier; there is no field to configure otherwise. The forwarding rule's ownnetwork_tierargument does exist, but for the global variant this module builds, the only legal value isPREMIUM— this module leaves it unset at that implicit default rather than model a single-legal-value enum as a variable.google_compute_url_maphas noregionargument — one early research result blended in prose belonging togoogle_compute_region_url_map, confirmed as contamination by reading the full live Argument Reference directly.ssl_certificatesongoogle_compute_target_https_proxyis schema-typedlist(string), but this module always populates it with exactly one entry — this module's own certificate'sself_link. The list shape is preserved on the resource argument so a future minor version can add multi-certificate/SNI support without a breaking variable-type change.- Same-module internal resource references (
target,url_map,ssl_certificates) all use.self_link, matching the live schema's own "URL of..." / "reference to the... resource" / "URLs to... resources" framing and the identical precedent already established interraform-google-managed-instance-group.ip_addressis the address resource's ownaddresscomputed attribute (the literal reserved IP number) — not an identity form at all. - Labels are present on
google_compute_global_forwarding_ruleandgoogle_compute_global_addressbut absent ongoogle_compute_managed_ssl_certificate,google_compute_target_https_proxy, andgoogle_compute_url_map— verified independently per resource, not assumed uniform. - Changing a managed certificate's
managed.domains(or itsname) is effectively force-new;main.tfaddslifecycle { create_before_destroy = true }on the certificate resource, mirroring Google's own official "Managed Ssl Certificate Recreation" example usage verbatim.
roles/compute.loadBalancerAdminon the target project — create, update, and delete every resource in this module's scope (global forwarding rules, target HTTPS proxies, URL maps, SSL certificates, and reserved global addresses are all covered by this role's permission set). The same role is used consistently across all three modules in this load-balancer family (terraform-google-health-check,terraform-google-backend-service,terraform-google-http-load-balancer).
(Sourced directly from SCOPE.md — not re-derived.)
compute.googleapis.comenabled on the target project (viaterraform-google-project-services, applied before this module). This is the only Google API this module's five resources require.certificatemanager.googleapis.comis explicitly NOT required.google_compute_managed_ssl_certificateis part of the classic Compute EnginesslCertificatesAPI surface — the samecompute.googleapis.comAPI used by every other resource in this module. It is a distinct resource fromgoogle_certificate_manager_certificate(the newer, separate Certificate Manager product, which does requirecertificatemanager.googleapis.comand is explicitly out of scope for this module). Do not enable an API this module does not use.- The backend service referenced by
var.backend_service_self_linkmust already exist (terraform-google-backend-serviceapplied first). - Every domain listed in
var.managed.domainsmust already have its DNS A/AAAA record pointing at this module'sip_addressoutput beforeapply, or the managed certificate will stall indefinitely inPROVISIONING/FAILED_NOT_VISIBLE— see 🔍 Troubleshooting. - No org-policy constraint is enforced directly by this module's own resources. is not aware of
a standard GCP org-policy constraint that specifically targets this resource family the way
constraints/compute.vmExternalIpAccesstargets VM instances.
terraform-google-http-load-balancer/
├── providers.tf # required_providers (hashicorp/google ~> 7.0) + required_version — no provider {} block
├── variables.tf # name/description/ip_protocol/port_range/load_balancing_scheme (forwarding rule),
│ # address_name/address_description/address_type/ip_version (address),
│ # ssl_certificate_name/ssl_certificate_description/managed (ssl certificate),
│ # https_proxy_name/https_proxy_description/quic_override/ssl_policy (https proxy),
│ # url_map_name/url_map_description/host_rule/path_matcher (url map),
│ # backend_service_self_link, labels, timeouts
├── main.tf # 5 resources: address, ssl_certificate, url_map, https_proxy, this (forwarding rule)
├── outputs.tf # id, self_link, ip_address, url_map_id, https_proxy_id, ssl_certificate_id, address_name
├── README.md # this file
├── SCOPE.md # full cross-module contract
└── examples/ # runnable example matching the Quick Start below
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}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 |
|---|---|---|
backend_service_self_link (required) |
string (self_link) |
terraform-google-backend-service |
ssl_policy (optional, informational resource-reference string) |
string (self_link) |
none yet — no terraform-google-ssl-policy module exists in this catalog |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Global forwarding rule resource id (keystone identity) | Reference only |
self_link |
Global forwarding rule self-link (URL form) | Reference only |
ip_address |
The reserved global address's address attribute — the single most operationally important output |
terraform-google-dns-zone (informational — literal value in an A/AAAA record, not a live Terraform reference) |
url_map_id |
URL map resource id | Diagnostic / routing-audit use |
https_proxy_id |
Target HTTPS proxy resource id | Diagnostic use |
ssl_certificate_id |
Managed SSL certificate resource id | Diagnostic use alongside a manual gcloud compute ssl-certificates describe status check |
address_name |
Reserved global address's name | Operator convenience / gcloud lookups |
1 · Minimal load balancer (single domain, single default backend)
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}💡 With no
host_rule/path_matchersupplied, every request is routed straight tovar.backend_service_self_linkvia the URL map'sdefault_service— the simplest working front door.
2 · network_tier / address_type left at their secure defaults
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
# address_type left at its default "EXTERNAL" — this module's entire purpose is an
# internet-reachable front door.
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}🔒 There is no
network_tiervariable on this module to override.google_compute_global_addresshas nonetwork_tierargument at all in the live schema (a global address is implicitly alwaysPREMIUM), and the forwarding rule's ownnetwork_tierargument allows onlyPREMIUMfor the global variant this module builds.STANDARDis simply not an offered path here — see Architecture Notes for the full grounding correction.
3 · Managed certificate provisioning window (read before applying)
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
managed = {
domains = ["app.example.com", "www.app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}
⚠️ terraform applyreturns as soon as the certificate object is created — the certificate itself then takes 15-60+ minutes to reachACTIVE. Every domain inmanaged.domainsmust already have its DNS A/AAAA record pointing at this module'sip_addressoutput before thisapplyruns, or Google's own domain-ownership validation stalls indefinitely inPROVISIONING/FAILED_NOT_VISIBLE(not a Terraform-visible error). A caller who tests HTTPS connectivity immediately afterapplyand gets a certificate error has not necessarily hit a misconfiguration — see 🔍 Troubleshooting.
4 · Multiple domains on one managed certificate
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
managed = {
domains = ["app.example.com", "api.example.com", "admin.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}ℹ️ Up to 100 domains per certificate, per the live schema. Adding or removing a domain from this list on an existing certificate is effectively force-new — see Architecture Notes.
5 · Host/path-based routing with a single path_matcher
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
managed = {
domains = ["app.example.com"]
}
host_rule = [
{
hosts = ["app.example.com"]
path_matcher = "app-routes"
}
]
path_matcher = [
{
name = "app-routes"
path_rule = [
{ paths = ["/api/*"], service = module.api_backend_service.self_link },
{ paths = ["/*"], service = null }
]
}
]
backend_service_self_link = module.backend_service.self_link
}ℹ️
path_matcher[*].path_ruleentries withservice = nullfall through to that path_matcher's owndefault_service(alsonullhere), which in turn falls through to the URL map's owndefault_service(var.backend_service_self_link).host_rule[*].path_matchermust reference a declaredpath_matcher[*].name— this module validates that reference atplantime.
6 · Multiple hosts routed to different path_matchers
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "multi-site-lb"
address_name = "multi-site-lb-ip"
ssl_certificate_name = "multi-site-lb-cert"
https_proxy_name = "multi-site-lb-proxy"
url_map_name = "multi-site-lb-url-map"
managed = {
domains = ["shop.example.com", "blog.example.com"]
}
host_rule = [
{ hosts = ["shop.example.com"], path_matcher = "shop-routes" },
{ hosts = ["blog.example.com"], path_matcher = "blog-routes" }
]
path_matcher = [
{
name = "shop-routes"
default_service = module.shop_backend_service.self_link
},
{
name = "blog-routes"
default_service = module.blog_backend_service.self_link
}
]
backend_service_self_link = module.backend_service.self_link
}
⚠️ google_compute_url_maphost_rule/path_matchermisconfiguration plans and applies cleanly even when it subtly misroutes traffic — this plan-only library cannot catch a routing-logic mistake. For non-trivial host/path routing, exercise the resource's owntest {}block (a first-class validation feature checked atapplytime by the API, not modeled by this module's variables in v1.0.0 — see Architecture Notes).
7 · path_rule with paths only (no explicit service — falls through)
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
managed = {
domains = ["app.example.com"]
}
host_rule = [
{ hosts = ["app.example.com"], path_matcher = "static-only" }
]
path_matcher = [
{
name = "static-only"
path_rule = [
{ paths = ["/assets/*", "/images/*"] }
]
}
]
backend_service_self_link = module.backend_service.self_link
}ℹ️
path_rule[*].serviceis optional — omitting it falls through to the enclosingpath_matcher's owndefault_service, then to the URL map'sdefault_serviceif that is also unset.
8 · ip_protocol / port_range overrides
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
ip_protocol = "TCP"
port_range = "8443"
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}ℹ️
ip_protocoldefaults toTCPandport_rangedefaults to"443"(standard HTTPS). Overrideport_rangefor a non-standard listener port — clients must then connect on that port explicitly.
9 · load_balancing_scheme = EXTERNAL (legacy) vs the default EXTERNAL_MANAGED
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "legacy-classic-alb"
address_name = "legacy-classic-alb-ip"
ssl_certificate_name = "legacy-classic-alb-cert"
https_proxy_name = "legacy-classic-alb-proxy"
url_map_name = "legacy-classic-alb-url-map"
load_balancing_scheme = "EXTERNAL"
managed = {
domains = ["legacy.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}ℹ️ This module defaults to
EXTERNAL_MANAGED(the modern Global external Application Load Balancer scheme), matchingterraform-google-backend-service's identical default.load_balancing_schemeshould match the backend service's own scheme — an incompatible combination is caught only atapply, never atplan.
10 · quic_override enabled
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
quic_override = "ENABLE"
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}ℹ️
quic_overridedefaults toNONE(Google manages whether QUIC is negotiated).ENABLEforces QUIC negotiation attempts;DISABLEforces classic TLS only.
11 · ssl_policy attached (informational reference, no module owns this yet)
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
ssl_policy = "https://www.googleapis.com/compute/v1/projects/example-project/global/sslPolicies/modern-tls12"
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}🔒
ssl_policydefaults tonull(GCP's own default SSL policy applies). Noterraform-google-ssl-policymodule exists yet in this catalog — this is a plain resource-reference string, not a live Terraform cross-module dependency.
12 · Custom labels on the forwarding rule and address
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
labels = {
team = "platform"
cost_center = "cc-1234"
}
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}ℹ️
labelsapplies to BOTH the forwarding rule and the reserved address — the only two of this module's five resources whose live schema exposes alabelsargument. There is no per-resource label variable in v1.0.0 — see Architecture Notes.
13 · Custom resource timeouts on the keystone
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
timeouts = {
create = "30m"
update = "30m"
delete = "30m"
}
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}ℹ️
timeoutsapplies only to the keystone forwarding rule (all three of create/update/delete default to 20 minutes per the live schema). The four supporting resources use the provider's own default timeouts (20-30 minutes each).
14 · ip_version = IPV6 dual-stack front door
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "app-lb"
address_name = "app-lb-ip"
ssl_certificate_name = "app-lb-cert"
https_proxy_name = "app-lb-proxy"
url_map_name = "app-lb-url-map"
ip_version = "IPV6"
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}ℹ️
ip_versionapplies to the reserved global address only. Leavenull(the default) to inherit the provider's ownIPV4default.
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 "health_check" {
source = "git::https://github.com/microsoftexpert/terraform-google-health-check.git?ref=v1.0.0"
name = "app-health-check"
http_health_check = {
port = 80
request_path = "/healthz"
}
depends_on = [module.project_services]
}
module "backend_service" {
source = "git::https://github.com/microsoftexpert/terraform-google-backend-service.git?ref=v1.0.0"
name = "prod-app-backend"
backends = {
"primary" = {
group = module.managed_instance_group.instance_group
}
}
health_check_self_link = module.health_check.self_link
}
module "http_load_balancer" {
source = "git::https://github.com/microsoftexpert/terraform-google-http-load-balancer.git?ref=v1.0.0"
name = "prod-app-lb"
address_name = "prod-app-lb-ip"
ssl_certificate_name = "prod-app-lb-cert"
https_proxy_name = "prod-app-lb-proxy"
url_map_name = "prod-app-lb-url-map"
managed = {
domains = ["app.example.com"]
}
backend_service_self_link = module.backend_service.self_link
}
# Informational only — NOT a live Terraform reference. Read this module's ip_address
# output and supply it as a literal value once terraform-google-dns-zone is applied,
# potentially in a different Terraform state entirely.
module "dns_zone" {
source = "git::https://github.com/microsoftexpert/terraform-google-dns-zone.git?ref=v1.0.0"
#...
records = {
"app.example.com" = {
type = "A"
ttl = 300
rrdatas = [module.http_load_balancer.ip_address]
}
}
}💡 This wires the full four-module chain documented in SCOPE.md:
terraform-google-health-check'sself_linkoutput ->terraform-google-backend-service'shealth_check_self_linkinput;terraform-google-backend-service'sself_linkoutput -> this module'sbackend_service_self_linkinput; this module'sip_addressoutput -> aterraform-google-dns-zoneA record, informational only — DNS zone ownership and load-balancer front-door ownership are independent concerns that may not even live in the same Terraform state.
⚠️ Do not applydns_zoneand expect the certificate to provision instantly — DNS must propagate and Google's own domain-ownership validation must complete before the certificate reachesACTIVE(15-60+ minutes). See 🔍 Troubleshooting.
| Variable | Type | Required | Default | Notes |
|---|---|---|---|---|
name |
string |
Yes | — | Forwarding rule (keystone) name; force-new, RFC1035 |
description |
string |
No | null |
Forwarding rule description |
ip_protocol |
string |
No | "TCP" |
Enum: TCP, UDP, ESP, AH, SCTP, ICMP |
port_range |
string |
No | "443" |
Single port or start-end range |
load_balancing_scheme |
string |
No | "EXTERNAL_MANAGED" |
Departs from the provider's own EXTERNAL default |
address_name |
string |
Yes | — | Reserved global address name; force-new, RFC1035 |
address_description |
string |
No | null |
Address description |
address_type |
string |
No | "EXTERNAL" |
Enum: EXTERNAL, INTERNAL |
ip_version |
string |
No | null |
Enum: IPV4, IPV6 (null inherits provider default) |
ssl_certificate_name |
string |
Yes | — | Managed certificate name; effectively force-new |
ssl_certificate_description |
string |
No | null |
Certificate description |
managed |
object({ domains = list(string) }) |
Yes | — | 1-100 domains per the live schema |
https_proxy_name |
string |
Yes | — | Target HTTPS proxy name; force-new |
https_proxy_description |
string |
No | null |
Proxy description |
quic_override |
string |
No | "NONE" |
Enum: NONE, ENABLE, DISABLE |
ssl_policy |
string |
No | null |
Informational self_link reference; no owning module yet |
url_map_name |
string |
Yes | — | URL map name; force-new |
url_map_description |
string |
No | null |
URL map description |
host_rule |
list(object({...})) |
No | [] |
Validated against path_matcher names at plan time |
path_matcher |
list(object({...})) |
No | [] |
Names validated unique; path_rule nested list |
backend_service_self_link |
string |
Yes | — | URL map default_service; source: terraform-google-backend-service |
labels |
map(string) |
No | {} |
Applies to BOTH forwarding rule and address |
timeouts |
object({ create, update, delete = optional(string) }) |
No | null |
Keystone forwarding rule only |
Full variable schemas
variable "name" {
type = string
# RFC1035, 1-63 chars, enforced via validation {}
}
variable "description" {
type = string
default = null
}
variable "ip_protocol" {
type = string
default = "TCP"
}
variable "port_range" {
type = string
default = "443"
}
variable "load_balancing_scheme" {
type = string
default = "EXTERNAL_MANAGED"
}
variable "address_name" {
type = string
# RFC1035, 1-63 chars, enforced via validation {}
}
variable "address_description" {
type = string
default = null
}
variable "address_type" {
type = string
default = "EXTERNAL"
}
variable "ip_version" {
type = string
default = null
}
variable "ssl_certificate_name" {
type = string
# RFC1035, 1-63 chars, enforced via validation {}
}
variable "ssl_certificate_description" {
type = string
default = null
}
variable "managed" {
type = object({
domains = list(string)
})
# 1-100 domains, enforced via validation {}
}
variable "https_proxy_name" {
type = string
# RFC1035, 1-63 chars, enforced via validation {}
}
variable "https_proxy_description" {
type = string
default = null
}
variable "quic_override" {
type = string
default = "NONE"
}
variable "ssl_policy" {
type = string
default = null
}
variable "url_map_name" {
type = string
# RFC1035, 1-63 chars, enforced via validation {}
}
variable "url_map_description" {
type = string
default = null
}
variable "host_rule" {
type = list(object({
hosts = list(string)
path_matcher = string
description = optional(string)
}))
default = []
}
variable "path_matcher" {
type = list(object({
name = string
default_service = optional(string)
description = optional(string)
path_rule = optional(list(object({
paths = list(string)
service = optional(string)
})), [])
}))
default = []
}
variable "backend_service_self_link" {
type = string
# validated startswith("https://www.googleapis.com/compute/")
}
variable "labels" {
type = map(string)
default = {}
}
variable "timeouts" {
type = object({
create = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Sensitive? |
|---|---|---|
id |
Global forwarding rule resource id | No |
self_link |
Global forwarding rule self-link (URL form) | No |
ip_address |
Reserved global address's IP number — the single most operationally important output | No |
url_map_id |
URL map resource id | No |
https_proxy_id |
Target HTTPS proxy resource id | No |
ssl_certificate_id |
Managed SSL certificate resource id | No |
address_name |
Reserved global address's name | No |
Nothing secret-adjacent is output; no sensitive = true applies to any input or output in this
module.
- Managed SSL certificate provisioning is asynchronous and can take 15-60+ minutes after
applyreturns.applysucceeds as soon as the certificate object is created; Google's own domain-ownership validation then runs before the certificate reachesACTIVE. This plan-only library has no mechanism to poll for that. Every domain invar.managed.domainsmust already have DNS pointed atip_addressbefore thisapply, or validation stalls indefinitely. - Changing
var.managed.domains(orvar.ssl_certificate_name) is effectively force-new. GCP does not support in-place domain-list changes on a managed certificate.main.tfmitigates the resulting HTTPS-availability gap withlifecycle { create_before_destroy = true }on the certificate resource, mirroring Google's own official example usage — but the fresh 15-60+ minute provisioning window still applies to the replacement certificate. host_rule/path_matchermisconfiguration plans and applies cleanly but can misroute traffic. This is a real-traffic, apply-time-only risk —terraform validateconfirms only structural consistency (e.g. this module's own cross-field validation that everyhost_rule[*].path_matcherreferences a declaredpath_matcher[*].name), never routing-logic correctness. Use the resource's owntest {}block (a first-class, apply-time-checked validation feature — not modeled by this module's variables in v1.0.0) for any non-trivial routing configuration.- Recreating the backend service this module's
url_mapreferences can fail withresourceInUseByAnotherResource. A cross-module operational note:terraform-google-backend-servicemitigates this on its own end withlifecycle { create_before_destroy = true }; nothing in this module's own code can fix a recreate initiated upstream. - Network tier: a genuine grounding correction made during this authoring session.
google_compute_global_addressexposes NOnetwork_tierargument at all in the live v7.39.0 schema — the prior scaffolding pass's SCOPE.md assumed one existed.terraform validatecaught this directly during authoring (an "Unsupported argument" error). The forwarding rule's ownnetwork_tierargument does exist, but the only legal value for the global variant this module builds isPREMIUM— a single-legal-value enum offers no real caller choice, so it is left unset at that implicit default rather than modeled as a variable. - Labels are inconsistent across this module's own five resources.
google_compute_global_forwarding_ruleandgoogle_compute_global_addressboth support a top-levellabelsargument;google_compute_managed_ssl_certificate,google_compute_target_https_proxy, andgoogle_compute_url_mapdo not expose one at all — verified independently per resource. This module models ONE sharedvar.labelsapplied to both label-supporting resources, rather than a per-resource variable, to keep the variable surface minimal for a module that already renders five top-level resources. timeoutsscoped to the keystone only. Mirrorsterraform-google-managed-instance-group's identical precedent — the four supporting resources use the provider's own default timeouts (20-30 minutes each, per the live schema) rather than exposing four near-duplicate timeout variables.- v1.0.0 scope decision on
url_map: the live schema exposes substantially more routing surface than modeled here —test,default_url_redirect,default_route_action(and its fullroute_actionnested surface: CORS, fault injection, retry policy, timeout, URL rewrite, weighted backend services, cache policy),header_action, anddefault_custom_error_response_policy/custom_error_response_policyare all real, schema-confirmed fields deliberately deferred, not silently dropped. A future minor version can add any of them without a breaking change, sincehost_rule/path_matcherare additiveobjectschemas. - Same-module internal references use
self_link;ip_addressis a literal attribute.target,url_map, andssl_certificatesall wire via.self_link(the live schema's own "URL of..." / reference framing, matchingterraform-google-managed-instance-group's identical precedent).ip_addressis the address resource's ownaddresscomputed attribute — the literal reserved IP number, not an identity reference at all.
| Concern | Secure default | Opt-out (explicit) |
|---|---|---|
Load balancing scheme (load_balancing_scheme) |
EXTERNAL_MANAGED — the modern Global external Application Load Balancer scheme; departs from the provider's own EXTERNAL default, matching terraform-google-backend-service |
Caller sets EXTERNAL explicitly |
Address type (address_type) |
EXTERNAL — this module's entire purpose is an internet-reachable front door |
Caller sets INTERNAL (a different GCP product surface — Private Service Connect / VPC peering) |
| Network tier | Implicitly PREMIUM for both the address (no argument exists) and the forwarding rule (only legal value for the global variant) |
N/A — not overridable; STANDARD is not a legal choice for this resource combination |
QUIC negotiation (quic_override) |
NONE — the provider's own default; Google manages whether QUIC is used |
Caller sets ENABLE or DISABLE |
SSL policy (ssl_policy) |
null — GCP's own default SSL policy applies |
Caller supplies a google_compute_ssl_policy self_link |
| Deletion guard | Not applicable — none of this module's five resources exposes a deletion_protection argument in the live schema |
N/A |
| Labels | Applies to forwarding rule + address only — the only two resources whose schema supports labels |
N/A — ssl_certificate/https_proxy/url_map have no labels argument to opt into |
| Certificate recreate safety | lifecycle { create_before_destroy = true } on the certificate resource, added by this authoring session |
Not overridable by a module variable |
cd terraform-google-http-load-balancer
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 RFC1035 name
validations, the managed.domains count check, the host_rule/path_matcher cross-field reference
validation (every host_rule[*].path_matcher must match a declared path_matcher[*].name), and
correct resource/output wiring. terraform fmt -check confirms canonical formatting. Neither can
catch GCP API-level rejections — most importantly: managed-certificate domain-ownership validation
(only observable via gcloud, never via terraform plan), host_rule/path_matcher
routing-logic correctness (only the resource's own test {} block, checked at apply, catches
this), load_balancing_scheme argument-combination incompatibilities, quota limits, or org-policy
constraints. 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
address_name = "prod-app-lb-ip"
https_proxy_id = "projects/casey-prod-networking/global/targetHttpsProxies/prod-app-lb-proxy"
id = "projects/casey-prod-networking/global/forwardingRules/prod-app-lb"
ip_address = "34.120.45.10"
self_link = "https://www.googleapis.com/compute/v1/projects/casey-prod-networking/global/forwardingRules/prod-app-lb"
ssl_certificate_id = "projects/casey-prod-networking/global/sslCertificates/prod-app-lb-cert"
url_map_id = "projects/casey-prod-networking/global/urlMaps/prod-app-lb-url-map"
| Symptom | Cause | Fix |
|---|---|---|
HTTPS requests fail with a certificate error immediately after apply |
Managed certificate is still provisioning (15-60+ minutes), not necessarily a misconfiguration | Wait out the provisioning window; confirm every domain in var.managed.domains already had its DNS A/AAAA record pointing at ip_address before apply. Check status with gcloud compute ssl-certificates describe <name> |
Certificate stuck in PROVISIONING/FAILED_NOT_VISIBLE indefinitely |
DNS for one or more managed.domains entries does not point at ip_address, or was only updated after apply |
Point DNS at ip_address first, then apply (or re-trigger validation by ensuring DNS is correct and waiting) |
plan fails with "Unsupported argument: network_tier" on the address resource |
A stale assumption (corrected during this module's authoring) that google_compute_global_address has a network_tier argument |
Not applicable in the shipped module — this module does not set network_tier on the address; see Architecture Notes if forking |
apply fails with resourceInUseByAnotherResource |
The backend service var.backend_service_self_link points at was recreated while this module's url_map still referenced its old identity |
terraform-google-backend-service mitigates with lifecycle { create_before_destroy = true }; re-point backend_service_self_link to the new backend service in the same apply if it still occurs |
Requests are misrouted despite a clean plan/apply |
host_rule/path_matcher logic error (e.g. path-rule ordering, an unintended host wildcard match) |
Review routing rules manually; add the resource's own test {} block via a fork/extension for apply-time verification — this plan-only library cannot catch routing-logic mistakes |
plan fails: "host_rule[*].path_matcher must reference..." |
A host_rule entry's path_matcher value doesn't match any path_matcher[*].name |
Fix the typo, or add the missing path_matcher entry |
plan fails: managed.domains count validation |
var.managed.domains has 0 entries or more than 100 |
Supply between 1 and 100 domains |
Renaming ssl_certificate_name or changing managed.domains unexpectedly recreates the certificate and re-triggers the provisioning wait |
Both are effectively force-new on this resource | Treat as a planned-maintenance-window change; create_before_destroy reduces but does not eliminate the availability gap during the new certificate's provisioning window |
google_compute_global_forwarding_ruleprovider resource referencegoogle_compute_global_addressprovider resource referencegoogle_compute_managed_ssl_certificateprovider resource referencegoogle_compute_target_https_proxyprovider resource referencegoogle_compute_url_mapprovider resource referenceterraform-google-health-check(upstream ofterraform-google-backend-service)terraform-google-backend-service(upstream — suppliesvar.backend_service_self_link)terraform-google-dns-zone(downstream, informational — consumesip_addressas a literal value)terraform-google-project-services(must enablecompute.googleapis.combefore this module applies)- This module's
SCOPE.md