Verified8 commandsAI-assisted

Load Balancing, Cloud Armor & DNS

.md

Verified against Google Cloud SDK 553.0.0, flags verified via `gcloud compute backend-services · official docs

What it is and where it fits 🎯#

This page covers the three services that together answer "how does a request from the public internet actually reach my application, safely, at the right domain name": Cloud Load Balancing (routing and distributing traffic), Cloud Armor (Compute's security-policies, edge filtering and DDoS/WAF protection in front of a load balancer), and Cloud DNS (resolving a domain name to the load balancer's IP in the first place). None of this was covered on pages 02/03, which assumed traffic already knew how to reach a VM, GKE Service, or Cloud Run service directly. This page assumes the VPC/firewall basics from page 03 and the Compute/GKE/Cloud Run resources from page 02, since a load balancer's backends are exactly those resources.

Core concepts: anatomy of a global external HTTP(S) load balancer#

Diagram

Five distinct resources chain together to make one load balancer, and each is created and referenced separately: a health check (is a backend alive), a backend service (a pool of backends plus that health check plus, optionally, a Cloud Armor policy), a URL map (which backend service handles which host/path), a target proxy (terminates the protocol, holds the TLS certificate for HTTPS), and a forwarding rule (the actual public IP + port that ties everything together). This is meaningfully more moving parts than AWS's single elbv2 create-load-balancer or Azure's Application Gateway resource, the tradeoff for a URL map that can route many services under one IP/certificate without a separate load balancer per service.

Creating a global external HTTP(S) load balancer, end to end#

# 1. Reserve a static external IP (so DNS below has something stable to point at)
gcloud compute addresses create web-lb-ip --global

# 2. Health check the backends will be graded against
gcloud compute health-checks create http web-health-check \
  --port=8080 --request-path=/healthz --check-interval=10s --timeout=5s

# 3. Backend service, pointing at an existing managed instance group (page 02) or NEG
gcloud compute backend-services create web-backend \
  --global --protocol=HTTP --port-name=http \
  --health-checks=web-health-check --enable-cdn
gcloud compute backend-services add-backend web-backend \
  --global --instance-group=my-mig --instance-group-zone=us-central1-a \
  --balancing-mode=UTILIZATION --max-utilization=0.8

# 4. URL map: route everything to the one backend for now
gcloud compute url-maps create web-url-map --default-service=web-backend

# 5. Managed SSL certificate + HTTPS target proxy
gcloud compute ssl-certificates create web-cert --domains=app.example.com --global
gcloud compute target-https-proxies create web-https-proxy \
  --url-map=web-url-map --ssl-certificates=web-cert

# 6. Forwarding rule: the public entry point
gcloud compute forwarding-rules create web-forwarding-rule \
  --global --target-https-proxy=web-https-proxy \
  --address=web-lb-ip --ports=443

--enable-cdn on the backend service opts that backend into Cloud CDN edge caching, worth enabling for static/cacheable content and leaving off for anything dynamic per-request. A managed SSL certificate (ssl-certificates create --domains=, shown above) is provisioned and renewed by Google automatically once DNS is pointed at it, the modern default over uploading and rotating a certificate yourself.

Important

A managed SSL certificate stays in PROVISIONING state until the domain's DNS actually resolves to the load balancer's IP, not the other way around. The certificate step above and the DNS step below have a real ordering dependency: create the static IP first, point DNS at it, then the managed certificate can finish provisioning. Creating everything in the order shown, but forgetting to actually add the DNS record, is a common "load balancer works over HTTP but HTTPS never comes up" stall that looks like a certificate bug and isn't one.

Routing multiple services under one URL map#

gcloud compute url-maps add-path-matcher web-url-map \
  --path-matcher-name=api-matcher \
  --default-service=web-backend \
  --path-rules="/api/*=api-backend" \
  --new-hosts=app.example.com

A path matcher is what lets one load balancer, one IP, one certificate serve both a web frontend and an API backend under different paths (or different hosts, via --new-hosts/--existing-host), routing by longest-match on the path. This is the direct mechanism behind an "everything under one domain" architecture without needing a reverse proxy layer of your own in front of the actual services.

Cloud Armor: edge security policies#

gcloud compute security-policies create web-armor-policy \
  --type=CLOUD_ARMOR --description="Baseline edge protection for web-backend"

gcloud compute security-policies rules create 1000 \
  --security-policy=web-armor-policy \
  --expression="origin.region_code == 'CN' || origin.region_code == 'RU'" \
  --action=deny-403 --description="Block traffic from specific regions"

gcloud compute security-policies rules create 2000 \
  --security-policy=web-armor-policy \
  --src-ip-ranges="*" --action=throttle \
  --rate-limit-threshold-count=100 --rate-limit-threshold-interval-sec=60 \
  --conform-action=allow --exceed-action=deny-429 --enforce-on-key=IP

gcloud compute backend-services update web-backend \
  --global --security-policy=web-armor-policy

Cloud Armor rules evaluate in ascending priority-number order (lower number first, a lower priority number wins, the same convention AWS security group and NACL rule numbering uses), the first matching rule's action applies and evaluation stops there. --type=CLOUD_ARMOR is the standard backend-attached policy; CLOUD_ARMOR_EDGE (seen in the security-policies create help output) attaches at Cloud CDN's edge instead, for filtering that should happen before a request even reaches the backend service tier. A security policy does nothing until explicitly attached to a backend service with backend-services update --security-policy, creating one and forgetting that last step is a real, easy-to-make gap.

Tip

Reach for --action=throttle with a rate-limit-threshold (the second rule above) as the default posture for a public API's abuse protection, rather than reaching straight for an outright deny rule. Throttling degrades gracefully for a legitimate client that briefly exceeds the threshold; an outright deny rule has no such grace and is better reserved for traffic you're confident is never legitimate (a known bad IP range, a specific attack signature).

Cloud DNS: managed zones and records#

gcloud dns managed-zones create example-com-zone \
  --dns-name=example.com. --description="Primary zone for example.com" --visibility=public

gcloud dns record-sets create app.example.com. \
  --zone=example-com-zone --type=A --ttl=300 \
  --rrdatas=$(gcloud compute addresses describe web-lb-ip --global --format="value(address)")

gcloud dns record-sets create www.example.com. \
  --zone=example-com-zone --type=CNAME --ttl=300 --rrdatas=app.example.com.

gcloud dns managed-zones describe example-com-zone --format="value(nameServers)"

The final describe --format="value(nameServers)" is what you actually hand to the domain registrar (GoDaddy, Namecheap, Google Domains) to delegate the domain to Cloud DNS in the first place, that delegation step happens outside gcloud entirely, at the registrar, and is easy to forget when everything else here is scriptable. --visibility=public (the default) creates an internet-resolvable zone; --visibility=private with --networks= instead creates one resolvable only from specified VPCs, the pattern for internal service discovery that shouldn't be publicly resolvable at all.

Certificate Manager: the newer, more flexible certificate path#

gcloud certificate-manager certificates create app-cert \
  --domains=app.example.com --global

gcloud certificate-manager maps create web-cert-map --global
gcloud certificate-manager maps entries create web-cert-map-entry \
  --map=web-cert-map --certificates=app-cert --hostname=app.example.com

gcloud compute target-https-proxies update web-https-proxy \
  --certificate-map=web-cert-map

gcloud compute ssl-certificates create (used in the walkthrough above) is the simpler path, one certificate, directly attached to one target proxy. Certificate Manager is the newer, standalone service, worth reaching for once a single proxy needs to serve many domains on different certificates (a certificate map resolves the right certificate per hostname at request time) or when certificate lifecycle needs to be managed independently of any one load balancer.

Hybrid connectivity: Cloud VPN basics#

gcloud compute vpn-gateways create prod-vpn-gw --network=prod-network --region=us-central1
gcloud compute routers create prod-vpn-router --network=prod-network --region=us-central1 \
  --asn=65001

gcloud compute vpn-tunnels create prod-tunnel-1 \
  --region=us-central1 --vpn-gateway=prod-vpn-gw --interface=0 \
  --peer-external-gateway=on-prem-gw --peer-external-gateway-interface=0 \
  --ike-version=2 --shared-secret=<pre-shared-key> \
  --router=prod-vpn-router

A Highly Available VPN gateway is GCP's encrypted, internet-transiting connection back to an on-premises network or another cloud, the lighter-weight alternative to a dedicated Cloud Interconnect circuit, worth reaching for when the throughput/latency of an internet-routed IPsec tunnel is acceptable and a physical cross-connect isn't justified. This is a large enough topic (BGP peering via Cloud Router, redundant tunnels, Interconnect vs. VPN tradeoffs) that this page only covers the minimum to stand up a single tunnel; a network engineering-focused source is worth consulting before designing production hybrid connectivity.

Real-world scenario: zero-downtime backend swap during a migration#

A team is migrating web-backend's instances from one machine type to another and wants the load balancer to shift traffic gradually rather than all at once:

gcloud compute backend-services create web-backend-v2 \
  --global --protocol=HTTP --port-name=http --health-checks=web-health-check
gcloud compute backend-services add-backend web-backend-v2 \
  --global --instance-group=my-mig-v2 --instance-group-zone=us-central1-a

gcloud compute url-maps add-path-matcher web-url-map \
  --path-matcher-name=canary-matcher --default-service=web-backend \
  --backend-service-path-rules="/*=web-backend-v2" --existing-host=app.example.com

# After validating in production, cut over fully:
gcloud compute url-maps set-default-service web-url-map --default-service=web-backend-v2

Standing up a second backend service and routing to it via the URL map, rather than replacing instances inside the existing backend service, means the old backend stays fully intact and instantly reachable again by reverting the URL map's default service, a much faster rollback than recreating destroyed instances.

Real-world scenario: incident response — an IP range hammering the API#

Monitoring (page 06) pages the on-call engineer about a sudden 10x request spike, traced to a narrow IP range with no legitimate reason to generate that volume:

  • Confirm the source with gcloud logging read (page 06) filtered to the load balancer's own request logs, not just the application's, to see the traffic before it even reaches a backend
  • Add an immediate Cloud Armor deny rule at a low priority number so it's evaluated before the existing throttle rule: gcloud compute security-policies rules create 500 --security-policy=web-armor-policy --src-ip-ranges=<offending-range> --action=deny-403
  • Confirm the rule took effect by watching the backend service's request volume drop in Monitoring, not just trusting that the rules create command returned successfully
  • Remove the emergency rule once the incident is resolved, an accumulating pile of forgotten one-off deny rules is its own maintenance problem down the line

CI/CD integration recipe: validating a URL map change before it goes live#

# .github/workflows/lb-config-check.yml
name: Load balancer config validation
on:
  pull_request:
    paths: ["infra/load-balancer/**"]

permissions:
  id-token: write
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
          service_account: lb-validator@my-project-id.iam.gserviceaccount.com
      - uses: google-github-actions/setup-gcloud@v2
      - run: |
          gcloud compute url-maps validate --source=infra/load-balancer/url-map.yaml

url-maps validate catches a malformed path-matcher or a reference to a backend service that doesn't exist before it's ever applied, worth running as a pull-request gate on any repository that manages load balancer configuration as YAML rather than one-off gcloud commands.

Common pitfalls#

  • Forgetting to attach a Cloud Armor policy to the backend service after creating it. The policy exists but enforces nothing until backend-services update --security-policy runs.
  • Creating a managed SSL certificate before DNS actually points at the reserved IP. See the IMPORTANT callout, the certificate stalls in PROVISIONING until DNS resolves correctly.
  • Assuming Cloud Armor rule priority works like a normal list (first-defined wins). It's the numeric priority argument that decides evaluation order, not creation order.
  • Skipping the domain registrar delegation step. Creating a perfect Cloud DNS zone has no effect until the registrar's own nameserver records actually point at Cloud DNS's assigned nameservers.
  • Using a regional forwarding rule/backend service when a global one was actually needed, or vice versa. Global resources (used throughout this page) serve traffic across regions under one anycast IP; regional ones are cheaper and lower-latency for traffic that's genuinely confined to one region, picking the wrong scope means re-creating several chained resources, not a simple flag flip.

Exit codes#

0 success, non-zero on any API/validation error, a forwarding-rules create referencing a target proxy that doesn't yet exist fails immediately and clearly; a managed certificate stuck in PROVISIONING due to a DNS misconfiguration does not fail any command's exit code, since from the API's perspective it's still correctly waiting, gcloud compute ssl-certificates describe <name> --format="value(managed.status)" is the way to actually check that state.

When to reach for something else#

For a single service with no need for host/path-based routing to multiple backends, GKE's own Ingress resource or Cloud Run's built-in HTTPS endpoint (page 02) provisions equivalent load balancing with far less manual resource-chaining, reach for the manual pipeline on this page specifically when multiple distinct backend services need to share one IP/certificate, or when a feature (Cloud Armor, custom URL routing) isn't exposed through the higher-level abstraction. For declarative, reviewable load balancer and DNS provisioning across environments, prefer Terraform's google_compute_backend_service/ google_compute_url_map/google_compute_security_policy/google_dns_managed_zone resources over a growing shell script of the commands on this page, consistent with the IaC guidance on every earlier page.