When you're building an application, it's tempting to deploy everything onto a single virtual machine. It's simple, inexpensive, and gets you to production quickly. For many startups, that's exactly how things begin.
Imagine FreshCart, a fast-growing online grocery platform. During most of the week, a single server comfortably handles browsing, product searches, and checkout requests. Then the marketing team launches a "Weekend Mega Sale" campaign.
Within minutes, thousands of users visit the site.
Pages begin loading slowly. Checkout requests queue up. CPU utilization reaches nearly 100%, memory becomes exhausted, and eventually the server stops responding. Every customer is trying to access the same machine, and because that machine hosts the application and is directly exposed to the internet, it becomes both the performance bottleneck and the biggest security risk.
This scenario is common because a single exposed VM has several limitations:
- Every request is handled by one server.
- If that server fails, the application becomes unavailable.
- The backend is directly reachable from the internet, increasing the attack surface.
- Scaling often means replacing the VM with a larger one instead of distributing traffic across multiple servers.
To address these problems, I designed and built a secure two-tier architecture on Google Cloud Platform (GCP). The architecture separates public traffic from the application layer, removes direct internet access to backend servers, and introduces redundancy so the application can continue serving requests even if one backend instance becomes unavailable.
Architecture Overview
The architecture consists of two logical tiers:
Presentation Layer
- Global HTTP Load Balancer
- Static website hosted in Cloud Storage
- HTTPS managed by Google-managed certificates
Application Layer
- Two backend Compute Engine instances
- Private subnet
- No public IP addresses
- Cloud NAT for outbound internet access only
The backend servers are never directly exposed to users. Every request must first pass through Google's Load Balancer, which distributes traffic across healthy backend instances.
(Insert your final architecture diagram here.)
Architecture Components
Custom VPC
The first step was creating a dedicated Virtual Private Cloud (VPC).
Rather than using Google's default network, I created a custom VPC to have complete control over IP ranges, firewall rules, and network segmentation.
gcloud compute networks create "$VPC_NAME" \
--subnet-mode=custom \
--bgp-routing-mode=regional
Using a custom VPC keeps the infrastructure isolated and easier to manage as the application grows.
Public and Private Subnets
Next, I created separate subnets.
- Public subnet
- Private subnet
The public subnet is intended for internet-facing resources, while backend application servers reside entirely inside the private subnet.
gcloud compute networks subnets create "$SUBNET_PUBLIC"
gcloud compute networks subnets create "$SUBNET_PRIVATE"
The private subnet also enables Private Google Access, allowing instances without public IPs to communicate securely with Google services.
Restrictive Firewall Rules
Instead of allowing unrestricted access, firewall rules follow the principle of least privilege.
SSH access is permitted only through Google's Identity-Aware Proxy (IAP).
gcloud compute firewall-rules create allow-ssh-iap
This eliminates the need to expose port 22 to the internet.
Similarly, backend servers only accept HTTP traffic from Google's Load Balancer health check ranges.
gcloud compute firewall-rules create allow-lb-health-check
Because users cannot directly reach the backend VMs, the attack surface is significantly reduced.
Cloud NAT
Backend servers require internet connectivity for activities such as:
- Installing packages
- Downloading updates
- Pulling dependencies
However, exposing them with public IP addresses would undermine the security model.
Cloud NAT solves this problem.
gcloud compute routers create "$ROUTER_NAME"
gcloud compute routers nats create "$NAT_NAME"
Cloud NAT provides outbound internet connectivity while preventing unsolicited inbound connections.
This means the backend servers can install software updates without ever becoming publicly accessible.
Backend Compute Engine Instances
To eliminate a single point of failure, I deployed two backend servers.
- Zone A
- Zone B
Each VM:
- uses the lightweight e2-micro machine type
- has no external IP address
- resides inside the private subnet
gcloud compute instances create freshcart-backend-a
gcloud compute instances create freshcart-backend-b
Deploying backend instances across multiple zones increases availability.
If one zone experiences an outage or maintenance event, traffic can continue flowing to the healthy instance in the remaining zone.
Global HTTP Load Balancer
Instead of allowing users to connect directly to backend servers, every request first reaches Google's Global HTTP Load Balancer.
The load balancer performs continuous health checks against the backend instances.
gcloud compute health-checks create http freshcart-health-check
Healthy instances receive traffic.
Unhealthy instances are automatically removed from rotation.
The backend service combines both instance groups into a single logical service.
gcloud compute backend-services create freshcart-backend-service
Finally, a URL map, HTTP proxy, and forwarding rule expose one public endpoint.
gcloud compute forwarding-rules create freshcart-http-rule
This architecture dramatically improves resilience during traffic spikes.
Instead of overwhelming a single server, incoming requests are distributed between multiple healthy backend instances.
Static Website Hosting
FreshCart's landing page, images, JavaScript, and CSS are excellent candidates for static hosting.
Rather than serving these files from the backend VMs, they are stored in Google Cloud Storage.
gcloud storage buckets create "gs://$BUCKET_NAME"
A Backend Bucket connects Cloud Storage to Google's Load Balancer.
gcloud compute backend-buckets create freshcart-static-backend
A managed SSL certificate provides HTTPS without manual certificate management.
gcloud compute ssl-certificates create freshcart-managed-cert
Separating static content from the backend significantly reduces unnecessary requests hitting the application servers.
For example, if 20,000 visitors all request the homepage logo, CSS, and JavaScript, those assets are served directly from Cloud Storage rather than consuming backend CPU cycles.
Lifecycle Management
Static assets accumulate over time.
Rather than storing old files indefinitely in expensive Standard Storage, I configured a lifecycle policy.
gcloud storage buckets update \
--lifecycle-file=/tmp/lifecycle.json
Objects older than 30 days automatically transition to Nearline Storage, reducing storage costs.
Objects older than 90 days are automatically deleted.
This ensures long-term storage costs remain predictable without requiring manual cleanup.
How This Architecture Handles Traffic Spikes
Returning to FreshCart's flash sale scenario, consider what happens when thousands of customers visit the website simultaneously.
Instead of every request hitting a single VM, users first connect to Google's Global HTTP Load Balancer.
The load balancer distributes incoming traffic across both backend instances. If one backend becomes overloaded or unhealthy, health checks detect the issue and automatically stop routing requests to it. Customers continue interacting with the remaining healthy backend while the unhealthy instance is repaired or replaced.
At the same time, static resources such as HTML, images, CSS, and JavaScript are served directly from Cloud Storage over HTTPS. These requests never reach the backend servers, significantly reducing CPU utilization and improving response times.
The backend servers remain private throughout this process. They can still install security updates and software packages using Cloud NAT, but because they have no public IP addresses, attackers cannot connect to them directly.
The result is an architecture that is considerably more resilient, secure, and scalable than a single exposed virtual machine.
Key Design Decisions
1. Why two backend instances instead of one?
Running a single VM would have been cheaper and simpler, but it would also remain a single point of failure.
Deploying backend instances across two availability zones increases fault tolerance. If one instance or zone becomes unavailable, the load balancer automatically routes traffic to the remaining healthy backend.
The trade-off is slightly higher infrastructure cost, but the improvement in availability justifies the additional expense.
2. Why remove public IP addresses?
Many beginner deployments expose backend servers directly to the internet.
I deliberately avoided this.
Instead, every backend VM resides inside a private subnet and communicates externally only through Cloud NAT.
This dramatically reduces the attack surface while still allowing package updates and outbound connections.
The trade-off is that administrators must connect using Identity-Aware Proxy instead of traditional SSH over the public internet.
3. Why separate static content from the backend?
Initially, it might seem easier to let the backend server host everything.
However, every image, stylesheet, and JavaScript file consumes server resources.
Hosting static assets in Cloud Storage allows the backend to focus exclusively on business logic such as authentication, inventory, and checkout.
This improves scalability and reduces infrastructure costs.
What I Would Build Next
If FreshCart continued growing, this architecture would evolve further.
The first improvement would be replacing unmanaged instance groups with Managed Instance Groups to enable automatic scaling during periods of high demand.
I would also place Cloud CDN in front of the Cloud Storage bucket so static assets are cached at Google's global edge locations, reducing latency for customers worldwide.
For observability, I would integrate Cloud Monitoring, Cloud Logging, and alerting policies to monitor CPU usage, request latency, backend health, and application errors in real time.
Finally, I would migrate the application database to Cloud SQL configured for high availability, ensuring the data layer matches the resilience of the application tier.
Conclusion
Designing cloud infrastructure isn't simply about deploying servers—it's about making deliberate architectural decisions that balance security, scalability, availability, and cost.
By introducing a custom VPC, private backend instances, Cloud NAT, a Global HTTP Load Balancer, Cloud Storage for static assets, and automated lifecycle policies, FreshCart is now significantly better equipped to withstand sudden traffic spikes than it would be with a single exposed virtual machine.
This architecture provides a solid production-ready foundation while remaining simple enough to understand, extend, and discuss during technical interviews or portfolio reviews.
Top comments (0)