Field guide · Google Cloud · ~12 commands

Put a static site behind HTTPS, a CDN, and your own domain.

A Cloud Storage bucket can already serve files — but only on a long Google URL, with no real directory index.html and no control over caching. This walks the upgrade: wrap the bucket in a global load balancer with Cloud CDN, give it a managed TLS certificate, and point your domain at it. Every command is annotated with why it's there and what each flag does.

origin
Storage bucket
+ Cloud CDN
Backend bucket
routing
URL map
TLS + listen
HTTPS proxy + IP
your domain
DNS A record
00

Prerequisites

Why

Authenticate, pick the project everything bills to, and switch on the Compute Engine API — load balancers, IPs, and certs all live under it.

gcloud auth login
gcloud config set project PROJECT_ID
gcloud services enable compute.googleapis.com
One choice up front: a name prefix you'll reuse for every resource (e.g. mysite-). Consistent names make the pile of resources legible later.
01

Create the bucket & upload your files

Why

The bucket is the origin — the actual bytes. Lay pages out as <folder>/index.html so each one gets a clean URL later.

gcloud storage buckets create gs://BUCKET --location=US
# a folder of files (index.html + assets):
gcloud storage cp --recursive ./site/* gs://BUCKET/hooks/
# or a single page, renamed to index.html:
gcloud storage cp page.html gs://BUCKET/hooks/index.html --content-type=text/html
--location=USWhere the bucket lives. US is a multi-region — redundant across US data centers and low-latency for US traffic. Use a single region (e.g. us-central1) to cut cost or pin data residency.
--recursiveUpload a whole directory tree, not one file.
--content-type=text/htmlForces the MIME type. Without it a file not ending in .html may download instead of render. gcloud infers it from the extension otherwise.
Bucket names are globally unique across all of Google Cloud — if creation fails, the name is taken.
02

Make the objects publicly readable

Why

The load balancer can only hand out objects it's allowed to read. Granting allUsers read makes them fetchable by the CDN — and by anyone with the URL.

gcloud storage buckets add-iam-policy-binding gs://BUCKET \
  --member=allUsers \
  --role=roles/storage.objectViewer
--member=allUsersEveryone, no sign-in. The special identity for anonymous public access.
--role=roles/storage.objectViewerRead objects only. Not list, not write, not delete — the minimum a public site needs.
This applies to the whole bucket. Every object becomes world-readable — keep secrets out of a public bucket. To undo, swap add-iam-policy-binding for remove-iam-policy-binding.
03

Set the website config (the index.html magic)

Why

This is what makes /hooks/ serve /hooks/index.html and missing paths serve your 404 page — but only when the bucket is reached through the load balancer (the A-record path), not the raw storage URL.

gcloud storage buckets update gs://BUCKET \
  --web-main-page-suffix=index.html \
  --web-error-page=404.html
--web-main-page-suffix=index.htmlDirectory index. A request to a "folder" URL resolves to that file inside it. Also serves the root.
--web-error-page=404.htmlCustom not-found page. Returned with a real 404 when no object and no index match. (Upload a 404.html for it to point at.)
Set this before the load balancer and the pretty URLs just work — no URL-rewrite rules needed. On the bare storage.googleapis.com/BUCKET/… URL it's ignored by design, so directory URLs there still 404.
04

Reserve a global static IP

Why

This is the single, fixed address you'll point DNS at. "Static" means it won't change under you; "global" means one anycast IP served from Google's edge worldwide.

gcloud compute addresses create mysite-ip --global \
  --network-tier=PREMIUM \
  --ip-version=IPV4
# print it — you'll need it for DNS:
gcloud compute addresses describe mysite-ip --global --format="value(address)"
--globalScope. Required for a global external load balancer — the IP isn't tied to one region.
--network-tier=PREMIUMGoogle's backbone + anycast. Premium is required for the global load balancer; traffic rides Google's network to the nearest edge.
--ip-version=IPV4Reserve an IPv4 address (add a separate IPv6 one later if you want both).
05

Wrap the bucket as a backend — with Cloud CDN

Why

A "backend bucket" is how a load balancer talks to a storage bucket. Turning on CDN caches your files at Google's edge so most requests never touch the bucket — faster and cheaper.

gcloud compute backend-buckets create mysite-backend \
  --gcs-bucket-name=BUCKET \
  --enable-cdn \
  --cache-mode=CACHE_ALL_STATIC
--gcs-bucket-name=BUCKETThe storage bucket from step 1 that this backend represents.
--enable-cdnTurn on Cloud CDN. Cache responses at the edge instead of fetching from the bucket each time.
--cache-mode=CACHE_ALL_STATICCache static content automatically (images, CSS, JS, HTML) even without explicit cache headers, while respecting any you do set.
After you publish updates, the edge may serve old copies until the cache TTL expires. Invalidate on demand with gcloud compute url-maps invalidate-cdn-cache.
06

Create the URL map (the router)

Why

The URL map decides which backend serves which path. For one bucket it's trivial — send everything to the backend bucket — but it's the hook for path rules later.

gcloud compute url-maps create mysite-urlmap \
  --default-backend-bucket=mysite-backend
--default-backend-bucket=mysite-backendCatch-all route. Any request with no more specific rule goes to this backend bucket.
07

Provision a Google-managed TLS certificate

Why

HTTPS needs a certificate for your domain. A managed cert is issued and auto-renewed by Google for free — you never touch a private key.

gcloud compute ssl-certificates create mysite-cert \
  --domains=your-domain.com \
  --global
--domains=your-domain.comExact hostnames covered. One cert can list several (e.g. your-domain.com,www.your-domain.com) — each needs its own DNS pointing at the LB.
--globalMatches the global load balancer the cert attaches to.
It starts in PROVISIONING and won't go ACTIVE until step 10's DNS points at the load balancer. That's expected — create it now, it issues automatically once DNS resolves.
08

Create the HTTPS proxy (terminates TLS)

Why

The proxy is the piece that decrypts incoming HTTPS using your certificate, then consults the URL map to route the request.

gcloud compute target-https-proxies create mysite-https-proxy \
  --url-map=mysite-urlmap \
  --ssl-certificates=mysite-cert \
  --global
--url-map=mysite-urlmapWhere to send requests once TLS is terminated.
--ssl-certificates=mysite-certThe cert(s) it serves. Presented to browsers during the TLS handshake.
09

Create the forwarding rule (the listener on :443)

Why

This is the front door. It binds your public IP and port 443 to the HTTPS proxy — the thing that actually accepts connections from the internet.

gcloud compute forwarding-rules create mysite-https-fr --global \
  --target-https-proxy=mysite-https-proxy \
  --ports=443 \
  --address=mysite-ip \
  --load-balancing-scheme=EXTERNAL_MANAGED
--target-https-proxy=…Hand accepted connections to the proxy from step 8.
--ports=443Listen on the HTTPS port.
--address=mysite-ipUse the reserved IP from step 4 (not a random ephemeral one).
--load-balancing-scheme=EXTERNAL_MANAGEDThe newer global external ALB. Use EXTERNAL for the classic version — pick one and keep every forwarding rule on the same scheme.
10

Point your domain at the load balancer (DNS)

Why

Until DNS resolves your domain to the LB's IP, nobody can reach it — and the managed cert can't validate. This is the one step you do at your DNS provider, not in gcloud.

Add a single A record mapping your host to the IP from step 4:

TypeNameValueTTL
A@ (root)the reserved IPdefault
If your domain is parked at the registrar (Squarespace, etc.), it likely already has a default A @ record pointing at the registrar's parking server. Remove that one first — two conflicting A @ records won't work — then add the record above. Leave unrelated records (email MX, verification CNAMEs) alone.
Apex vs. subdomain. A bare domain (your-domain.com) needs an A record to the IP. A subdomain (www, docs) can use an A record too, or a CNAME to the apex. Whatever hostnames you serve must match the cert's --domains.
11

Wait for the cert, then verify

Why

Once DNS propagates, Google sees your domain pointing at the LB and issues the certificate. Watch it flip from PROVISIONING to ACTIVE, then test.

# poll the certificate status:
gcloud compute ssl-certificates describe mysite-cert --global \
  --format="value(managed.status)"
# once ACTIVE, confirm it serves:
curl -I https://your-domain.com/hooks/
Provisioning usually takes 15–60 minutes after DNS is correct. A misconfigured domain times out after ~30 minutes and retries — so if it's stuck, re-check the A record actually resolves to the LB IP (dig your-domain.com).

Optional: redirect HTTP → HTTPS

Visitors who type http:// should land on https://. This adds a tiny second front door on port 80 whose only job is to bounce people to HTTPS. It reuses the same IP and adds no forwarding-rule cost.

# a url-map whose only behavior is "redirect to https" (imported from YAML):
cat > redirect.yaml <<'EOF'
kind: compute#urlMap
name: mysite-redirect
defaultUrlRedirect:
  redirectResponseCode: MOVED_PERMANENTLY_DEFAULT
  httpsRedirect: true
EOF
gcloud compute url-maps import mysite-redirect --source=redirect.yaml --global

gcloud compute target-http-proxies create mysite-http-proxy --url-map=mysite-redirect --global

gcloud compute forwarding-rules create mysite-http-fr --global \
  --target-http-proxy=mysite-http-proxy --ports=80 \
  --address=mysite-ip --load-balancing-scheme=EXTERNAL_MANAGED
httpsRedirect: trueSend a 301 to the https:// version of whatever path was requested.
MOVED_PERMANENTLY_DEFAULTUse a permanent 301 redirect so browsers remember it.

Cost & teardown

WhatRoughlyNotes
Forwarding rule (LB baseline)~$18/moFlat, for the first 5 rules combined — the unavoidable "LB is on" charge.
Cloud CDN egressfrom ~$0.08/GiBCache hits served to visitors; tiered by volume.
CDN cache fill + lookups~$0.01/GiB + tinyOrigin→edge fills and per-request lookups.
StoragecentsStandard bucket storage + any cache-miss egress.

For a low-traffic site the bill is dominated by the ~$18/mo forwarding-rule baseline. If that's not worth it for a few pages, a CDN like Cloudflare in front of the bucket gives free HTTPS + caching without the load balancer.

To tear it all down, delete in reverse: forwarding rules → proxies → url-maps → cert → backend bucket → address. Then the bucket itself if you're done with it.