Environment & Verification Matrix

Architectural TL;DR

When you set out to launch a new technical blog or project subdomain on Google Cloud, the natural first instinct is often to provision a separate, dedicated static-site stack. Resist that urge.

Every extra Global Forwarding Rule you spin up adds roughly $0.025/hour (~$18/month) to your cloud bill—an expense that quietly piles up as you add subdomains.

Instead, let’s refactor your Terraform static-site module to support an isolated_subdomains list. This approach lets you terminate traffic on a single shared Global Load Balancer IP (complete with native dual-stack IPv4 and IPv6 support) while keeping your storage buckets, deployment pipelines, and Cloud CDN backends neatly isolated from one another.

flowchart LR subgraph GCP Edge ["Google Cloud Edge (Global IP)"] Client([Dual-Stack Client]) -->|IPv4 / IPv6| LB[Global HTTPS Load Balancer] end LB -->|Host: example.com| RootBucket[(Root GCS Bucket)] LB -->|Host: blog.example.com| BlogBucket[(Blog GCS Bucket)]

1. The Terraform Module Refactor (isolated_subdomains)

To avoid duplicating expensive load-balancing infrastructure, we want our module to provision backend buckets and map routing rules dynamically.

Here is how we set this up: we split primary domains from isolated subdomains using locals, then leverage for_each and dynamic blocks to build out the GCS buckets, backend buckets, and URL map host rules automatically.

Dynamic Host Rules & Backend Buckets

Take a look at how this fits into the module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
locals {
  primary_domains = distinct(concat(
    [var.domain_name],
    length(split(".", var.domain_name)) == 2 ? ["www.${var.domain_name}"] : [],
    [for sub in var.subdomains : "${sub}.${var.domain_name}"]
  ))

  isolated_domain_list = [for sub in var.isolated_subdomains : "${sub}.${var.domain_name}"]

  all_domains = distinct(concat(local.primary_domains, local.isolated_domain_list))
}

# Isolated Subdomain GCS Buckets
resource "google_storage_bucket" "isolated_subdomain_site" {
  for_each                    = toset(var.isolated_subdomains)
  project                     = var.project_id
  name                        = "${each.key}-${replace(var.domain_name, ".", "-")}-site-data"
  location                    = "US"
  uniform_bucket_level_access = true
  
  website {
    main_page_suffix = "index.html"
    not_found_page   = "404.html"
  }
}

resource "google_compute_backend_backend" "isolated_site_backend" {
  for_each    = toset(var.isolated_subdomains)
  project     = var.project_id
  name        = "${each.key}-${replace(var.domain_name, ".", "-")}-backend"
  bucket_name = google_storage_bucket.isolated_subdomain_site[each.key].name
  enable_cdn  = true
}

# URL Map Routing via Dynamic Blocks
resource "google_compute_url_map" "site_url_map" {
  project         = var.project_id
  name            = "${replace(var.domain_name, ".", "-")}-url-map"
  default_service = google_compute_backend_bucket.site_backend.id

  dynamic "host_rule" {
    for_each = toset(var.isolated_subdomains)
    content {
      hosts        = ["${host_rule.value}.${var.domain_name}"]
      path_matcher = "${host_rule.value}-paths"
    }
  }

  dynamic "path_matcher" {
    for_each = toset(var.isolated_subdomains)
    content {
      name            = "${path_matcher.value}-paths"
      default_service = google_compute_backend_bucket.isolated_site_backend[path_matcher.value].id
    }
  }
}

2. Enabling Dual-Stack IPv4 & IPv6 at the Edge

A good habit when designing edge networking is future-proofing for IPv6 from day one. On GCP, you can allocate global IPv6 addresses and wire up their forwarding rules without incurring additional base IP fees within your forwarding rule quota.

Here is how you bind both address types to your HTTPS proxy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# --- Reserved Static IP Addresses (Dual-Stack) ---
resource "google_compute_global_address" "site_ipv4" {
  project    = var.project_id
  name       = "${local.bucket_name}-ip4"
  ip_version = "IPV4"
}

resource "google_compute_global_address" "site_ipv6" {
  project    = var.project_id
  name       = "${local.bucket_name}-ip6"
  ip_version = "IPV6"
}

# --- HTTPS Forwarding Rules ---
resource "google_compute_global_forwarding_rule" "https_forward_ipv4" {
  project    = var.project_id
  name       = "${local.bucket_name}-https-forward-ipv4"
  target     = google_compute_target_https_proxy.site_https_proxy.id
  port_range = "443"
  ip_address = google_compute_global_address.site_ipv4.address
}

resource "google_compute_global_forwarding_rule" "https_forward_ipv6" {
  project    = var.project_id
  name       = "${local.bucket_name}-https-forward-ipv6"
  target     = google_compute_target_https_proxy.site_https_proxy.id
  port_range = "443"
  ip_address = google_compute_global_address.site_ipv6.address
}

3. Root Configuration (infra-main)

Now let’s bring it all together in your root configuration.

Because we did the heavy lifting inside the module, declaring a new isolated subdomain (blog) alongside your base domain and mapping the corresponding DNS A and AAAA records takes just a few lines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module "static_site" {
  source              = "../terraform-gcp-modules/modules/terraform-gcp-static-site"
  project_id          = "example-prod"
  domain_name         = "example.com"
  subdomains          = ["www"]
  isolated_subdomains = ["blog"]

  common_labels = {
    environment = "production"
    owner       = "prata"
  }
}

module "dns" {
  source      = "../terraform-gcp-modules/modules/terraform-gcp-dns"
  project_id  = "example-prod"
  zone_name   = "example-zone"
  domain_name = "example.com"

  a_records = {
    "@"    = { ips = [module.static_site.load_balancer_ipv4] }
    "www"  = { ips = [module.static_site.load_balancer_ipv4] }
    "blog" = { ips = [module.static_site.load_balancer_ipv4] }
  }

  aaaa_records = {
    "@"    = { ips = [module.static_site.load_balancer_ipv6] }
    "www"  = { ips = [module.static_site.load_balancer_ipv6] }
    "blog" = { ips = [module.static_site.load_balancer_ipv6] }
  }
}

4. Verification & Receipts

Once your terraform apply completes, make it a habit to check the edge directly rather than assuming everything routed cleanly. Verify that your DNS resolves on both protocols and that Google has successfully provisioned certificates across all your Subject Alternative Names (SANs):

1
2
3
4
5
6
# Verify IPv4 and IPv6 resolution
dig A blog.example.com +short
dig AAAA blog.example.com +short

# Verify Google-Managed SSL Certificate status across all SANs
gcloud compute ssl-certificates list --project=example-prod --format="yaml(name,managed.status,managed.domainStatus)"

With this pattern in place, you get clean edge termination, automated HTTPS redirects, zero server maintenance, and an architecture that scales to handle multiple isolated subdomains for pennies on the dollar.