# gcloud CLI Cheat Sheet — Load Balancing, Cloud Armor & DNS

> **Tool:** Google Cloud CLI (gcloud)
> **Category:** Cloud CLIs
> **Verified against:** Google Cloud SDK 553.0.0, flags verified via `gcloud compute backend-services
> create --help`, `gcloud compute health-checks create http --help`, `gcloud compute url-maps create
> --help`, `gcloud compute url-maps add-path-matcher --help`, `gcloud compute target-https-proxies create
> --help`, `gcloud compute forwarding-rules create --help`, `gcloud compute addresses create --help`,
> `gcloud compute ssl-certificates create --help`, `gcloud compute security-policies create --help`,
> `gcloud compute security-policies rules create --help`, `gcloud compute vpn-gateways create --help`,
> `gcloud compute vpn-tunnels create --help`, `gcloud dns managed-zones create --help`, `gcloud dns
> record-sets create --help`, and `gcloud certificate-manager certificates create --help` run locally,
> 2026-09-17
> **Official docs:** https://cloud.google.com/sdk/gcloud/reference/compute/backend-services, https://cloud.google.com/sdk/gcloud/reference/compute/security-policies, and https://cloud.google.com/sdk/gcloud/reference/dns

## 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

```mermaid
flowchart LR
    Client["Client"] --> DNS["Cloud DNS<br/>A record → static IP"]
    DNS --> FR["Forwarding rule<br/>(the reserved IP + port)"]
    FR --> Proxy["Target HTTPS proxy<br/>(terminates TLS)"]
    Proxy -.->|"cert attached here"| Cert["Managed SSL certificate"]
    Proxy --> URLMap["URL map<br/>(host + path routing)"]
    URLMap --> BE1["Backend service: api<br/>+ health check"]
    URLMap --> BE2["Backend service: web<br/>+ health check"]
    BE1 --> MIG["Instance group / NEG<br/>(the actual VMs, GKE, or Cloud Run)"]
    Armor["Cloud Armor security policy"] -.->|"attached to backend service"| BE1

    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    classDef accent fill:#f0e9fb,stroke:#6d43c0,color:#10161c
    classDef crit fill:#fbe8e6,stroke:#b3261e,color:#10161c
    class DNS,FR info
    class Proxy,URLMap accent
    class Armor crit
```

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

```bash
# 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

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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:

```bash
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

```yaml
# .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.
