GCP Lesson 27 of 98

Engineering the Global External Application Load Balancer on GCP

In a nutshell

A load balancer is the single front door that catches every request to your app and hands it to a healthy server that can answer it. The global external Application Load Balancer (ALB) is Google Cloud’s biggest, smartest version of that door: one internet address that works from anywhere on Earth, in front of servers spread across many regions, with a caching layer and a web firewall built into the doorway itself.

Picture a global hotel chain with one phone number. A guest in Tokyo and a guest in London both dial the same number, but each call is answered by the nearest front desk — that shared number is the global anycast IP. The operator who picks up decides where to route you: room service to the kitchen, housekeeping to the cleaning team — that is the URL map, routing by what you asked for. Before the operator even connects you, a security guard at the door turns away obvious troublemakers (that is Cloud Armor, the web firewall), and a concierge answers common questions instantly from a printed sheet instead of calling the back office every time (that is Cloud CDN, the cache). If one city’s front desk is overwhelmed or closed for the night, the same number quietly rings the next-nearest desk — that is capacity-based cross-region routing and failover.

The thing that trips people up is that it is not one resource. It is a chain of five: a forwarding rule → a target proxy → a URL map → a backend service → the actual backends (NEGs or instance groups). Each link is a separate object you create and tune, and each one quietly controls something that shows up later as tail latency, blast radius during an outage, or a surprise on the bill. Get the chain right and you have a fast, global, self-defending front door. Get one link wrong — a regional forwarding rule instead of global, the wrong load-balancing scheme, FORCE_CACHE_ALL near a login page — and it silently misbehaves in ways that are maddening to debug.

Why a beginner should care: almost every internet-facing GCP app sits behind this thing. Understanding the chain is the difference between “I clicked buttons until it worked” and “I know exactly which of the five links is misbehaving and why.”

Level: Advanced (the mental model is beginner-friendly; the tuning is deep) · Time: ~40 min to read and work the challenges

Global external Application Load Balancer: how a request flows from the anycast VIP through the forwarding rule, target proxy, URL map, and backend service to instance-group, serverless, and hybrid NEG backends

Read the diagram left → right as the life of one request: it arrives at a single global anycast IP at the nearest Google edge, the forwarding rule hands it to a target HTTPS proxy that terminates TLS, the URL map routes it by host/path/header, Cloud CDN and Cloud Armor (both attached to the backend service) serve-from-cache or block-at-edge, and finally the backend service picks the closest region with spare capacity and sends it to a backend — an instance group, a serverless NEG, or a hybrid/internet NEG.

Prerequisites and what you’ll be able to do

Know these first. You should be comfortable with the basics of how an HTTP request works (client, TLS, host header, path), and with GCP networking fundamentals — a VPC, a subnet, a firewall rule, and the idea of regions and zones. You should know what a managed instance group (MIG) is (a fleet of identical VMs behind one handle) and, ideally, have met Cloud Run as a way to run containers. If any of that is fuzzy, these sibling lessons set it up:

After this lesson you will be able to:

The global external Application Load Balancer (ALB) is the front door for most internet-facing workloads on GCP, and it is deceptively deep. It is not one resource but a graph: a global anycast IP fronts a target proxy, which references a URL map, which routes to backend services, which point at backends spread across regions. Each layer has tuning knobs that quietly determine your tail latency, your blast radius during a regional outage, and your bill. This walkthrough builds one end to end against the Envoy-based global ALB, then layers on the controls a platform team needs in production: balancing modes, header-based routing, hybrid NEGs, Cloud CDN, Cloud Armor at the edge, mTLS, and the observability to debug it at 2 a.m.

This is the global external Application Load Balancer, the modern successor to the classic HTTP(S) load balancer: global anycast, Envoy data plane, advanced traffic management. Its components live in the global scope, which matters for every command below.

Step 1: Understand the resource graph

Before any gcloud, internalize the chain. A request flows through five distinct resources:

Resource Role Scope
Forwarding rule Binds the global anycast IP + port to a target proxy Global
Target proxy Terminates TLS (HTTPS proxy) or plain HTTP; references the URL map Global
URL map Routes by host/path/header/query to backend services Global
Backend service Health checks, balancing mode, CDN, Cloud Armor, timeouts Global
Backend (NEG/MIG) The actual endpoints: instance groups, serverless, hybrid, internet Regional or zonal

Two rules save hours of confusion. First, the global ALB uses global backend services and forwarding rules; create a regional forwarding rule by accident and you get the regional ALB, a different product with no global anycast. Second, Cloud CDN and Cloud Armor attach to the backend service, not the proxy, so caching and WAF policy are per-backend, not per-frontend.

# Reserve the global anycast IP first; everything else references it.
gcloud compute addresses create web-ip \
  --ip-version=IPV4 \
  --network-tier=PREMIUM \
  --global

Premium tier is mandatory for the global ALB; Standard tier only supports regional load balancing. The --global flag is the tell that you are on the right product in every command below.

Step 2: Build and tune backend services

The backend service is where most production decisions live. Start with a health check and a service, then tune the balancing behavior.

gcloud compute health-checks create http web-hc \
  --port=8080 \
  --request-path="/healthz" \
  --check-interval=5s \
  --timeout=5s \
  --healthy-threshold=2 \
  --unhealthy-threshold=3 \
  --global

gcloud compute backend-services create web-bes \
  --protocol=HTTP \
  --port-name=http \
  --health-checks=web-hc \
  --global \
  --load-balancing-scheme=EXTERNAL_MANAGED \
  --timeout=30s

EXTERNAL_MANAGED is the load balancing scheme for the Envoy-based global ALB. The older EXTERNAL scheme selects the classic HTTP(S) load balancer and locks you out of advanced traffic management. Get this wrong and you will not understand why header routing silently does nothing.

Balancing mode and capacity scaler

When you add a backend, the balancing mode decides how the ALB measures whether a backend is “full.” For an ALB the choice is usually RATE (requests per second) or UTILIZATION (backend CPU). RATE is more predictable for stateless web tiers because it does not depend on noisy CPU signals.

gcloud compute backend-services add-backend web-bes \
  --instance-group=web-mig-usc1 \
  --instance-group-region=us-central1 \
  --balancing-mode=RATE \
  --max-rate-per-instance=200 \
  --capacity-scaler=1.0 \
  --global

# Repeat add-backend per region (e.g. europe-west1) for global spread.

The global ALB prefers the region closest to the user, then spills to the next-closest region once a region hits its configured capacity. The capacity scaler (0.0 to 1.0) is your pressure-relief valve: it scales the effective max-rate down without redeploying. Setting a backend’s scaler to 0.0 drains it gracefully — new connections stop arriving while existing ones finish — which is how you cordon a region for maintenance. Leave the others at 1.0 and the ALB rebalances automatically.

Capacity scaler is the single most useful operational knob on the ALB. Wire update-backend --capacity-scaler=0.0 into your regional-drain runbook; it is far gentler than deleting a backend and faster than scaling a MIG to zero.

Outlier detection and connection draining

Health checks catch a dead instance; outlier detection catches a sick one — an endpoint returning 5xx or gateway errors while still passing health checks. It ejects the bad endpoint from the load balancing pool for a cooldown, much like a circuit breaker.

gcloud compute backend-services update web-bes --global \
  --connection-draining-timeout=60s \
  --outlier-detection-consecutive-errors=5 \
  --outlier-detection-interval=10s \
  --outlier-detection-base-ejection-time=30s \
  --outlier-detection-max-ejection-percent=50

Connection draining gives in-flight requests up to the timeout to finish when an endpoint is removed or a MIG scales in, so rolling deploys do not sever live requests. Outlier detection plus draining is the difference between one bad pod causing a brief blip versus a sustained error rate.

Step 3: Route with URL maps

The URL map is the routing brain. The simplest form sends everything to one backend; the interesting form routes by host, path, header, and query, and splits traffic for canaries.

gcloud compute url-maps create web-urlmap --default-service=web-bes --global

For anything beyond a default service, export the URL map to YAML, edit, and re-import. This is the only sane way to manage host rules, path matchers, and header routing, and it is reviewable in git.

gcloud compute url-maps export web-urlmap --global --destination=urlmap.yaml

A URL map that routes /api to an API backend, header-routes a beta cohort, and canaries 5% of root traffic to a new backend looks like this:

defaultService: global/backendServices/web-bes
hostRules:
  - hosts: ["app.example.com"]
    pathMatcher: app-matcher
pathMatchers:
  - name: app-matcher
    defaultService: global/backendServices/web-bes
    routeRules:
      # Header-based routing: beta cohort to a separate backend.
      - priority: 10
        matchRules:
          - prefixMatch: "/"
            headerMatches:
              - headerName: "X-Canary"
                exactMatch: "beta"
        service: global/backendServices/web-beta-bes
      # Path-based routing for the API tier.
      - priority: 20
        matchRules:
          - prefixMatch: "/api/"
        service: global/backendServices/api-bes
      # Weighted traffic split: 5% canary on the root path.
      - priority: 30
        matchRules:
          - prefixMatch: "/"
        routeAction:
          weightedBackendServices:
            - backendService: global/backendServices/web-bes
              weight: 95
            - backendService: global/backendServices/web-canary-bes
              weight: 5

routeRules are evaluated by ascending priority, and the first match wins — order is semantic, not cosmetic. Put specific matches (headers, exact paths) at low priority numbers and catch-alls last. weightedBackendServices is true L7 traffic splitting at the edge: no client changes, no DNS tricks, just shift the weights to ramp a canary and watch your SLOs. You can match on queryParameterMatches the same way, and add routeAction.urlRewrite to rewrite host or path before the request reaches the backend.

gcloud compute url-maps import web-urlmap --global \
  --source=urlmap.yaml --quiet

Step 4: Hybrid and internet NEGs

Backends are not limited to GCP instance groups. Network Endpoint Groups (NEGs) let the same ALB front serverless services, on-prem systems, and arbitrary internet endpoints — one consistent edge (CDN + Cloud Armor + TLS) in front of a heterogeneous estate during a migration.

Hybrid NEGs (NON_GCP_PRIVATE_IP_PORT) point at on-prem or other-cloud backends reachable over Cloud VPN or Interconnect — the canonical strangler-fig pattern: new paths to GCP, legacy paths to the data center, all behind one IP.

gcloud compute network-endpoint-groups create onprem-neg \
  --network-endpoint-type=NON_GCP_PRIVATE_IP_PORT \
  --zone=us-central1-a \
  --network=hub-vpc \
  --subnet=prod-usc1

gcloud compute network-endpoint-groups update onprem-neg \
  --zone=us-central1-a \
  --add-endpoint="ip=10.50.0.10,port=8080"

Internet NEGs (INTERNET_FQDN_PORT or INTERNET_IP_PORT) reference an external endpoint by FQDN or IP — useful for fronting a third-party API or an external origin with Cloud CDN and Cloud Armor in front of it.

gcloud compute network-endpoint-groups create ext-origin-neg \
  --network-endpoint-type=INTERNET_FQDN_PORT \
  --global

gcloud compute network-endpoint-groups update ext-origin-neg --global \
  --add-endpoint="fqdn=origin.partner.example.com,port=443"

Hybrid NEGs require an EXTERNAL_MANAGED backend service and a distributed Envoy health check with a proxy-only subnet in the relevant region. Internet NEGs are global and cannot be health-checked by the ALB — it trusts the endpoint’s availability — so pair them with outlier detection to eject a failing origin.

There is a fourth NEG you meet constantly: the serverless NEG (--network-endpoint-type=serverless), which points at a Cloud Run service, a Cloud Functions function, or an App Engine app instead of an IP. It is regional, has no health check (the platform manages availability), and is how you put Cloud CDN, Cloud Armor, a custom domain, and a shared IP in front of a serverless backend:

gcloud compute network-endpoint-groups create run-neg \
  --region=us-central1 \
  --network-endpoint-type=serverless \
  --cloud-run-service=web-api

gcloud compute backend-services add-backend web-bes --global \
  --network-endpoint-group=run-neg \
  --network-endpoint-group-region=us-central1

So the four NEG types — zonal (GCE VMs by IP:port), serverless (Cloud Run/Functions/App Engine), hybrid (NON_GCP_PRIVATE_IP_PORT, on-prem/other-cloud), and internet (INTERNET_FQDN_PORT/INTERNET_IP_PORT) — plus classic instance-group backends, let one URL map fan out to anything, anywhere, behind one anycast IP.

Step 5: Cloud CDN

Cloud CDN is a flag on the backend service plus a cache policy. The cache mode is the first decision and the easiest to get wrong.

gcloud compute backend-services update web-bes --global \
  --enable-cdn \
  --cache-mode=CACHE_ALL_STATIC \
  --default-ttl=3600 \
  --max-ttl=86400 \
  --client-ttl=3600 \
  --negative-caching \
  --serve-while-stale=86400
Cache mode Behavior
USE_ORIGIN_HEADERS Cache only what the origin marks cacheable via Cache-Control. Safest; origin is authoritative.
CACHE_ALL_STATIC Cache static content types automatically; honor origin headers for the rest. Good default for web.
FORCE_CACHE_ALL Cache every response regardless of headers. Dangerous near auth-gated content — you can cache a logged-in user’s page and serve it to others.

negative-caching caches error responses (404, 410, 5xx) for a short TTL so a thundering herd for a missing object does not hammer the origin. serve-while-stale keeps serving slightly-stale content while revalidating in the background, protecting you during an origin blip.

Cache keys

By default the cache key includes the full host, path, and query string. If your URLs carry per-user or tracking query params, every variation becomes a distinct cache entry and your hit rate collapses. Strip what does not change the response.

gcloud compute backend-services update web-bes --global \
  --cache-key-include-protocol \
  --cache-key-include-host \
  --no-cache-key-include-query-string

To key on only specific params (for example a v cache-buster) use --cache-key-query-string-whitelist=v. Getting the cache key right is usually the single biggest lever on hit ratio.

Signed URLs

For paywalled or time-limited assets, signed URLs (or signed cookies) let the CDN serve private content without an origin round-trip per request. Attach a signing key with gcloud compute backend-services add-signed-url-key web-bes --global --key-name=key1 --key-file=cdn-key.b64; requests must then carry a valid Expires, KeyName, and Signature query string, and the CDN rejects anything expired or tampered before it reaches your origin.

Step 6: Cloud Armor at the edge

Cloud Armor is a security policy attached to the backend service, enforced at Google’s edge before traffic reaches your backends. Build the policy, add rules, then bind it.

gcloud compute security-policies create web-armor \
  --description="Edge WAF + rate limiting for web tier"

# Pre-configured WAF rule: block SQL injection at sensitivity 1.
gcloud compute security-policies rules create 1000 \
  --security-policy=web-armor \
  --expression="evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 1})" \
  --action=deny-403

# Per-client rate limiting: throttle abusive IPs.
gcloud compute security-policies rules create 2000 \
  --security-policy=web-armor \
  --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

The pre-configured WAF rules implement the OWASP ModSecurity Core Rule Set (sqli, xss, lfi, rfi, rce, and more). Sensitivity is the false-positive dial: move to --action=deny-403 only after a soak in preview mode (--preview), because a too-aggressive WAF blocks legitimate traffic that happens to look like an attack.

Rate limiting supports throttle (cap the rate) and rate-based-ban (block an offender entirely once they exceed a threshold). --enforce-on-key controls the bucket: IP, HTTP-HEADER, XFF-IP, or HTTP-COOKIE to limit per authenticated user rather than per source IP.

For bot management, layer in reCAPTCHA-based rules and Google’s threat intelligence feeds, then bind the policy to the backend service:

# Block known malicious IPs via Google threat intelligence.
gcloud compute security-policies rules create 500 \
  --security-policy=web-armor \
  --expression="evaluateThreatIntelligence('iplist-known-malicious-ips')" \
  --action=deny-403

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

Always deploy a new WAF rule with --preview first, then read the verdict = preview-deny entries in the load balancer logs for a few days. Promoting straight to deny is how you take down checkout during a sale because someone’s coupon code matched an SQLi signature.

Step 7: TLS termination, certificate maps, and mTLS

The HTTPS target proxy terminates TLS. Use Google-managed certificates so you never rotate manually. The modern path uses the Certificate Manager API and a certificate map, which lets one proxy serve many domains and handles wildcard plus SAN combinations cleanly.

gcloud certificate-manager certificates create web-cert \
  --domains="app.example.com,www.example.com"

gcloud certificate-manager maps create web-cert-map

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

gcloud compute target-https-proxies create web-https-proxy \
  --url-map=web-urlmap \
  --certificate-map=web-cert-map \
  --global

gcloud compute forwarding-rules create web-https-fr \
  --address=web-ip \
  --target-https-proxy=web-https-proxy \
  --ports=443 \
  --load-balancing-scheme=EXTERNAL_MANAGED \
  --global

Managed certificates require the domain to resolve to the ALB IP for validation, so create the A record before expecting ACTIVE status. Pin a modern TLS policy with --ssl-policy to disable TLS 1.0/1.1 and weak ciphers; the default profile is permissive.

mTLS with Trust Config

For mutual TLS — verifying client certificates at the edge — the global ALB uses Certificate Manager Trust Config (a store of root and intermediate CAs) plus a ServerTlsPolicy that references it. Validation happens before the request hits your backend, and the result is passed downstream as a header.

# Trust config holds the CA bundle that signs valid client certs.
gcloud certificate-manager trust-configs import client-trust \
  --source=trust-config.yaml

# ServerTlsPolicy ties the trust config to a validation mode.
gcloud network-security server-tls-policies import mtls-policy \
  --source=server-tls-policy.yaml \
  --location=global

A minimal server-tls-policy.yaml enforcing client certs looks like:

name: mtls-policy
mtlsPolicy:
  clientValidationMode: REJECT_INVALID
  clientValidationTrustConfig: "projects/PROJECT/locations/global/trustConfigs/client-trust"

REJECT_INVALID drops connections without a valid client cert at the edge; ALLOW_INVALID_OR_MISSING_CLIENT_CERT lets them through but stamps the validation outcome into a header (X-Client-Cert-*) so the backend decides. Attach the ServerTlsPolicy to the target HTTPS proxy, and client-cert verification stays off your application servers entirely.

Verify

Prove each layer independently before declaring victory.

# Frontend chain resolves and the cert is ACTIVE.
gcloud compute target-https-proxies describe web-https-proxy --global \
  --format="value(sslCertificates,certificateMap)"
gcloud certificate-manager certificates describe web-cert \
  --format="value(managed.state)"

# Backends are HEALTHY in every region.
gcloud compute backend-services get-health web-bes --global

# End-to-end request; inspect status, CDN cache, and timing.
curl -sS -o /dev/null -w "code=%{http_code} ttfb=%{time_starttransfer}s\n" \
  https://app.example.com/

# Confirm CDN is serving from cache (look for the cache header).
curl -sSI https://app.example.com/static/app.js | grep -i "x-cache\|age\|cache-control"

# Header routing actually splits to the beta backend.
curl -sSI -H "X-Canary: beta" https://app.example.com/ | grep -i "via\|server"

For any failure, the load balancer log’s jsonPayload.statusDetails gives the verdict, and httpRequest.cacheHit tells you CDN hit versus miss (covered next).

Observability and 5xx triage

Enable logging on the backend service with a sample rate; 100% is fine to start, then dial down for cost.

gcloud compute backend-services update web-bes --global \
  --enable-logging \
  --logging-sample-rate=1.0

The ALB log’s statusDetails field is the fastest 5xx triage tool on GCP because it distinguishes who failed. A 502 with failed_to_connect_to_backend is a backend or firewall problem; 502 with backend_timeout means your --timeout is shorter than the backend’s real latency; 503 with no_healthy_upstream means health checks are failing across the board. The latency breakdown separates frontend RTT from backend latency, so you can tell a slow client from a slow service.

resource.type="http_load_balancer"
httpRequest.status>=500
| project timestamp, httpRequest.status, jsonPayload.statusDetails,
          httpRequest.requestUrl, httpRequest.latency

Enterprise scenario

A retail platform team ran a global ALB fronting a regional GKE service in us-central1 and europe-west1, with Cloud CDN on the static backend and Cloud Armor in front. Black Friday traffic tripled and they began seeing sporadic 502s — but only on the API path, never on static assets, and only under load. Their first instinct was “the ALB is overwhelmed,” which sent them chasing capacity that was not the problem.

The log told the real story. Every failing request carried jsonPayload.statusDetails = "backend_timeout". The backend timeout was the default-ish 30s, but the API’s p99 under peak load had drifted to ~34s because a downstream dependency was slow. The ALB did exactly what it was told: cut the connection at the timeout and return 502. Static assets never tripped it because they were served from the CDN edge and never reached the origin.

Raising the timeout blindly would have masked the latency regression, so they did two things. They bumped the timeout to a deliberate 45s to stop severing nearly-complete requests, and they enabled the outlier detection from Step 2 so a single slow pod got ejected instead of dragging down the pool:

gcloud compute backend-services update api-bes --global --timeout=45s

They also wired a Cloud Monitoring alert on backend 5xx rate broken down by statusDetails, so the next incident would name its own root cause. The lesson: on the global ALB, a 502 is not one failure mode — statusDetails tells you whether it is connectivity, timeout, or health, and the backend timeout is a latency contract you set on purpose, not inherit.

Going deeper

You now have a working ALB. This section is the depth an experienced engineer needs to run it at scale without surprises — the classic-vs-global distinction, the Envoy data plane, how cross-region routing actually decides, the timeout internals, and the cost, quota, IAM, and Terraform realities.

Classic vs global: what actually changed

The word “external HTTP(S) load balancer” covers three products, and mixing them up is the root of most confusion. The distinction is the load-balancing scheme and the data plane.

Classic external ALB Global external ALB (this lesson) Regional external ALB
Scheme EXTERNAL EXTERNAL_MANAGED EXTERNAL_MANAGED (regional)
Data plane Google Front Ends (GFE) GFE + Envoy, global Envoy, single region
Anycast VIP Global (Premium) Global (Premium) Regional
Advanced traffic mgmt No (limited) Yes (header/weight/mirroring/rewrite) Yes
Proxy-only subnet in your VPC No No Yes (required)
Cross-region failover Yes Yes No (one region)
Serverless / hybrid / internet NEGs Partial Full Full (regional)

Three practical takeaways. First, EXTERNAL_MANAGED is the modern global product and the one you want for a new global app — the classic EXTERNAL scheme is on a deprecation path and lacks routeRules-based advanced traffic management. Second, unlike the regional Envoy load balancers, the global external ALB does not require you to provision a proxy-only subnet in your VPC — its Envoy proxies live in Google’s edge, not your network (the hybrid-NEG health-check path in Step 4 is the one place a regional proxy-only subnet enters the picture). Third, if you ever see routeRules “silently do nothing,” check the scheme first — that symptom is almost always a backend service still on EXTERNAL.

Inside the Envoy data plane

A request does not travel to your region and back on every hop. It lands on the nearest Google Front End (GFE) — the same anycast edge that fronts Search — which terminates TLS close to the user, then forwards over Google’s private backbone to an Envoy proxy layer that applies the URL map, Cloud Armor, and CDN logic, and finally to your backend. This is why terminating TLS at the edge cuts handshake RTT dramatically for distant users: the expensive round trips happen on the last mile to the nearest POP, not across an ocean to your origin.

Two knobs ride on this. HTTP/3 (QUIC) is a one-line enable on the target proxy and shines on lossy mobile networks because it removes head-of-line blocking:

gcloud compute target-https-proxies update web-https-proxy --global \
  --quic-override=ENABLE

And backend protocol is independent of client protocol — clients can speak HTTP/2 or HTTP/3 to the edge while the ALB speaks HTTP/1.1 or HTTP/2 to your VMs (--protocol=HTTP2 on the backend service, needed for gRPC backends). The edge is a protocol translator, not a passthrough.

How cross-region routing actually decides

The “closest region, then spill” behavior in Step 2 is worth making precise, because it is what determines your failover behavior:

  1. The ALB computes, per request, the closest region with a healthy backend that has spare capacity. “Capacity” is max-rate × capacity-scaler (for RATE mode) summed across the region’s backends.
  2. If the closest region is at capacity, the request overflows to the next-closest region — automatically, per request, no config.
  3. If every backend in a region fails health checks, that region is removed from the pool entirely and traffic fails over to the next region. This is your regional-outage story: it is automatic, but only if you have a second region with capacity.
  4. Outlier detection operates a layer finer — it ejects an individual sick endpoint that still passes health checks, without taking the whole region out.

The trap: a single-region backend service has no failover, full stop. Global anycast gets the request to Google’s edge worldwide, but if all your backends are in us-central1, a us-central1 outage is an outage. Cross-region resilience is a property of having backends in multiple regions with headroom, not of the ALB being “global.” Size each region to absorb at least one peer’s traffic (the classic N+1) or the failover just moves the overload.

Timeouts, deadlines, and streaming

The backend service --timeout is not an idle timeout — it is the maximum time for the backend to return a full response (a request deadline). For a normal request/response API this is your latency contract, as the enterprise scenario showed. But it bites two workloads specifically:

There is also a separate frontend idle timeout (default ~10 minutes for the HTTP connection between client and GFE) that you do not usually touch. When someone says “the load balancer timed out,” always ask which timeout — backend response deadline, route timeout, or frontend idle — because the fix differs for each.

Cost, quotas, and IAM

Manage it as code

At scale the whole chain is Terraform, not gcloud — and because the objects reference each other, code makes the graph explicit and reviewable. This is a representative, schema-correct skeleton for the google provider:

resource "google_compute_global_address" "web_ip" {
  name = "web-ip"
}

resource "google_compute_health_check" "web_hc" {
  name = "web-hc"
  http_health_check {
    port         = 8080
    request_path = "/healthz"
  }
}

resource "google_compute_backend_service" "web_bes" {
  name                  = "web-bes"
  protocol              = "HTTP"
  port_name             = "http"
  load_balancing_scheme = "EXTERNAL_MANAGED"   # the global Envoy ALB
  timeout_sec           = 30
  health_checks         = [google_compute_health_check.web_hc.id]
  enable_cdn            = true
  security_policy       = google_compute_security_policy.web_armor.id

  backend {
    group                 = google_compute_region_instance_group_manager.web_usc1.instance_group
    balancing_mode        = "RATE"
    max_rate_per_instance = 200
    capacity_scaler       = 1.0
  }
}

resource "google_compute_url_map" "web" {
  name            = "web-urlmap"
  default_service = google_compute_backend_service.web_bes.id
}

resource "google_compute_target_https_proxy" "web" {
  name            = "web-https-proxy"
  url_map         = google_compute_url_map.web.id
  certificate_map = "projects/${var.project}/locations/global/certificateMaps/web-cert-map"
}

resource "google_compute_global_forwarding_rule" "web" {
  name                  = "web-https-fr"
  load_balancing_scheme = "EXTERNAL_MANAGED"
  ip_address            = google_compute_global_address.web_ip.address
  port_range            = "443"
  target                = google_compute_target_https_proxy.web.id
}

The value of code here is not tidiness — it is that the references (which proxy points at which URL map, which URL map at which backend service) are version-controlled and reviewed, so nobody re-points production at a stale backend with an untracked console click.

Practice challenges

Reading the chain is easy; building the right link and reasoning about failure is the skill. Work each challenge the way the lesson teaches — name the resource, pick the scheme/flag, do the capacity math — then open the solution. They escalate from an obvious call to genuine judgement, and a couple hide a distractor designed to tempt the wrong answer.

Challenge 1 (beginner). You created a backend service with --load-balancing-scheme=EXTERNAL and now your routeRules header routing does nothing. What is wrong, and what is the correct scheme for the global Envoy ALB?

<details> <summary>Model answer</summary>

EXTERNAL selects the classic HTTP(S) load balancer, which lacks routeRules-based advanced traffic management — so header routing is silently ignored. The global Envoy ALB requires --load-balancing-scheme=EXTERNAL_MANAGED on both the backend service and the forwarding rule. Why: the scheme is what picks the product; the classic scheme is a different data plane that never evaluates your route rules. </details>

Challenge 2 (beginner). Write the gcloud to reserve the global anycast IP the ALB will use, on the correct network tier.

<details> <summary>Model answer</summary>

gcloud compute addresses create web-ip \
  --ip-version=IPV4 \
  --network-tier=PREMIUM \
  --global

Why: the global ALB requires Premium tier (Standard supports only regional load balancing), and --global is what makes the address an anycast VIP rather than a regional one. </details>

Challenge 3 (intermediate). Author the URL-map routeRules fragment that sends /api/ to api-bes and canaries 10% of root-path traffic to web-canary-bes (90% stays on web-bes). Explain why the two rules cannot be in the wrong order.

<details> <summary>Model answer</summary>

routeRules:
  - priority: 10
    matchRules:
      - prefixMatch: "/api/"
    service: global/backendServices/api-bes
  - priority: 20
    matchRules:
      - prefixMatch: "/"
    routeAction:
      weightedBackendServices:
        - backendService: global/backendServices/web-bes
          weight: 90
        - backendService: global/backendServices/web-canary-bes
          weight: 10

Why order matters: routeRules are evaluated by ascending priority, first match wins. prefixMatch: "/" matches everything, so if it had the lower priority number it would swallow /api/ too and the API rule would never run. Specific matches must sit at lower priority numbers than catch-alls. </details>

Challenge 4 (intermediate). A region (europe-west1) is going into a maintenance window. You must stop sending it new traffic but let in-flight requests finish, without deleting the backend or scaling the MIG to zero. What is the single command?

<details> <summary>Model answer</summary>

gcloud compute backend-services update-backend web-bes --global \
  --instance-group=web-mig-euw1 \
  --instance-group-region=europe-west1 \
  --capacity-scaler=0.0

Why: setting the capacity scaler to 0.0 drains the backend gracefully — the ALB stops routing new requests to it (its effective capacity is now zero, so all traffic overflows to other regions) while existing connections finish. It is reversible in one command (set it back to 1.0), unlike deleting the backend. </details>

Challenge 5 (advanced). Under load you see 502s whose ALB log shows jsonPayload.statusDetails = "backend_timeout", while a different incident shows 503 with no_healthy_upstream. Diagnose each, and give the fix for the first that does not simply mask a latency regression.

<details> <summary>Model answer</summary>

Why: statusDetails names which layer failed — timeout (latency contract) vs health (empty pool) — so the two 5xxs have completely different fixes. </details>

Challenge 6 (advanced). You must put one anycast IP, with Cloud CDN and Cloud Armor, in front of both an on-prem service (reachable over Interconnect) and a Cloud Run service. Name the NEG type for each backend and the extra requirement the on-prem one carries.

<details> <summary>Model answer</summary>

Both attach as backends to backend services under one URL map; CDN and Armor bind to those backend services, giving one edge over a heterogeneous estate. Why: NEGs are exactly the abstraction that lets the ALB front non-VM, non-GCP, and serverless endpoints uniformly. </details>

Common beginner mistakes

Checklist

Closing notes

Treat the global ALB as a configurable distributed system, not a black-box appliance. Three layers deserve attention from day one: the backend timeout as an explicit latency contract, the cache key as your hit-ratio lever, and Cloud Armor preview mode as the seatbelt before you arm a WAF rule. Manage the URL map and security policy as code, alert on statusDetails rather than raw 5xx counts, and use capacity scaler for graceful regional drains.

Glossary

gcpload-balancingcloud-armorcloud-cdnnetworking
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments