Cloud Networking Explained: VPCs, Subnets, and Security Groups
A Virtual Private Cloud, or VPC, is the foundational building block of any modern cloud architecture. It provides a logically isolated, private section within a public cloud provider's global infrastructure, giving you full control over a virtual network that you define. Understanding how VPCs and their components work is not an esoteric networking specialty; it's a core competency for any engineer or technical leader building or managing systems in the cloud. These constructs are what allow you to run a public-facing web service and a private database on the same cloud platform without exposing the database to the open internet.
This article explains the mechanisms of cloud networking from first principles. We will cover the VPC itself, how it's divided using subnets and IP addressing, and how traffic is controlled using security groups and network ACLs. We'll explore the history that led to this model, walk through a concrete example of building a secure application environment, and analyze the real-world tradeoffs of cost, complexity, and security that leaders must navigate.
The Foundation: What is a Virtual Private Cloud?
A VPC is your own private data center inside a public cloud like AWS, Google Cloud, or Azure. It's a virtual network that closely resembles a traditional network you might operate in a physical data center, but it's built on top of the cloud provider's massive, shared physical infrastructure. The isolation is achieved through virtualization and Software-Defined Networking (SDN)[1]. SDN separates the network's control plane (which decides where traffic goes) from the data plane (which forwards the traffic), allowing the network to be configured and managed programmatically.
When you create a VPC, you define a private IP address range for it using Classless Inter-Domain Routing (CIDR)[2] notation. Typically, you select a range from the private address spaces designated by RFC 1918[3], such as 10.0.0.0/16. This block of IP addresses is yours to assign to resources like virtual machines, databases, and load balancers within that VPC. Because this address space is private, it's not reachable from the public internet by default, providing an immediate layer of isolation.
This model is a direct result of the evolution of cloud computing. In the early days of Amazon Web Services, the networking model was flat. All EC2 instances resided in a single, shared network space called EC2-Classic[4]. While convenient, this offered limited network isolation. In response to customer demand for greater security and control, AWS launched the Virtual Private Cloud in August 2009, establishing the paradigm that all major cloud providers have since adopted. Today, all new accounts on platforms like AWS operate exclusively within a VPC environment.
While the terminology varies slightly between providers (VPC on AWS[5] and GCP[6], Virtual Network or VNet on Azure[7]), the core concept is identical: a software-defined private network space that you control.
Carving Up the Space: Subnets and IP Addressing
A VPC is just a large, empty plot of land defined by an IP address range. To make it useful, you must subdivide it into one or more subnets. A subnet, or subnetwork, is a logical partition of a VPC's IP address range. Each subnet exists entirely within a single Availability Zone (AZ), which is a distinct physical data center within a geographic region. This design forces you to think about high availability; to build a resilient application, you must place resources in subnets across multiple AZs.
When you create a subnet, you assign it a smaller CIDR block that is a subset of the VPC's main block. For example, if your VPC has the range 10.0.0.0/16 (providing 65,536 total IPs), you could create subnets like:
10.0.1.0/24(256 IPs)10.0.2.0/24(256 IPs)10.0.3.0/24(256 IPs)
Cloud providers reserve a few IP addresses in each subnet for their own networking purposes, so the number of usable addresses is slightly less than the CIDR block size indicates. For instance, AWS reserves the first four and the last IP address[8] in every subnet.
Subnets are categorized as either public or private, a distinction determined not by a checkbox but by their routing configuration.
Public vs. Private Subnets
The flow of traffic in and out of subnets is governed by Route Tables. A route table contains a set of rules, called routes, that determine where network traffic from your subnet is directed.
A public subnet is one whose associated route table has a route to an Internet Gateway (IGW). An IGW is a horizontally scaled, redundant, and highly available VPC component that allows communication between your VPC and the internet. An instance in a public subnet can receive traffic from the internet if it has a public IP address. This is where you place resources that must be directly reachable from the outside world, like web servers or public-facing load balancers.
A private subnet is one whose route table does not have a route to the Internet Gateway. Instances in a private subnet cannot be reached directly from the internet. This is the correct location for backend resources like databases, caching layers, or internal microservices that should never be exposed. These instances can still communicate with other instances in the same VPC.
But what if a private instance needs to access the internet for a task like downloading software updates? This is accomplished with a Network Address Translation (NAT) Gateway. A NAT Gateway is a managed service that sits in a public subnet and has an elastic IP address. You add a route to the private subnet's route table that directs internet-bound traffic (0.0.0.0/0) to the NAT Gateway. The NAT Gateway then forwards the traffic to the Internet Gateway, translating the private source IP address of the instance to its own public IP address. This allows instances in the private subnet to initiate outbound connections to the internet, but the internet cannot initiate connections back to those private instances.
The Gatekeepers: Security Groups and Network ACLs
IP addresses and routes define where traffic can go, but you need firewalls to control what traffic is allowed. Cloud platforms provide two primary firewall constructs: Security Groups and Network Access Control Lists (NACLs).
Security Groups (SGs)
A Security Group acts as a virtual firewall for your instances to control inbound and outbound traffic. Crucially, SGs are stateful and operate at the instance level (specifically, at the Elastic Network Interface or ENI[9]).
- Stateful: This is the most important characteristic. If you allow an inbound connection on a certain port (e.g., TCP port 80 for HTTP), the return traffic for that connection is automatically allowed, regardless of any outbound rules. You don't have to manage rules for the response traffic, which greatly simplifies configuration.
- Instance-Level: Each network interface on an instance can be associated with one or more security groups. They act as a firewall directly attached to the resource.
- Allow Rules Only: Security group rules are permissive only. You cannot create a rule to deny traffic. By default, all inbound traffic is denied, and all outbound traffic is allowed. You create rules to explicitly allow specific inbound traffic.
A powerful feature of security groups is that they can reference other security groups as a source or destination. For example, you can create a rule in your database's security group (db-sg) that allows inbound traffic on port 3306 only from the security group attached to your web servers (web-sg). This creates a tight, logical coupling that is independent of IP addresses, which might change.
Network Access Control Lists (NACLs)
NACLs are an optional layer of security that act as a firewall for controlling traffic in and out of one or more subnets. They are stateless and operate at the subnet level.
- Stateless: Unlike security groups, NACLs do not track connection state. For every request, you must have explicit rules for both the inbound traffic and the corresponding outbound response traffic. For example, to allow an inbound web request on port 80, you must also have an outbound rule that allows traffic on the high-numbered ephemeral ports (typically 1024-65535 in AWS, as different client OSes and services like NAT/ELB use different ranges)[10] that clients use for the return connection. This makes NACLs more complex to configure and debug.
- Subnet-Level: A NACL is associated with a subnet, and all instances within that subnet are subject to its rules.
- Allow and Deny Rules: NACLs support both allow and deny rules. Each rule is assigned a number, and rules are evaluated in numerical order, from lowest to highest. The first rule that matches the traffic is applied, regardless of any subsequent rules.
The default NACL created with a VPC allows all inbound and outbound traffic. Because of their complexity, NACLs are best used for broad, stateless filtering, like explicitly blocking a known malicious IP address from an entire subnet. For most application-level security, security groups are the preferred tool.
A Concrete Example: Building a Two-Tier Web Application
Let's apply these concepts to build a standard web application with a public web tier and a private database tier. We'll use AWS terminology and a Terraform[11] configuration to define the infrastructure.
1. The Plan:
- A VPC with the IP range
10.10.0.0/16. - A public subnet (
10.10.1.0/24) for our web servers. - A private subnet (
10.10.2.0/24) for our database. - An Internet Gateway for internet access.
- A NAT Gateway in the public subnet so the database can fetch updates.
- A
web-sgsecurity group allowing HTTP/HTTPS traffic from the internet. - A
db-sgsecurity group allowing MySQL traffic only from theweb-sg.
2. The Infrastructure as Code (Terraform):
# main.tf
provider "aws" {
region = "us-east-1"
}
# 1. VPC and Gateways
resource "aws_vpc" "main" {
cidr_block = "10.10.0.0/16"
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
}
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "nat" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
depends_on = [aws_internet_gateway.gw]
}
# 2. Subnets
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.10.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true # Assign public IPs to instances here
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.10.2.0/24"
availability_zone = "us-east-1b" # Use a different AZ for high availability
}
# 3. Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat.id
}
}
# 4. Route Table Associations
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
# 5. Security Groups
resource "aws_security_group" "web_sg" {
name = "web-tier-sg"
description = "Allow HTTP/HTTPS inbound traffic"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "db_sg" {
name = "database-tier-sg"
description = "Allow traffic from web tier"
vpc_id = aws_vpc.main.id
ingress {
description = "MySQL from Web SG"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web_sg.id] # Reference the web SG
}
# Statefulness only allows return traffic for inbound requests; an explicit outbound rule is strictly needed for the database to initiate outbound connections (such as fetching updates, as mentioned earlier in the article).
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
This code declaratively creates a secure, two-tier network architecture. Web servers launched in the public subnet with the web_sg can accept internet traffic, and they can connect to a database launched in the private subnet with the db_sg. The database itself remains completely inaccessible from the public internet.
Tradeoffs, Costs, and Decision-Making
While powerful, VPCs and their components introduce tradeoffs that technical leaders must weigh.
Complexity and Cost: The primary cost is not always monetary but cognitive. Managing route tables, security groups, and NACLs for a complex environment requires expertise. Monetarily, while VPCs and subnets are free, components like NAT Gateways[12] incur both an hourly charge and a per-gigabyte data processing fee, which can become a significant operational expense. Similarly, data transfer between Availability Zones is not free, a factor to consider when designing for high availability. More advanced constructs for connecting VPCs, like AWS Transit Gateway[13], also add to the monthly bill.
Failure Modes: Misconfiguration is the most common failure mode. A wrong entry in a route table can blackhole traffic to an entire subnet. An overly restrictive NACL rule can block legitimate return traffic in non-obvious ways, leading to hours of debugging. Running out of IP addresses in a subnet is a serious operational incident that can prevent new resources from launching; this requires careful initial planning of CIDR ranges.
Decision Lens for Leaders:
- Start with a Custom VPC: While cloud providers offer a default VPC for convenience, any production workload should reside in a custom-built VPC where you control the IP ranges and structure.
- Plan Your IP Space: Choose a CIDR block that is large enough for future growth and does not overlap with your on-premises networks or other VPCs you may need to connect to later.
- Default to Security Groups: Use security groups as your primary firewall. Their stateful nature and ability to reference other groups make them powerful and relatively easy to manage. Use NACLs as a secondary, blunt instrument only when necessary, for example, to enforce a hard block on a specific IP range at the subnet boundary.
- Embrace Infrastructure as Code: Manually configuring networking through a web console is error-prone and not scalable. Use tools like Terraform or AWS CloudFormation[14] to define your network, enabling version control, peer review, and automated, repeatable deployments.
Common Misconceptions
- "Private subnets are completely offline." This is incorrect. They are isolated from inbound internet connections, but they can initiate outbound connections to the internet through a NAT Gateway. This is essential for system maintenance.
- "Security Groups are just like old-school firewalls." This misses the key distinctions. SGs are stateful by default and are attached to network interfaces, not a physical network segment. Their ability to use other SGs as a source/destination is a cloud-native concept that provides dynamic, application-aware security that traditional IP-based rules cannot easily match.
- "More NACL rules mean more security." Not necessarily. The default NACL allows all traffic, relying on security groups for protection. Adding complex NACL rules increases management overhead and the risk of misconfiguration. Often, well-architected security groups provide sufficient and more manageable security.
The State of Cloud Networking Today
Cloud networking continues to evolve beyond the basic VPC construct.
Inter-VPC Connectivity: As organizations grow, they often need to connect multiple VPCs. While VPC peering works for a few connections, it becomes a complex mesh at scale. Services like AWS Transit Gateway[15] and Azure VNet Peering[16] provide a hub-and-spoke model to simplify inter-network communication. Google Cloud Shared VPC[17] shares a single VPC across multiple projects; Google Cloud Network Connectivity Center (NCC) is the service that provides a hub-and-spoke model for inter-network communication.
IPv6 Adoption: All major cloud providers now offer dual-stack (IPv4 and IPv6) support for VPCs. Adopting IPv6 can alleviate concerns about IPv4 address exhaustion and simplify certain network topologies by eliminating the need for NAT.
The Rise of the Service Mesh: While VPC networking operates at Layers 3 and 4 of the OSI model[18], a newer class of technology called a service mesh operates at Layer 7 (application). Tools like Istio[19] and Linkerd[20] run alongside applications within a VPC, managing inter-service communication, providing traffic management, observability, and security features like mutual TLS. Service meshes complement, rather than replace, the underlying VPC network, adding a finer-grained layer of control directly at the application level.
Sources (20)
- Open Networking Foundation: SDN Definition
- RFC 4632: Classless Inter-domain Routing (CIDR)
- RFC 1918: Address Allocation for Private Internets
- AWS User Guide: EC2-Classic
- AWS VPC
- Google Cloud VPC
- Azure Virtual Network (VNet)
- AWS User Guide: VPCs and subnets
- AWS User Guide: Elastic network interfaces
- IANA: Service Name and Transport Protocol Port Number Registry
- Terraform
- AWS VPC Pricing
- AWS Transit Gateway
- AWS CloudFormation
- AWS User Guide: What is AWS Transit Gateway?
- Azure Docs: Virtual network peering
- Google Cloud: Shared VPC overview
- Wikipedia: OSI model
- Istio
- Linkerd