{"id":166177,"date":"2026-04-15T01:21:53","date_gmt":"2026-04-14T22:21:53","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=166177"},"modified":"2026-06-04T14:51:37","modified_gmt":"2026-06-04T11:51:37","slug":"gcp-shared-lb-cert-map-swap","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/gcp-shared-lb-cert-map-swap\/","title":{"rendered":"Consolidate GCP Certs on a Shared LB with Cert Maps"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Per-service ManagedCertificate attached to a per-service target HTTPS proxy is why you have 120 forwarding rules across 4 environments. Certificate Maps are the mechanism that collapses all of that onto a single LB, and in Terraform they come with a specific trap: swapping from <code>ssl_certificates<\/code> to <code>certificate_map<\/code> on an existing target HTTPS proxy forces a full resource replacement. This article walks through building a shared Global External HTTPS LB with a Certificate Map in Terraform, attaching four cert map entries (PRIMARY + three hostnames) against the <a href=\"https:\/\/computingforgeeks.com\/gcp-cert-manager-wildcard-dns-auth\/\" target=\"_blank\" rel=\"noreferrer noopener\">wildcard cert from the previous article<\/a>, verifying HSTS at the backend, and spelling out the three safe patterns to swap a target proxy over without dropping connections.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You also hit, hands-on, the wildcard sub-subdomain gotcha that breaks <code>api.food.cfg-lab.computingforgeeks.com<\/code> even though the wildcard technically covers <code>*.cfg-lab.computingforgeeks.com<\/code>. The fix is a second cert and an explicit cert map entry, not a bigger wildcard.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Tested April 2026 on Google Cloud, Certificate Manager (global), Compute Engine GXLB, Terraform 1.9.8 + google provider 6.12, OpenTofu 1.10<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What a Certificate Map Actually Does<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The older path on GCP attaches certs directly to a target HTTPS proxy via <code>ssl_certificates<\/code>. That list has a hard ceiling of 15 certs per proxy. Past 15 you&#8217;re forced to provision another LB, another forwarding rule, another IP, and soon you&#8217;ve built the per-service sprawl you were trying to escape.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A Certificate Map is a named routing layer that sits between the target proxy and the certs. The proxy references one cert map. The cert map holds N entries. Each entry is <code>(hostname) -&gt; cert<\/code>, plus one <code>PRIMARY<\/code> entry that serves as the fallback when no hostname matches. There&#8217;s no per-proxy cert limit any more; the map scales to thousands of entries. And one cert map can feed multiple target proxies, which is how a cert inventory stays DRY across regions and environments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this article, the single <code>*.cfg-lab.computingforgeeks.com<\/code> cert from the previous article gets attached four times: once as <code>PRIMARY<\/code>, and once per real hostname (<code>food<\/code>, <code>admin<\/code>, <code>api<\/code>). The explicit entries let the LB pick the right cert from SNI without relying on the fallback chain.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The target state for this article is one LB serving three hostnames:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>One Global External HTTPS LB (anycast IP, 3-hop path through Google&#8217;s backbone)<\/li>\n\n\n\n<li>One URL map with host-based routing<\/li>\n\n\n\n<li>One Cert Map with four entries against the wildcard cert<\/li>\n\n\n\n<li>One Target HTTPS Proxy pointing at the URL map and the cert map<\/li>\n\n\n\n<li>One Forwarding Rule on port 443 with a global anycast IP<\/li>\n\n\n\n<li>One backend bucket holding a demo static site<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Before this article, each service in the per-service sprawl pattern had its own static IP, own target proxy, own cert, own forwarding rule. After this article, the three services share one of each. The wildcard cert from the previous article covers all three hostnames; the cert map is what lets the LB choose it on every TLS handshake.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A Google-managed wildcard cert in <code>ACTIVE<\/code> state (from the previous article in this series)<\/li>\n\n\n\n<li>Cloud DNS zone where you can write A records (<code>cfg-lab<\/code>)<\/li>\n\n\n\n<li>A GCS bucket with a static <code>index.html<\/code> (used as the shared demo backend)<\/li>\n\n\n\n<li>Project roles: <code>roles\/compute.networkAdmin<\/code>, <code>roles\/certificatemanager.editor<\/code><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">The Terraform Module<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everything lives in <code>modules\/gxlb\/<\/code>. The interesting resources are the cert map and its entries. A cert map entry is either <code>PRIMARY<\/code> (the default cert, no hostname match needed) or hostname-specific:<\/p>\n\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_certificate_manager_certificate_map\" \"this\" {\n  project = var.project_id\n  name    = \"${var.name}-cert-map\"\n}\n\nresource \"google_certificate_manager_certificate_map_entry\" \"this\" {\n  for_each = var.certificate_ids\n\n  project      = var.project_id\n  name         = each.key\n  map          = google_certificate_manager_certificate_map.this.name\n  certificates = &#91;each.value.certificate_id]\n\n  hostname = each.value.hostname\n  matcher  = each.value.hostname == null ? \"PRIMARY\" : null\n}<\/code><\/pre>\n\n\n<p>A PRIMARY entry sets <code>matcher = \"PRIMARY\"<\/code> with <code>hostname = null<\/code>. A hostname entry sets <code>hostname = \"food.cfg-lab...\"<\/code> with <code>matcher = null<\/code>. One or the other, never both.<\/p>\n<p>The target HTTPS proxy references the cert map through a specific URL scheme:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_compute_target_https_proxy\" \"this\" {\n  project         = var.project_id\n  name            = \"${var.name}-https-proxy\"\n  url_map         = google_compute_url_map.this.id\n  certificate_map = \"\/\/certificatemanager.googleapis.com\/${google_certificate_manager_certificate_map.this.id}\"\n\n  depends_on = &#91;google_certificate_manager_certificate_map_entry.this]\n}<\/code><\/pre>\n\n\n<p>The <code>\/\/certificatemanager.googleapis.com\/<\/code> prefix is not optional. Leave it out and the API rejects the update with a confusing <code>invalid resource<\/code> error. The <code>depends_on<\/code> ensures every cert map entry exists before the proxy comes up, which prevents a brief window where the LB serves the fallback cert for a hostname that should match an explicit entry.<\/p>\n<h2>HSTS at the Backend<\/h2>\n<p>HSTS is a response header, not an LB primitive. For a backend bucket, set it as a custom response header:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_compute_backend_bucket\" \"this\" {\n  project     = var.project_id\n  name        = \"${var.name}-backend\"\n  bucket_name = var.backend_bucket_name\n\n  custom_response_headers = &#91;\n    \"Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\",\n  ]\n}<\/code><\/pre>\n\n\n<p>For backend services (fronting GKE or Cloud Run), the field is <code>custom_response_headers<\/code> on <code>google_compute_backend_service<\/code>. Same syntax, same semantics.<\/p>\n<h3>About the <code>preload<\/code> flag<\/h3>\n<p>The <code>preload<\/code> token is a submission flag, not an automatic opt-in. Sending it in the header does not enrol the domain in Chrome&#8217;s HSTS preload list. That happens at <a href=\"https:\/\/hstspreload.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">hstspreload.org<\/a>, and submission is effectively one-way: getting off the list is a months-long process that propagates with browser releases. Check the pre-submission checklist there before submitting: every subdomain has to serve HTTPS correctly, HTTP needs to redirect, <code>includeSubDomains<\/code> must cover every subdomain you plan to keep, and you can&#8217;t rely on an insecure subdomain existing ever again.<\/p>\n<p>A quieter win: <code>.dev<\/code> and <code>.app<\/code> TLDs are preloaded at the TLD level by default. If any of your services live under <code>.dev<\/code>, they already enforce HTTPS in modern browsers with no submission required.<\/p>\n<h2>Applying the Stack<\/h2>\n<p>Terragrunt wires the module against the wildcard cert and creates four cert map entries:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>inputs = {\n  name                = \"cfg-lab\"\n  backend_bucket_name = \"cfg-lab-demo-your-gcp-project-id\"\n\n  certificate_ids = {\n    \"primary-wildcard\" = {\n      certificate_id = dependency.certs.outputs.certificate_ids&#91;\"cfg-lab-wildcard\"]\n      hostname       = null\n    }\n    \"food-wildcard\" = {\n      certificate_id = dependency.certs.outputs.certificate_ids&#91;\"cfg-lab-wildcard\"]\n      hostname       = \"food.cfg-lab.computingforgeeks.com\"\n    }\n    \"admin-wildcard\" = {\n      certificate_id = dependency.certs.outputs.certificate_ids&#91;\"cfg-lab-wildcard\"]\n      hostname       = \"admin.cfg-lab.computingforgeeks.com\"\n    }\n    \"api-wildcard\" = {\n      certificate_id = dependency.certs.outputs.certificate_ids&#91;\"cfg-lab-wildcard\"]\n      hostname       = \"api.cfg-lab.computingforgeeks.com\"\n    }\n  }\n}<\/code><\/pre>\n\n\n<p>Run it:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>cd live\/article-lab\/europe-west1\/gxlb-cfg-lab\nterragrunt apply -auto-approve<\/code><\/pre>\n\n\n<p>The whole stack comes up in about 85 seconds. The global forwarding rule is the slowest single resource, taking about 25 seconds to propagate across edge PoPs. Creation timings from a real run:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-apply.png\" alt=\"Terminal showing Terraform apply for cert map entries backend bucket URL map target proxy and forwarding rule\" class=\"wp-image-166174\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-apply.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-apply-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-apply-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>Point the three hostnames at the LB IP with DNS A records in the delegated <code>cfg-lab<\/code> zone:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>LB_IP=$(terragrunt output -raw ip_address)\nfor host in food admin api; do\n  gcloud dns record-sets create ${host}.cfg-lab.computingforgeeks.com. \\\n    --zone=cfg-lab --type=A --ttl=300 --rrdatas=$LB_IP\ndone<\/code><\/pre>\n\n\n<p>Give the LB a couple of minutes to propagate to all edge locations. Then verify.<\/p>\n<p>In the Cloud console, the Cert Map page shows all four entries tied to the shared wildcard cert. Primary plus one explicit hostname entry per service, zero expired, zero failed:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2512\" height=\"1446\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-certificate-map-entries-shared-load-balancer.png\" alt=\"Google Cloud Console Certificate Map cfg-lab-cert-map with PRIMARY and 3 hostname entries\" class=\"wp-image-166249\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-certificate-map-entries-shared-load-balancer.png 2512w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-certificate-map-entries-shared-load-balancer-300x173.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-certificate-map-entries-shared-load-balancer-1024x589.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-certificate-map-entries-shared-load-balancer-768x442.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-certificate-map-entries-shared-load-balancer-1536x884.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-certificate-map-entries-shared-load-balancer-2048x1179.png 2048w\" sizes=\"auto, (max-width: 2512px) 100vw, 2512px\" \/><\/figure>\n\n\n<p>The Load Balancer detail page brings the full picture together: HTTPS frontend on the anycast IP, the cert map reference, four host rules routing to the shared backend bucket. This is the single view that replaces three separate LB pages from the pre-consolidation state:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2546\" height=\"1554\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-global-https-load-balancer-cert-map-backend.png\" alt=\"Google Cloud Console Load Balancer details showing HTTPS frontend cert map and 4 host rules\" class=\"wp-image-166250\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-global-https-load-balancer-cert-map-backend.png 2546w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-global-https-load-balancer-cert-map-backend-300x183.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-global-https-load-balancer-cert-map-backend-1024x625.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-global-https-load-balancer-cert-map-backend-768x469.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-global-https-load-balancer-cert-map-backend-1536x938.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-global-https-load-balancer-cert-map-backend-2048x1250.png 2048w\" sizes=\"auto, (max-width: 2546px) 100vw, 2546px\" \/><\/figure>\n\n\n<h2>Verifying TLS and HSTS<\/h2>\n<p>Check the cert served for the food hostname:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>echo | openssl s_client -connect food.cfg-lab.computingforgeeks.com:443 \\\n  -servername food.cfg-lab.computingforgeeks.com 2&gt;\/dev\/null \\\n  | openssl x509 -noout -subject -issuer -ext subjectAltName<\/code><\/pre>\n\n\n<p>The subject is <code>CN=*.cfg-lab.computingforgeeks.com<\/code>, the issuer is Google Trust Services, and the SAN list includes both the wildcard and the apex. Same cert, same issuer, across all three hostnames.<\/p>\n<p>Check HSTS with curl:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>for h in food admin api; do\n  curl -sI https:\/\/$h.cfg-lab.computingforgeeks.com\/ | grep -E \"HTTP|strict-transport\"\ndone<\/code><\/pre>\n\n\n<p>Every response carries <code>strict-transport-security: max-age=31536000; includeSubDomains; preload<\/code> and a clean <code>HTTP\/2 200<\/code>. Three hostnames, one LB, one cert, one map. This is the state that lets you destroy the three per-service LBs and reclaim their forwarding rules.<\/p>\n<p>The browser padlock is the reader-facing proof. Clicking it on any of the three hostnames opens the certificate viewer showing the same wildcard SAN, issued by Google Trust Services, valid for the next three months:<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"2312\" height=\"1534\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-wildcard-cert-browser-padlock-food-cfg-lab.png\" alt=\"Browser certificate viewer showing wildcard cert issued by Google Trust Services for food cfg-lab\" class=\"wp-image-166253\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-wildcard-cert-browser-padlock-food-cfg-lab.png 2312w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-wildcard-cert-browser-padlock-food-cfg-lab-300x199.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-wildcard-cert-browser-padlock-food-cfg-lab-1024x679.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-wildcard-cert-browser-padlock-food-cfg-lab-768x510.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-wildcard-cert-browser-padlock-food-cfg-lab-1536x1019.png 1536w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gcp-wildcard-cert-browser-padlock-food-cfg-lab-2048x1359.png 2048w\" sizes=\"auto, (max-width: 2312px) 100vw, 2312px\" \/><\/figure>\n\n\n<h2>The Sub-Subdomain Gotcha<\/h2>\n<p>Try to reach <code>api.food.cfg-lab.computingforgeeks.com<\/code> through the same LB, using <code>--resolve<\/code> so DNS doesn&#8217;t get in the way:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>curl --resolve api.food.cfg-lab.computingforgeeks.com:443:34.36.168.166 \\\n  https:&#47;&#47;api.food.cfg-lab.computingforgeeks.com\/<\/code><\/pre>\n\n\n<p>curl refuses to continue:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>curl: (60) SSL: no alternative certificate subject name matches\n       target hostname 'api.food.cfg-lab.computingforgeeks.com'<\/code><\/pre>\n\n\n<h3>Error: &#8220;SSL: no alternative certificate subject name matches target hostname&#8221;<\/h3>\n<p>RFC 6125 says a wildcard cert matches exactly one label. <code>*.cfg-lab.computingforgeeks.com<\/code> covers <code>food.cfg-lab.computingforgeeks.com<\/code> but not <code>api.food.cfg-lab.computingforgeeks.com<\/code>. Every compliant TLS client (every browser, curl, openssl) enforces this strictly. The LB happily returned the wildcard because PRIMARY fallback served it on SNI miss; the client rejected it on name validation.<\/p>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"920\" height=\"800\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-gotcha.png\" alt=\"Terminal showing openssl s_client wildcard cert SAN and curl SSL error for sub-subdomain\" class=\"wp-image-166175\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-gotcha.png 920w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-gotcha-300x261.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-gxlb-gotcha-768x668.png 768w\" sizes=\"auto, (max-width: 920px) 100vw, 920px\" \/><\/figure>\n\n\n<p>Two ways to fix this, neither is &#8220;use a bigger wildcard&#8221; because there&#8217;s no such thing in RFC 6125:<\/p>\n<ol>\n<li>Issue a second wildcard, one level deeper: <code>*.food.cfg-lab.computingforgeeks.com<\/code>. Add it to the cert map with an explicit hostname entry. Use this when there are many services under the <code>food<\/code> subzone.<\/li>\n<li>Issue a single-name cert for the exact hostname and attach it with an explicit entry. Use this when the sub-subdomain is rare.<\/li>\n<\/ol>\n<p>Both paths are mechanical extensions of the previous article&#8217;s DNS authorization. The deeper wildcard needs its own authorization on <code>food.cfg-lab.computingforgeeks.com<\/code>; the single-name cert needs an authorization on the same apex. Either way, the cert map grows by one entry.<\/p>\n<h2>The Terraform Swap Trap<\/h2>\n<p>Existing LBs you already have in production were almost certainly built with <code>ssl_certificates<\/code> directly on the target HTTPS proxy. Switching the same proxy over to <code>certificate_map<\/code> is the single migration step that actually collapses cert sprawl. It&#8217;s also the step that can drop connections for 20-30 seconds if you run a naive <code>terraform apply<\/code>.<\/p>\n<p>The reason is architectural: the Compute API treats <code>ssl_certificates<\/code> and <code>certificate_map<\/code> as mutually exclusive fields on a target HTTPS proxy, and Terraform&#8217;s google provider marks changes to either as ForceReplacement. A ForceReplacement deletes the proxy and creates a new one. Until the new one is ready, the forwarding rule has no target, and every TLS handshake fails.<\/p>\n<p>Three patterns, ranked by operational safety.<\/p>\n<h3>Method 1: Naive apply (don&#8217;t)<\/h3>\n<p>The default plan looks like this:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>  # google_compute_target_https_proxy.this must be replaced\n-\/+ resource \"google_compute_target_https_proxy\" \"this\" {\n      ~ ssl_certificates = &#91;...] -&gt; null\n      + certificate_map  = \"\/\/certificatemanager.googleapis.com\/...\"\n    }<\/code><\/pre>\n\n\n<p>Terraform destroys the old proxy, then creates the new one, then updates the forwarding rule&#8217;s target. Observed gap on a real run: 20-25 seconds where the forwarding rule has no proxy attached. During that window, every new TLS handshake fails with connection reset. Existing sessions already in the middle of transferring bytes continue, because TLS session resumption is still on the old path in Google&#8217;s fleet until the session expires, but new connections fail hard.<\/p>\n<p>Never do this on a live LB.<\/p>\n<h3>Method 2: create_before_destroy<\/h3>\n<p>The Terraform lifecycle block forces Terraform to create the replacement first, then destroy the old one. On the target proxy:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_compute_target_https_proxy\" \"this\" {\n  # ...\n  lifecycle {\n    create_before_destroy = true\n  }\n}<\/code><\/pre>\n\n\n<p>Terraform creates a second proxy with a new name, repoints the forwarding rule at it, then destroys the old proxy. The forwarding rule switch is atomic. Observed gap: sub-second. On a 10 RPS curl loop during the swap, one to zero requests fail depending on timing.<\/p>\n<p>The catch: names have to be unique per project. Terraform handles this by changing the name during replacement, which means either the proxy name has a random suffix, or the create_before_destroy machinery uses a different name scheme. Plan carefully; name collisions during replacement cause the apply to fail half-way.<\/p>\n<h3>Method 3: blue-green (zero blip)<\/h3>\n<p>Build a second target proxy side-by-side, using a different resource name in Terraform. The old proxy stays up. Flip the forwarding rule&#8217;s <code>target<\/code> from proxy-a to proxy-b in a second apply. Destroy proxy-a in a third apply after a soak period.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>resource \"google_compute_target_https_proxy\" \"old\" {\n  name             = \"${var.name}-https-proxy-legacy\"\n  url_map          = google_compute_url_map.this.id\n  ssl_certificates = &#91;google_compute_managed_ssl_certificate.legacy.id]\n}\n\nresource \"google_compute_target_https_proxy\" \"new\" {\n  name            = \"${var.name}-https-proxy\"\n  url_map         = google_compute_url_map.this.id\n  certificate_map = \"\/\/certificatemanager.googleapis.com\/${google_certificate_manager_certificate_map.this.id}\"\n}\n\nresource \"google_compute_global_forwarding_rule\" \"https\" {\n  # Flip this in a second apply once proxy.new is healthy\n  target = google_compute_target_https_proxy.new.id\n}<\/code><\/pre>\n\n\n<p>Zero connection blip because the forwarding rule retarget is atomic in the Compute API: Google processes the update as a single state transition, not a destroy-create. A 10 RPS curl loop through the retarget shows no failures.<\/p>\n<p>This is the right pattern for any production swap. The cost is temporary: two target proxies exist for the soak period, which is free (target proxies don&#8217;t cost anything by themselves). Pay the extra complexity for the zero-blip guarantee.<\/p>\n<h2>Long-Term SSL Session Behavior During the Swap<\/h2>\n<p>Google&#8217;s LBs terminate TLS at the edge PoP. When you swap a target proxy, existing TLS sessions that already completed a handshake keep working until the session resumption TTL expires (configurable on the SSL policy; defaults are measured in hours). What the swap breaks is <em>new<\/em> handshakes. If you run a live <code>curl --tls-max 1.3<\/code> loop through the three methods, the failure pattern on Method 1 is sharp: a clean step where every new connection fails for 20 seconds, then recovery. Method 2 is a single-second dip. Method 3 shows a flat line.<\/p>\n<p>This distinction matters for incident response. If you see &#8220;some users affected&#8221; during a cert rotation, and the user complaints are clustered around 20-30 seconds of downtime, you ran Method 1. If you see &#8220;everyone affected&#8221; for minutes, the CAA policy or DNS authorization is broken and no amount of swap cleverness helps.<\/p>\n<h2>Reclaiming Resources<\/h2>\n<p>With the shared LB serving three hostnames, the three per-service LBs from the original sprawl can go. Each one was:<\/p>\n<ul>\n<li>One static IP ($7.30\/month when unattached)<\/li>\n<li>One global forwarding rule ($18.26\/month)<\/li>\n<li>One target HTTPS proxy (free)<\/li>\n<li>One ManagedCertificate or google_compute_managed_ssl_certificate (free)<\/li>\n<\/ul>\n<p>Three services \u00d7 $25.56\/month = about $77\/month saved. Extrapolate to 30 services and 4 environments as stated in the series opener: the math gets ugly fast. Cert map consolidation is not a security win, it&#8217;s a fleet hygiene and cost win. The security win happens later, when Private CA and cert pinning enter the picture for financial services.<\/p>\n<h2>Cleanup<\/h2>\n<p>The stack is a series foundation, not an ephemeral lab. Leave it up for the next article&#8217;s work. When you do tear down, destroy in module order so the forwarding rule goes before the target proxy before the cert map entries:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>terragrunt destroy<\/code><\/pre>\n\n\n<p>Terragrunt handles the ordering from the module&#8217;s dependency graph. Cert map entries with PRIMARY matchers clean up first; hostname entries clean up in parallel.<\/p>\n<h2>What&#8217;s Next<\/h2>\n<p>With shared LB + cert map live, the next article in the series migrates the GKE-side workloads from per-service <code>ManagedCertificate<\/code> + Ingress to the Gateway API, attaching the same wildcard cert through the Gateway-level annotation. That closes the consolidation loop on the GKE side.<\/p>","protected":false},"excerpt":{"rendered":"<p>Per-service ManagedCertificate attached to a per-service target HTTPS proxy is why you have 120 forwarding rules across 4 environments. Certificate Maps are the mechanism that collapses all of that onto a single LB, and in Terraform they come with a specific trap: swapping from ssl_certificates to certificate_map on an existing target HTTPS proxy forces a &#8230; <a title=\"Consolidate GCP Certs on a Shared LB with Cert Maps\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/gcp-shared-lb-cert-map-swap\/\" aria-label=\"Read more about Consolidate GCP Certs on a Shared LB with Cert Maps\">Read more<\/a><\/p>\n","protected":false},"author":3,"featured_media":166176,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2680,36939,55],"tags":[36175],"cfg_series":[39797],"class_list":["post-166177","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cloud","category-gcp","category-networking","tag-gcp","cfg_series-gcp-shared-traffic"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166177","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=166177"}],"version-history":[{"count":3,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166177\/revisions"}],"predecessor-version":[{"id":166329,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166177\/revisions\/166329"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/166176"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=166177"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=166177"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=166177"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=166177"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}