This post builds a small but realistic network: private VMs with no public addresses, administered through Identity-Aware Proxy, reaching the internet through Cloud NAT, serving traffic behind a load balancer, with a locked-down Cloud Storage bucket.
Architecture
flowchart TB
U[Users] --> LB[External Application Load Balancer<br/>global anycast IP]
ADM[Admin] -->|SSH through IAP| MIG
subgraph VPC[VPC app-vpc custom mode]
subgraph SUB[Subnet app-mumbai 10.30.0.0/24]
MIG[Managed instance group<br/>VMs across asia-south1 zones]
end
NAT[Cloud Router and Cloud NAT]
end
LB --> MIG
MIG --> NAT --> NET[Internet for updates]
MIG --> GCS[Cloud Storage bucket<br/>via Private Google Access]
VPC
A GCP VPC is global; subnets are regional, so one network spans regions without peering. Firewall rules apply to the whole network and match by network tag or, better, by service account. Every VPC has implied rules: deny all ingress and allow all egress.
gcloud compute networks create app-vpc --subnet-mode=custom
gcloud compute networks subnets create app-mumbai --network=app-vpc \
--region=asia-south1 --range=10.30.0.0/24 --enable-private-ip-google-access
gcloud compute firewall-rules create allow-ssh-iap --network=app-vpc \
--allow=tcp:22 --source-ranges=35.235.240.0/20
gcloud compute firewall-rules create allow-lb-health --network=app-vpc \
--allow=tcp:8080 --source-ranges=130.211.0.0/22,35.191.0.0/16 --target-tags=web
35.235.240.0/20 is the range used by Identity-Aware Proxy for TCP forwarding, which lets you SSH without a public IP. The second rule allows Google’s load balancer health checkers. Users need roles/iap.tunnelResourceAccessor to use the tunnel.
Private VMs still need to download updates, which needs Cloud NAT:
gcloud compute routers create nat-router --network=app-vpc --region=asia-south1
gcloud compute routers nats create app-nat --router=nat-router --region=asia-south1 \
--auto-allocate-nat-external-ips --nat-all-subnet-ip-ranges
Compute Engine
Start with a single VM to test the network:
gcloud compute instances create vm-demo --zone=asia-south1-a \
--machine-type=e2-small --subnet=app-mumbai --no-address --tags=web \
--image-family=ubuntu-2404-lts-amd64 --image-project=ubuntu-os-cloud \
--metadata=enable-oslogin=TRUE --shielded-secure-boot
gcloud compute ssh vm-demo --zone=asia-south1-a --tunnel-through-iap
OS Login ties SSH access to IAM (roles/compute.osLogin) instead of keys copied into metadata. For production, do not run pet VMs: create an instance template and a managed instance group that heals and scales:
gcloud compute instance-templates create web-tpl --machine-type=e2-small --subnet=app-mumbai \
--region=asia-south1 --no-address --tags=web --image-family=ubuntu-2404-lts-amd64 \
--image-project=ubuntu-os-cloud --metadata-from-file=startup-script=startup.sh
gcloud compute health-checks create http web-hc --port=8080 --request-path=/healthz
gcloud compute instance-groups managed create web-mig --region=asia-south1 \
--template=web-tpl --size=2 \
--health-check=web-hc --initial-delay=120
gcloud compute instance-groups managed set-autoscaling web-mig --region=asia-south1 \
--min-num-replicas=2 --max-num-replicas=6 --target-cpu-utilization=0.6
A regional group spreads instances across zones. Attach it to an external Application Load Balancer through a backend service, with a managed certificate for HTTPS. Compared with AWS: a GCP load balancer uses one global anycast IP, and a backend service plays the part of the target group.
Cloud Storage
gcloud storage buckets create gs://demo-ci-2025-assets --location=asia-south1 \
--uniform-bucket-level-access --public-access-prevention
gcloud storage cp backup.tar.gz gs://demo-ci-2025-assets/
gcloud storage buckets update gs://demo-ci-2025-assets --versioning
- Uniform bucket-level access turns off per-object ACLs, so IAM alone decides who can read, and public access prevention blocks anything from being made public by mistake.
- New buckets also have soft delete with a seven-day default retention, which allows recovery of deleted objects (and is billed as storage while it holds them).
- Add lifecycle rules to move old objects to cheaper classes or delete them, and consider Autoclass when access patterns are unknown.
- VMs and the group reach the bucket through Private Google Access without public IPs; grant the VM’s service account
roles/storage.objectUseron this bucket only.
Cost hygiene
Delete test VMs and unattached disks when finished. Stopped VMs still bill for their disks and reserved external addresses, and Cloud NAT and load balancers bill by the hour. Label every resource with owner and environment, and set a budget alert (see the previous post).