Storage & Networking
.mdVerified against Google Cloud SDK 553.0.0, flags verified via `gcloud storage buckets create --help`, · official docs
What it is and where it fits 🎯#
Cloud Storage is GCP's object-storage primitive — the direct analogue of AWS S3 or Azure Blob Storage — and
GCP's VPC networking stack (networks, subnets, firewall rules, Cloud Router/NAT) is what the VMs and GKE
clusters from page 02 run inside. This page assumes the auth/project basics from page 01.
Cloud Storage — buckets#
gcloud storage buckets create gs://my-bucket --location=us-central1 --default-storage-class=STANDARD \
--uniform-bucket-level-access
gcloud storage buckets list
gcloud storage buckets describe gs://my-bucket
gcloud storage rm --recursive gs://my-bucket # delete a bucket and everything in itgcloud storage is the current, unified CLI surface for Cloud Storage — it replaces the older standalone
gsutil for day-to-day object operations and is the one to reach for in new scripts; gsutil still exists
for a handful of things gcloud storage hasn't covered yet. --uniform-bucket-level-access disables
legacy per-object ACLs in favor of IAM-only access control — the current recommended default for any new
bucket, since mixed ACL-plus-IAM access is a common source of "why can this identity still read this one
object" confusion.
Cloud Storage — objects#
gcloud storage cp ./file.txt gs://my-bucket/path/file.txt
gcloud storage cp gs://my-bucket/path/file.txt ./file.txt
gcloud storage cp -r ./local-dir gs://my-bucket/path/ # recursive upload
gcloud storage ls gs://my-bucket/path/ --recursive
gcloud storage rm gs://my-bucket/path/file.txt
gcloud storage rsync ./local-dir gs://my-bucket/path/ --delete-unmatched-objects-from-destinationrsync --delete-unmatched-objects-from-destination mirrors a local directory to a bucket prefix exactly,
removing remote objects that no longer exist locally — the pattern for deploying a static site build output
directory, so a removed page in the source doesn't linger as an orphaned object in the bucket.
Static website hosting#
gcloud storage buckets update gs://my-bucket --web-main-page-suffix=index.html --web-error-page=404.html
gcloud storage buckets add-iam-policy-binding gs://my-bucket \
--member=allUsers --role=roles/storage.objectViewerPublic static-site hosting in GCS is IAM-driven rather than a dedicated "static website" toggle the way
Azure Blob Storage has one — allUsers as the --member is what actually makes objects publicly readable;
--web-main-page-suffix/--web-error-page only configure which object serves for a directory-style request
and for a 404, they don't themselves change access. A bucket meant to serve a static site typically fronts
this setup with Cloud CDN (via a load balancer) for edge caching and a custom domain, rather than
serving directly from the bucket's own storage.googleapis.com endpoint.
Warning
--uniform-bucket-level-access and public-via-allUsers access are independent settings that combine
in a way that's easy to get backwards. A bucket can be allUsers-readable and have uniform access
enabled — the binding still applies, it's just enforced through IAM only, not legacy ACLs. The actual risk
is applying allUsers: objectViewer at the bucket level when only a specific prefix (e.g. a public/
folder) was meant to be public — bucket-level IAM has no path-prefix scoping, so a bucket mixing public and
private content needs two separate buckets, not one bucket with mixed intent.
Bucket lifecycle rules#
cat > lifecycle.json <<'EOF'
{
"rule": [
{"action": {"type": "Delete"}, "condition": {"age": 365}},
{"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"}, "condition": {"age": 30}}
]
}
EOF
gcloud storage buckets update gs://my-bucket --lifecycle-file=lifecycle.json
gcloud storage buckets update gs://my-bucket --clear-lifecycle # remove all lifecycle rulesLifecycle rules apply in order and are evaluated daily by GCS, not instantly on upload — expect up to 24 hours before a newly-eligible object is actually acted on. Combining an age-based class transition (Standard → Nearline) with an eventual deletion rule is the standard cost-tiering pattern, mirroring S3 lifecycle transitions and Azure Blob's own management policy JSON on this site's Azure storage page.
Bucket-level IAM#
gcloud storage buckets add-iam-policy-binding gs://my-bucket \
--member="serviceAccount:my-service@my-project-id.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
gcloud storage buckets get-iam-policy gs://my-bucket
gcloud storage buckets remove-iam-policy-binding gs://my-bucket \
--member="user:jane@example.com" --role="roles/storage.objectViewer"Cloud Storage IAM bindings can be set at the bucket level (shown here) or per-object with legacy ACLs
(gcloud storage objects update --add-acl-grant) — bucket-level IAM is the recommended approach for
anything beyond a one-off exception, since per-object ACLs don't show up in get-iam-policy and are easy to
lose track of, especially once --uniform-bucket-level-access is enabled and ACLs stop applying entirely.
Cloud Build — building from source#
gcloud builds submit --tag=gcr.io/my-project-id/my-image:latest . # build a Dockerfile in cwd, push to GCR
gcloud builds submit --config=cloudbuild.yaml --substitutions=_ENV=staging .
gcloud builds list --limit=5
gcloud builds log <build-id>--config points at a cloudbuild.yaml defining a multi-step build pipeline (build, test, push, deploy)
instead of the single implicit "docker build" --tag does; --substitutions passes _UNDERSCORE_PREFIXED
variables into that config. gcloud builds submit uploads the current directory as the build context, so
run it from the repo root the same way you would docker build ..
Core concepts: VPC network shape#
A GCP VPC network is global, not regional — a single network spans every region, with individual subnets scoped per region. This is a real structural difference from AWS/Azure, where a VPC/VNet itself is regional and cross-region connectivity requires explicit peering or a transit setup even within one account/subscription.
VPC networks and subnets#
gcloud compute networks create my-network --subnet-mode=custom
gcloud compute networks subnets create my-subnet \
--network=my-network --range=10.0.1.0/24 --region=us-central1
gcloud compute networks subnets create gke-subnet \
--network=my-network --range=10.0.2.0/23 --region=us-central1 \
--secondary-range=pods=10.4.0.0/14,services=10.8.0.0/20
gcloud compute networks list
gcloud compute networks subnets list --filter="region:us-central1"--subnet-mode=custom is the deliberate choice for anything beyond a quick test — auto mode creates one
subnet per region automatically with fixed ranges, which is convenient but takes IP planning out of your
hands; custom mode is standard for production VPC design. --secondary-range is what a VPC-native GKE
cluster's --enable-ip-alias (page 02) actually consumes — the pods and services ranges must exist on
the subnet before creating a cluster that references them.
VPC peering#
gcloud compute networks peerings create hub-to-spoke \
--network=hub-network --peer-network=spoke-network --peer-project=my-project-id
gcloud compute networks peerings create spoke-to-hub \
--network=spoke-network --peer-network=hub-network --peer-project=my-project-id
gcloud compute networks peerings list --network=hub-networkLike Azure VNet peering, GCP network peering must be created on both sides and is not transitive — a spoke peered to a hub cannot reach another spoke peered to the same hub unless it's also peered directly to that spoke, or the connectivity is provided by something else (Cloud VPN, an appliance) actively routing between them. This is the same hub-and-spoke trap covered on this site's Azure networking page, just with GCP's own command surface.
Cloud Router and Cloud NAT#
gcloud compute routers create my-router --network=my-network --region=us-central1
gcloud compute routers nats create my-nat \
--router=my-router --region=us-central1 \
--auto-allocate-nat-external-ips --nat-all-subnet-ip-ranges \
--enable-loggingGCP instances get a route to the internet by default only if they have a public IP; a private instance
(no public IP — the recommended default for GKE nodes per the private-cluster pattern on page 02) needs
Cloud NAT for outbound-only internet access (pulling container images, hitting external APIs) without ever
being reachable from the internet inbound. Cloud Router is the underlying resource Cloud NAT attaches to;
--auto-allocate-nat-external-ips lets Google manage the NAT IP pool instead of you reserving static IPs
yourself.
Important
A private GKE cluster with no Cloud NAT configured will fail to pull container images from public
registries — --enable-private-nodes (page 02) removes public IPs from nodes, and without Cloud NAT
there is then no outbound path at all. This is a common "cluster created successfully, but every pod is
stuck in ImagePullBackOff" surprise for a first private-cluster setup — always pair --enable-private- nodes with a Cloud Router + Cloud NAT on the same network/region.
Firewall rules#
gcloud compute firewall-rules create allow-https \
--network=my-network --direction=INGRESS \
--allow=tcp:443 --source-ranges=0.0.0.0/0
gcloud compute firewall-rules create allow-internal-ssh \
--network=my-network --direction=INGRESS \
--allow=tcp:22 --source-ranges=10.0.0.0/16 --target-tags=ssh-allowed
gcloud compute firewall-rules list
gcloud compute firewall-rules describe allow-httpsGCP firewall rules are stateful and apply at the VPC network level (not per-subnet) — --target-tags
scopes a rule to instances carrying that network tag, the equivalent of AWS's per-security-group model but
implemented as network-wide rules filtered by tag instead of a security group object attached to the
instance. There is also an implicit deny-all for anything not matched by an explicit allow rule, and a
default-created default-allow-internal rule on the default network specifically (custom-mode networks
you create yourself start with no implicit allow rules beyond the deny-all).
Real-world scenario: private GKE cluster with working outbound image pulls#
Combining the private-nodes pattern from page 02 with the Cloud NAT dependency above, the full working
sequence for a private cluster's networking prerequisites:
gcloud compute networks create prod-network --subnet-mode=custom
gcloud compute networks subnets create prod-subnet \
--network=prod-network --range=10.0.0.0/20 --region=us-central1 \
--secondary-range=pods=10.4.0.0/14,services=10.8.0.0/20
gcloud compute routers create prod-router --network=prod-network --region=us-central1
gcloud compute routers nats create prod-nat \
--router=prod-router --region=us-central1 \
--auto-allocate-nat-external-ips --nat-all-subnet-ip-ranges
gcloud container clusters create prod-cluster \
--region=us-central1 --network=prod-network --subnetwork=prod-subnet \
--enable-private-nodes --enable-ip-alias --cluster-secondary-range-name=pods \
--services-secondary-range-name=servicesCreating the network, subnet (with secondary ranges), and Cloud NAT before the cluster avoids the
ImagePullBackOff trap from the IMPORTANT callout above — the cluster create command references resources
that already exist, rather than discovering the NAT gap only after nodes come up and pods start failing.
Real-world scenario: shared VPC for a multi-team org#
- Designate one project as the host project and enable Shared VPC on it
(
gcloud compute shared-vpc enable my-host-project) - Attach each team's project as a service project
(
gcloud compute shared-vpc associated-projects add my-team-project --host-project=my-host-project) - Grant each team's service account
roles/compute.networkUserscoped to the specific subnet it should deploy into, not the whole shared network - Confirm each team can create instances/GKE clusters in the shared subnet but cannot modify firewall rules or the network itself (that stays centralized in the host project)
Shared VPC is GCP's answer to "many teams, one network, centralized control" — the alternative (VPC peering between many separate per-team networks) scales worse as team count grows, since peering relationships are pairwise and non-transitive as shown above.
CI/CD integration recipe: sync a static build to a bucket from GitHub Actions#
# .github/workflows/deploy-static-site.yml
name: Deploy static site
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
service_account: ci@my-project-id.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud storage rsync ./out gs://my-bucket --recursive --delete-unmatched-objects-from-destination
- run: gcloud compute url-maps invalidate-cdn-cache my-lb --path="/*" --asyncThe final cache-invalidation step matters for the same reason this site's own make deploy invalidates
CloudFront and the Azure recipe purges its CDN endpoint — a bucket sync alone doesn't clear Cloud CDN's edge
cache, so skipping it leaves stale content served for the cache's full TTL.
Common pitfalls#
- Treating a GCP VPC network as regional — it's global; only subnets are regional, a real difference from AWS/Azure that trips up anyone porting a mental model over.
- Enabling
allUsersread access at the bucket level for a bucket with mixed public/private content — see the WARNING above; there's no path-prefix scoping in bucket-level IAM. - Forgetting Cloud NAT on a private-nodes GKE cluster — see the IMPORTANT callout; this reliably produces
ImagePullBackOffon every pod, not an obviously-networking-shaped error. - Treating VPC peering as transitive — it isn't, same trap as Azure VNet peering.
- Assuming
gcloud storage rsyncwithout--delete-unmatched-objects-from-destinationremoves stale remote files — it doesn't by default; a removed local file leaves the remote object in place.
Exit codes#
0 success · non-zero on any API/validation error — gcloud storage commands share the same non-
differentiated exit-code behavior as the rest of gcloud; check gcloud storage cp --verbosity=debug for
the underlying HTTP status on an ambiguous failure.
When to reach for something else#
For a CDN with more advanced edge logic than Cloud CDN's load-balancer-attached model, or a team already
standardized elsewhere, third-party CDNs (Cloudflare, Fastly) remain a common front for GCS-origin static
content. For network topology as reviewable code, model VPCs, subnets, peerings, and firewall rules in
Terraform's google_compute_network family of resources rather than a growing shell script of gcloud compute calls, consistent with pages 01/02's IaC guidance.