DEV Community

Cover image for The 3-Tier VPC: Building a Real Architecture
Nariman Baubekov
Nariman Baubekov

Posted on

The 3-Tier VPC: Building a Real Architecture

We've spent two articles on pieces: CIDR blocks, subnets, routing, DNS, NAT in Part 1; how AWS and GCP each implement those pieces in Part 2. Time to assemble them into something you'd actually deploy.

The pattern we're building is the one you'll see, in some form, behind nearly every production data platform: the 3-tier VPC. Three layers, each more locked-down than the last.

The Shape of It

The 3-tier VPC: public, private app, and isolated data tiers

Notice the arrows: the internet can reach Tier 1, and only Tier 1. Tier 2 is reachable from Tier 1, but not directly from the internet. Tier 3 is reachable only from Tier 2 — nothing else touches it, ever. Each tier is a stricter version of the last, and that strictness is the entire point: a compromised load balancer doesn't hand an attacker your database, because there's no direct path between them.

Let's build this concretely, tier by tier, on both AWS and GCP.

Tier 1: The Public Subnet

This is the only tier with a route to an Internet Gateway. What lives here is deliberately narrow:

  • Load balancers (ALB/NLB on AWS, external Load Balancer on GCP) — the front door for real traffic
  • NAT Gateway / Cloud NAT — so Tier 2 can reach the internet outbound without living here itself
  • Bastion host (or its modern replacement — more on this below) — the narrow door for human administrative access

Everything else does not belong in Tier 1. Not your database, not your compute, not your internal services — even though it would technically still be protected by security groups/firewall rules, minimizing what's reachable at the network layer is a much stronger guarantee than trusting every instance-level rule to be configured correctly forever.

Tier 2: The Private Subnet — Where Your Compute Actually Lives

This is home for your Spark cluster, Airflow workers, EMR/Dataproc nodes, application servers — the things that need to do work and occasionally reach the internet (installing a package, calling an external API), but should never be reachable from the internet.

  • AWS: route table sends 0.0.0.0/0 to the NAT Gateway sitting in Tier 1
  • GCP: instances here simply have no external IP; Cloud NAT (attached via a Cloud Router) handles outbound access

Tier 3: The Data Subnet — Fully Isolated

Your RDS instance, your internal warehouse endpoint, anything holding the data itself. This tier often doesn't even get outbound internet access — there's rarely a legitimate reason for a database to initiate a connection to the public internet.

  • AWS: often no route to 0.0.0.0/0 at all — not even via NAT
  • GCP: same idea — no external IP, and you can skip attaching this subnet's range to Cloud NAT entirely

Access into Tier 3 is granted narrowly: only Tier 2's security group (AWS) or Tier 2's service account (GCP) is allowed in, on exactly the port the database needs.

A Concrete AWS Build

Here's what this looks like as an actual subnet layout, spread across two AZs for availability:

Subnet CIDR AZ Tier
Public Subnet 1 10.0.1.0/24 AZ 1 Public
Private Subnet 1 10.0.2.0/24 AZ 1 App
Public Subnet 2 10.0.3.0/24 AZ 2 Public
Private Subnet 2 10.0.4.0/24 AZ 2 App

(A data tier would typically add 10.0.10.0/24 and 10.0.11.0/24, one per AZ, following the same pattern.)

And the security group chain that governs traffic between tiers:

AWS security group chaining: internet → ALB-SG → EC2-SG → RDS-SG

Each security group only trusts the one before it — never the internet directly, past the first hop. This is the AWS security group chaining pattern, and it's worth internalizing: it's the mechanism, not just this specific example, that you'll reapply constantly.

The Same Build on GCP

GCP gets you the same three-tier isolation, but remember from Part 2: there's no subnet-level "public" flag here — it comes down to external IPs and firewall rules.

GCP equivalent: firewall rules chained via network tags

Instead of chaining security group IDs, you chain network tags (or service accounts) through firewall rule sources. The load balancer's backend instances carry an lb tag; the firewall rule for your app tier says "allow traffic tagged lb"; the database tier's rule says "allow traffic tagged app." Same layered-trust outcome, different mechanism — tags and service accounts instead of security group references.

The Narrow Door: Bastion Hosts

Even with three tiers locked down, someone eventually needs to actually log into a Tier 2 or Tier 3 instance — to debug a stuck Spark job, check disk space, whatever. You don't want to open SSH from the internet directly to that instance. The bastion host (sometimes called a jump host) exists to solve exactly this.

SSH hop through a bastion host into a private subnet

A bastion is a single, hardened instance sitting in the public subnet, whose security group allows SSH from a narrow, known set of IPs — your office, your VPN range, nowhere else. Every private-subnet instance's security group, in turn, allows SSH only from the bastion. You hop through it to reach anything private. One door, tightly guarded, heavily audited — rather than every private instance having its own exposed SSH port.

The Modern Alternative: Skip the Bastion Entirely

Both clouds now offer a way to get the same outcome — controlled admin access to private instances — without running a bastion host at all, and without opening an inbound port anywhere:

  • AWS: SSM Session Manager. No inbound port, no SSH keys to manage, no bastion instance to patch and pay for. Access is governed entirely by IAM policy, and every session is logged automatically.
  • GCP: Identity-Aware Proxy (IAP) TCP forwarding. Same idea — you connect to the private instance through IAP's proxy, authenticated by your Google identity and IAM role, with no external IP and no open SSH port required on the instance at all.

SSM Session Manager / IAP broker admin access with no open inbound ports

The trade-off is close to nonexistent at this point — both are the recommended default on their respective clouds, and a real bastion host is increasingly something you'd only stand up for a specific compliance requirement or a legacy workflow that hasn't been migrated yet. If you're designing a new data platform today, start with SSM or IAP and only reach for a traditional bastion if something specifically forces your hand.

Putting the Whole Thing Together

The full 3-tier architecture with IAM-brokered admin access

This is, structurally, most of what you need to know to read any production data platform's network diagram — including the one from the intro of Part 1. Three tiers, decreasing trust, one narrow administrative door that increasingly doesn't even need to be a door anymore.

What's Next

So far, every private-subnet resource that needs to reach a managed service — S3, BigQuery, a managed Kafka cluster — has been going out through NAT, over the public internet, even though the traffic never leaves the cloud provider's own network in practice. Part 4 covers how to close that gap entirely: VPC endpoints / PrivateLink on AWS and Private Service Connect on GCP, so that traffic to your data services never touches the public internet at all — not even for a private subnet with NAT egress.

Top comments (0)