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
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/0to 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/0at 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:
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.
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.
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.
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
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)