Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Google Cloud HTTP(S) Load Balancer Terraform Module

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). Targets hashicorp/google ~> 7.0, Terraform >= 1.12.0.

Terraform Google Provider Module Version Module Type Resources Posture


🧩 Overview

  • 🎯 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_each child collection at all: the four supporting resources are plain, unconditional, role-named resources — never for_each, never count — 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 from terraform-google-backend-service), wired straight through to the URL map's default_service.
  • 🌐 Reserves its own static IP (google_compute_global_address.address) rather than accepting one from a sibling module — the ip_address output 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 own EXTERNAL default, matching terraform-google-backend-service), address_type = "EXTERNAL", network tier implicitly PREMIUM (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 apply returns — 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.


❤️ Support this project

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

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


🗺️ Where this fits

graph LR
 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;
Loading

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).


🧬 What this builds

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;
Loading

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

✅ Provider / Versions

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

Schema notes that bite (verified against hashicorp/google v7.39.0 via terraform-registry, providerDocIDs 12683401 / 12683400 / 12683429 / 12683522 / 12683527):

  • google_compute_global_address has no network_tier argument 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 always PREMIUM tier; there is no field to configure otherwise. The forwarding rule's own network_tier argument does exist, but for the global variant this module builds, the only legal value is PREMIUM — this module leaves it unset at that implicit default rather than model a single-legal-value enum as a variable.
  • google_compute_url_map has no region argument — one early research result blended in prose belonging to google_compute_region_url_map, confirmed as contamination by reading the full live Argument Reference directly.
  • ssl_certificates on google_compute_target_https_proxy is schema-typed list(string), but this module always populates it with exactly one entry — this module's own certificate's self_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 in terraform-google-managed-instance-group. ip_address is the address resource's own address computed attribute (the literal reserved IP number) — not an identity form at all.
  • Labels are present on google_compute_global_forwarding_rule and google_compute_global_address but absent on google_compute_managed_ssl_certificate, google_compute_target_https_proxy, and google_compute_url_map — verified independently per resource, not assumed uniform.
  • Changing a managed certificate's managed.domains (or its name) is effectively force-new; main.tf adds lifecycle { create_before_destroy = true } on the certificate resource, mirroring Google's own official "Managed Ssl Certificate Recreation" example usage verbatim.

🔑 Required IAM Roles

  • roles/compute.loadBalancerAdmin on 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.)


☁️ GCP Prerequisites

  • compute.googleapis.com enabled on the target project (via terraform-google-project-services, applied before this module). This is the only Google API this module's five resources require.
  • certificatemanager.googleapis.com is explicitly NOT required. google_compute_managed_ssl_certificate is part of the classic Compute Engine sslCertificates API surface — the same compute.googleapis.com API used by every other resource in this module. It is a distinct resource from google_certificate_manager_certificate (the newer, separate Certificate Manager product, which does require certificatemanager.googleapis.com and 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_link must already exist (terraform-google-backend-service applied first).
  • Every domain listed in var.managed.domains must already have its DNS A/AAAA record pointing at this module's ip_address output before apply, or the managed certificate will stall indefinitely in PROVISIONING/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.vmExternalIpAccess targets VM instances.

📁 Module Structure

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

⚙️ Quick Start

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.


🔌 Cross-Module Contract

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

📚 Example Library

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_matcher supplied, every request is routed straight to var.backend_service_self_link via the URL map's default_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_tier variable on this module to override. google_compute_global_address has no network_tier argument at all in the live schema (a global address is implicitly always PREMIUM), and the forwarding rule's own network_tier argument allows only PREMIUM for the global variant this module builds. STANDARD is 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 apply returns as soon as the certificate object is created — the certificate itself then takes 15-60+ minutes to reach ACTIVE. Every domain in managed.domains must already have its DNS A/AAAA record pointing at this module's ip_address output before this apply runs, or Google's own domain-ownership validation stalls indefinitely in PROVISIONING/FAILED_NOT_VISIBLE (not a Terraform-visible error). A caller who tests HTTPS connectivity immediately after apply and 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_rule entries with service = null fall through to that path_matcher's own default_service (also null here), which in turn falls through to the URL map's own default_service (var.backend_service_self_link). host_rule[*].path_matcher must reference a declared path_matcher[*].name — this module validates that reference at plan time.

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_map host_rule/path_matcher misconfiguration 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 own test {} block (a first-class validation feature checked at apply time 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[*].service is optional — omitting it falls through to the enclosing path_matcher's own default_service, then to the URL map's default_service if 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_protocol defaults to TCP and port_range defaults to "443" (standard HTTPS). Override port_range for 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), matching terraform-google-backend-service's identical default. load_balancing_scheme should match the backend service's own scheme — an incompatible combination is caught only at apply, never at plan.

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_override defaults to NONE (Google manages whether QUIC is negotiated). ENABLE forces QUIC negotiation attempts; DISABLE forces 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_policy defaults to null (GCP's own default SSL policy applies). No terraform-google-ssl-policy module 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
}

ℹ️ labels applies to BOTH the forwarding rule and the reserved address — the only two of this module's five resources whose live schema exposes a labels argument. 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
}

ℹ️ timeouts applies 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_version applies to the reserved global address only. Leave null (the default) to inherit the provider's own IPV4 default.

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's self_link output -> terraform-google-backend-service's health_check_self_link input; terraform-google-backend-service's self_link output -> this module's backend_service_self_link input; this module's ip_address output -> a terraform-google-dns-zone A 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 apply dns_zone and expect the certificate to provision instantly — DNS must propagate and Google's own domain-ownership validation must complete before the certificate reaches ACTIVE (15-60+ minutes). See 🔍 Troubleshooting.


📥 Inputs

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
}

🧾 Outputs

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.


🧠 Architecture Notes

  • Managed SSL certificate provisioning is asynchronous and can take 15-60+ minutes after apply returns. apply succeeds as soon as the certificate object is created; Google's own domain-ownership validation then runs before the certificate reaches ACTIVE. This plan-only library has no mechanism to poll for that. Every domain in var.managed.domains must already have DNS pointed at ip_address before this apply, or validation stalls indefinitely.
  • Changing var.managed.domains (or var.ssl_certificate_name) is effectively force-new. GCP does not support in-place domain-list changes on a managed certificate. main.tf mitigates the resulting HTTPS-availability gap with lifecycle { 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_matcher misconfiguration plans and applies cleanly but can misroute traffic. This is a real-traffic, apply-time-only risk — terraform validate confirms only structural consistency (e.g. this module's own cross-field validation that every host_rule[*].path_matcher references a declared path_matcher[*].name), never routing-logic correctness. Use the resource's own test {} 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_map references can fail with resourceInUseByAnotherResource. A cross-module operational note: terraform-google-backend-service mitigates this on its own end with lifecycle { 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_address exposes NO network_tier argument at all in the live v7.39.0 schema — the prior scaffolding pass's SCOPE.md assumed one existed. terraform validate caught this directly during authoring (an "Unsupported argument" error). The forwarding rule's own network_tier argument does exist, but the only legal value for the global variant this module builds is PREMIUM — 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_rule and google_compute_global_address both support a top-level labels argument; google_compute_managed_ssl_certificate, google_compute_target_https_proxy, and google_compute_url_map do not expose one at all — verified independently per resource. This module models ONE shared var.labels applied 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.
  • timeouts scoped to the keystone only. Mirrors terraform-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 full route_action nested surface: CORS, fault injection, retry policy, timeout, URL rewrite, weighted backend services, cache policy), header_action, and default_custom_error_response_policy / custom_error_response_policy are all real, schema-confirmed fields deliberately deferred, not silently dropped. A future minor version can add any of them without a breaking change, since host_rule/path_matcher are additive object schemas.
  • Same-module internal references use self_link; ip_address is a literal attribute. target, url_map, and ssl_certificates all wire via .self_link (the live schema's own "URL of..." / reference framing, matching terraform-google-managed-instance-group's identical precedent). ip_address is the address resource's own address computed attribute — the literal reserved IP number, not an identity reference at all.

🧱 Design Principles

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

🚀 Runbook

cd terraform-google-http-load-balancer
terraform init -backend=false
terraform validate
terraform fmt -check

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


🧪 Testing

terraform validate confirms internal type and reference consistency: 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.


💬 Example Output

$ 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"

🔍 Troubleshooting

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

🔗 Related Docs

About

Terraform module: terraform-google-http-load-balancer

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages