Load Balancing, Cloud Armor & DNS
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#
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.comA 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-policyCloud 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-mapgcloud 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-routerA 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-v2Standing 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(page06) 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 createcommand 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.yamlurl-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-policyruns. - Creating a managed SSL certificate before DNS actually points at the reserved IP. See the IMPORTANT
callout, the certificate stalls in
PROVISIONINGuntil DNS resolves correctly. - Assuming Cloud Armor rule priority works like a normal list (first-defined wins). It's the numeric
priorityargument 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.