A practical guide to Amazon VPC — subnets, route tables, gateways, and the networking concepts that quietly sit underneath almost every other AWS service.
A VPC, or Virtual Private Cloud, is your own private, isolated section of the AWS network. Every resource you launch that needs networking — an EC2 instance, an RDS database, a Lambda function configured to run inside a VPC — lives inside one, whether you thought about it explicitly or not. When you create a new AWS account, AWS automatically creates a default VPC in each region, already configured with public subnets, so that beginners can launch an EC2 instance and connect to it without needing to understand networking at all. That default setup works, but understanding what is actually happening underneath it becomes important the moment you need anything more deliberate — private databases, controlled internet access, or multiple environments that should not be able to reach each other.
Conceptually, a VPC is similar to setting up your own private network in a traditional data center, except everything is virtual and defined through configuration rather than physical cables and switches. You define an IP address range for the VPC using CIDR notation — something like 10.0.0.0/16, which gives you roughly 65,000 addresses to work with — and then divide that range into smaller pieces called subnets.
A VPC by itself does nothing except reserve an address space. The real structure comes from what you put inside it: subnets that divide the address space into smaller sections, route tables that determine where traffic goes, and gateways that connect the VPC to the outside internet or to other networks. Together these pieces determine which resources can talk to which other resources, and which resources are reachable from the public internet at all.
VPCs are region-specific — a VPC created in eu-west-1 exists only in that region and cannot span multiple regions on its own, though AWS does offer VPC peering and other tools to connect VPCs across regions when needed. For most projects, especially smaller ones, a single VPC per region is more than sufficient, and the complexity comes from how you structure the subnets within it rather than from needing multiple VPCs.
A subnet is a subdivision of your VPC's address range, and every subnet exists entirely within a single Availability Zone — one of the physically separate data center facilities within an AWS region. This matters for reliability: spreading resources across subnets in different Availability Zones protects your application from a single facility going down.
The distinction between a public subnet and a private subnet comes down to one thing: whether the subnet's route table sends internet-bound traffic to an internet gateway. A public subnet has a route to an internet gateway, meaning resources inside it can be reached from, and can reach out to, the public internet, assuming they also have a public IP address. A private subnet has no such route, so resources inside it cannot be reached directly from the internet and cannot initiate outbound internet connections either, unless additional infrastructure is set up to allow it.
A typical, sensible architecture puts anything that genuinely needs to be internet-facing — a load balancer, a bastion host for SSH access — in public subnets, while putting everything else, particularly databases and application servers that should never be directly reachable from outside, into private subnets. This is not just theoretical security advice; it is the standard pattern used across the vast majority of production AWS architectures, because it drastically reduces what an attacker can even attempt to reach directly.
Here is what creating a basic public subnet looks like using boto3:
The CidrBlock here, 10.0.1.0/24, gives this particular subnet 256 addresses, a small slice of the VPC's overall 10.0.0.0/16 range. Whether this subnet ends up being public or private depends entirely on the route table associated with it, not on anything set at creation time — a subnet only becomes public once you explicitly associate it with a route table that sends traffic to an internet gateway.
VPC subnets documentation: https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.htmlA route table is a set of rules that determines where network traffic from a subnet is directed, based on the destination address. Every subnet is associated with exactly one route table, though a single route table can be shared by multiple subnets. The default rule in every route table allows traffic to flow freely within the VPC itself — resources in different subnets of the same VPC can reach each other without any additional configuration.
An internet gateway is what connects a VPC to the public internet. It is a single resource attached to the VPC as a whole, and a subnet becomes public specifically by having a route in its route table that sends traffic destined for 0.0.0.0/0 — meaning any address not covered by a more specific route — to that internet gateway. Without this route, even a resource with a public IP address has no actual path to or from the internet.
NAT Gateways solve a specific and common problem: resources in a private subnet often still need to reach the internet, for example to download software updates or call an external API, even though they should never be reachable from the internet themselves. A NAT Gateway sits in a public subnet and allows private subnet resources to initiate outbound connections to the internet, while blocking any inbound connections initiated from outside. This asymmetry — outbound allowed, inbound blocked — is exactly the behaviour you want for private resources that need occasional internet access without being exposed.
NAT Gateways are worth knowing about early because they have a real, ongoing cost — an hourly charge plus a per-gigabyte data processing fee — which surprises people who assume all networking within a VPC is free. For smaller projects, this is one of the more common sources of unexpectedly high AWS bills, and it is worth genuinely asking whether a NAT Gateway is necessary before adding one, rather than including it reflexively as part of a "standard" architecture.
Security Groups and Network ACLs, covered briefly in the EC2 course, are the other major piece of VPC networking. Security groups operate at the resource level and are stateful, while Network ACLs operate at the subnet level and are stateless, evaluating inbound and outbound rules independently. Most projects rely primarily on security groups for day-to-day access control, with Network ACLs used more sparingly for broader subnet-level restrictions.
For a new project, the default VPC that AWS provides is genuinely fine to start with, and there is no strict requirement to design a custom VPC before you have a real reason to. The moment that changes is usually when you need a private database that should never be internet-facing, when you are building something with compliance requirements around network isolation, or when your architecture has grown complex enough that keeping public and private resources cleanly separated becomes important for security rather than optional.
A common, solid starting pattern for a custom VPC involves two public subnets and two private subnets, spread across two different Availability Zones for redundancy. Public-facing resources like load balancers go in the public subnets; application servers, Lambda functions that need VPC access, and databases go in the private subnets. This structure covers the needs of the large majority of applications without requiring more elaborate multi-VPC or multi-account setups, which are really only necessary at much larger scale or for specific organisational requirements.
One mistake that comes up repeatedly: putting a Lambda function inside a VPC without a clear reason to. Lambda functions do not need to be in a VPC at all unless they specifically need to reach a resource that only exists inside one, like an RDS database in a private subnet. Placing a Lambda function inside a VPC unnecessarily adds cold start latency and requires additional networking configuration, including often a NAT Gateway if the function also needs general internet access, for no real benefit. If your Lambda function only talks to other AWS services like DynamoDB or S3 through their public APIs, it does not need to be in a VPC.
Another common mistake is over-provisioning the address space unnecessarily or under-provisioning it in a way that becomes limiting later. A /16 VPC CIDR block, giving around 65,000 addresses, is generous enough for the vast majority of projects and leaves plenty of room to add more subnets later without redesigning anything. Choosing something too small early on, like a /24, can force an awkward redesign once the project grows past the initial plan.
VPC design does not need to be perfect from day one, but understanding the core pieces — subnets, route tables, gateways, and the public versus private distinction — makes it much easier to reason about why a resource can or cannot reach the internet, why a database is or is not exposed, and where a NAT Gateway might be quietly adding cost to a bill. Most networking problems in AWS come down to one of these pieces being misconfigured or misunderstood, and having a clear mental model of how they fit together resolves the majority of confusion before it becomes an actual production issue.