VPC: Complete Setup and Configuration Guide
A Virtual Private Cloud (VPC) is the foundational networking layer for almost every cloud deployment. Whether you are launching a single web server or a multi-tier microservices architecture, the VPC defines the isolated network boundary in which your resources live, how they reach the internet, and how they communicate with each other securely. This guide walks through the concepts, the end-to-end setup, and the operational best practices you need to build a production-grade VPC from scratch.
What Is a VPC?
A VPC is a logically isolated section of a cloud provider's network where you can launch resources in a virtual network that you define. In AWS, the most widely used implementation, a VPC spans an entire region and is subdivided into subnets that live in individual Availability Zones (AZs). You control IP addressing, routing, gateways, security filtering, and connectivity to on-premises networks or other clouds.
Key building blocks of a VPC include:
- CIDR block: The primary IPv4 (and optionally IPv6) address range assigned to the VPC.
- Subnets: Smaller IP ranges carved out of the VPC CIDR, each tied to a single AZ.
- Route tables: Rules that determine where network traffic from a subnet or gateway is directed.
- Internet Gateway (IGW): The component that allows communication between VPC resources and the public internet.
- NAT Gateway: A managed service that lets private subnet resources initiate outbound internet traffic without being reachable from the internet.
- Security Groups: Stateful firewalls attached to individual network interfaces.
- Network ACLs: Stateless firewalls applied at the subnet boundary.
- VPC Endpoints: Private links that connect your VPC to supported services without traversing the public internet.
Why VPC Matters
Without a VPC, cloud resources either cannot exist (in AWS, every EC2 instance must live in a VPC) or exist in a flat, shared network where isolation is impossible. A well-designed VPC gives you:
- Isolation: Your workloads are separated from other tenants and from your own less-sensitive environments.
- Granular access control: Security groups and NACLs enforce least-privilege networking.
- High availability: Multi-AZ subnets let you spread workloads across failure domains.
- Hybrid connectivity: VPNs, Direct Connect, and Transit Gateway bridge your data center and cloud.
- Cost control: Properly routed traffic avoids expensive NAT or data transfer paths.
- Compliance: Many regulatory frameworks require private networks, encryption in transit, and auditable network controls.
Planning Your Network
Before creating anything, plan your CIDR layout. A common pattern is to use a /16 for the VPC and carve it into /20 or /24 subnets. Reserve ranges for future expansion, VPN peering, and Transit Gateway attachments. A sample plan:
VPC CIDR: 10.20.0.0/16
Public subnets: 10.20.0.0/24, 10.20.1.0/24, 10.20.2.0/24
Private app subnets: 10.20.16.0/20, 10.20.32.0/20, 10.20.48.0/20
Private data subnets: 10.20.64.0/20, 10.20.80.0/20, 10.20.96.0/20
Reserved (peering): 10.20.128.0/17
Each subnet type lives in a different AZ. Public subnets host load balancers and bastion hosts; private app subnets host application servers; private data subnets host databases. This three-tier layout is the most common production topology.
Creating the VPC with the AWS CLI
The fastest way to understand a VPC is to build one. The following commands use the AWS CLI. Replace region and IDs as needed.
# Create the VPC
aws ec2 create-vpc \
--cidr-block 10.20.0.0/16 \
--region us-east-1 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=prod-vpc}]'
# Capture the VPC ID from the output, e.g. vpc-0abc123def456
VPC_ID=vpc-0abc123def456
# Enable DNS support and hostnames (required for VPC endpoints and private hosted zones)
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames
Creating Subnets Across Availability Zones
Subnets are AZ-scoped. Create at least two of each type in different AZs for high availability.
# Public subnets
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.20.0.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-public-a}]'
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.20.1.0/24 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-public-b}]'
# Private app subnets
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.20.16.0/20 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-app-a}]'
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.20.32.0/20 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-app-b}]'
# Private data subnets
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.20.64.0/20 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-data-a}]'
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.20.80.0/20 \
--availability-zone us-east-1b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=prod-data-b}]'
By default, subnets are private. To make a subnet public, you must enable public IP assignment on launch and point its route table at the Internet Gateway.
# Enable auto-assign public IPs on public subnets
aws ec2 modify-subnet-attribute \
--subnet-id subnet-0aaa111 \
--map-public-ip-on-launch
aws ec2 modify-subnet-attribute \
--subnet-id subnet-0bbb222 \
--map-public-ip-on-launch
Attaching an Internet Gateway
The Internet Gateway is the bridge between your VPC and the public internet. Create one and attach it to the VPC.
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=prod-igw}]'
IGW_ID=igw-0abc123
aws ec2 attach-internet-gateway \
--internet-gateway-id $IGW_ID \
--vpc-id $VPC_ID
Configuring Route Tables
Each subnet is associated with one route table. Public subnets route 0.0.0.0/0 to the IGW. Private subnets route 0.0.0.0/0 to a NAT Gateway.
# Public route table
aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=prod-public-rt}]'
PUBLIC_RT=rtb-0pub111
aws ec2 create-route \
--route-table-id $PUBLIC_RT \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id $IGW_ID
# Associate public subnets with the public route table
aws ec2 associate-route-table \
--route-table-id $PUBLIC_RT \
--subnet-id subnet-0aaa111
aws ec2 associate-route-table \
--route-table-id $PUBLIC_RT \
--subnet-id subnet-0bbb222
Provisioning a NAT Gateway
Private subnets need outbound internet access for package updates, API calls, and similar tasks. A NAT Gateway provides this without exposing the instances to inbound traffic from the internet. Allocate an Elastic IP and create the NAT Gateway in a public subnet.
# Allocate an Elastic IP
aws ec2 allocate-address --domain vpc
# EIP_ALLOC=eipalloc-0abc123
# Create the NAT Gateway in a public subnet
aws ec2 create-nat-gateway \
--subnet-id subnet-0aaa111 \
--allocation-id eipalloc-0abc123 \
--tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=prod-nat-a}]'
# Wait until it becomes Available
aws ec2 wait nat-gateway-available --nat-gateway-ids nat-0abc123
Then create a private route table that points default traffic to the NAT Gateway and associate it with the private subnets.
aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=prod-private-rt-a}]'
PRIVATE_RT_A=rtb-0priv111
aws ec2 create-route \
--route-table-id $PRIVATE_RT_A \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-0abc123
aws ec2 associate-route-table \
--route-table-id $PRIVATE_RT_A \
--subnet-id subnet-0app111
aws ec2 associate-route-table \
--route-table-id $PRIVATE_RT_A \
--subnet-id subnet-0data111
For high availability, deploy a second NAT Gateway in the second public subnet and create a separate private route table for the AZ-b subnets. This prevents a single AZ outage from breaking outbound traffic.
Securing the VPC with Security Groups and NACLs
Security groups are stateful and attached to network interfaces. A typical web tier security group allows inbound HTTPS from the load balancer and all outbound traffic.
# Create a security group for the app tier
aws ec2 create-security-group \
--vpc-id $VPC_ID \
--group-name prod-app-sg \
--description "App tier security group"
APP_SG=sg-0app111
# Allow inbound 443 from the VPC CIDR (e.g., from the ALB)
aws ec2 authorize-security-group-ingress \
--group-id $APP_SG \
--protocol tcp \
--port 443 \
--cidr 10.20.0.0/16
# Allow inbound 8080 from the VPC CIDR
aws ec2 authorize-security-group-ingress \
--group-id $APP_SG \
--protocol tcp \
--port 8080 \
--cidr 10.20.0.0/16
For the database tier, reference the app security group directly instead of a CIDR. This is the recommended pattern because it ties access to identity rather than IP ranges.
aws ec2 create-security-group \
--vpc-id $VPC_ID \
--group-name prod-db-sg \
--description "Database tier security group"
DB_SG=sg-0db111
aws ec2 authorize-security-group-ingress \
--group-id $DB_SG \
--protocol tcp \
--port 5432 \
--source-group $APP_SG
Network ACLs add a second layer of defense at the subnet boundary. They are stateless, so you must explicitly allow both inbound and outbound traffic. A common NACL allows ephemeral ports outbound and restricts inbound to known ports.
# Create a custom NACL for the private app subnets
aws ec2 create-network-acl \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=prod-app-nacl}]'
APP_NACL=acl-0app111
# Allow inbound 443 from the VPC
aws ec2 create-network-acl-entry \
--network-acl-id $APP_NACL \
--rule-number 100 \
--protocol tcp \
--port-range From=443,To=443 \
--cidr-block 10.20.0.0/16 \
--rule-action allow
# Allow outbound ephemeral ports (1024-65535) to anywhere
aws ec2 create-network-acl-entry \
--network-acl-id $APP_NACL \
--rule-number 100 \
--egress \
--protocol tcp \
--port-range From=1024,To=65535 \
--cidr-block 0.0.0.0/0 \
--rule-action allow
Using VPC Endpoints for Private Service Access
Without endpoints, traffic from private subnets to services like S3 or DynamoDB must traverse the NAT Gateway, which costs money and exposes the path to the internet. VPC endpoints solve this. A gateway endpoint for S3 is free and is configured as a route table entry.
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids $PRIVATE_RT_A \
--vpc-endpoint-type Gateway
For services like Systems Manager, Secrets Manager, or KMS, use an interface endpoint, which places an elastic network interface with a private IP in your chosen subnets.
aws ec2 create-vpc-endpoint \
--vpc-id $VPC_ID \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.us-east-1.ssm \
--subnet-ids subnet-0app111 subnet-0app222 \
--security-group-id $APP_SG \
--private-dns-enabled
Deploying the VPC with Terraform
For reproducible infrastructure, define the VPC as code. The Terraform AWS VPC module encapsulates the entire topology in a single resource block.
provider "aws" {
region = "us-east-1"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "prod-vpc"
cidr = "10.20.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.20.0.0/24", "10.20.1.0/24", "10.20.2.0/24"]
private_subnets = ["10.20.16.0/20", "10.20.32.0/20", "10.20.48.0/20"]
database_subnets = ["10.20.64.0/20", "10.20.80.0/20", "10.20.96.0/20"]
enable_nat_gateway = true
single_nat_gateway = false
one_nat_gateway_per_az = true
enable_dns_hostnames = true
enable_dns_support = true
create_database_subnet_group = true
tags = {
Environment = "prod"
Owner = "platform-team"
}
}
This single module creates the VPC, six subnets, an Internet Gateway, three NAT Gateways, public and private route tables, and a database subnet group. Running terraform apply provisions the entire network in minutes.
Connecting to On-Premises or Other VPCs
For hybrid connectivity, use a Site-to-Site VPN or AWS Direct Connect. For VPC-to-VPC connectivity within the same region, VPC peering is simple but does not support transitive routing. For many VPCs, use a Transit Gateway as a central hub.
# Create a Transit Gateway
aws ec2 create-transit-gateway \
--description "prod-tgw" \
--options AutoAcceptSharedAttachments=enable,DefaultRouteTableAssociation=enable,DefaultRouteTablePropagation=enable
# Attach the VPC to the TGW
aws ec2 create-transit-gateway-vpc-attachment \
--transit-gateway-id tgw-0abc123 \
--vpc-id $VPC_ID \
--subnet-ids subnet-0app111 subnet-0app222
Monitoring and Flow Logs
VPC Flow Logs capture IP traffic metadata and are essential for troubleshooting and security auditing. Send them to CloudWatch Logs or S3.
# Create an IAM role and policy for flow logs (omitted for brevity)
# Then enable flow logs at the VPC level
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids $VPC_ID \
--traffic-type ALL \
--log-destination-type s3 \
--log-destination arn:aws:s3:::prod-vpc-flowlogs \
--log-destination-format json
Pair Flow Logs with CloudWatch metrics and VPC Reachability Analyzer to detect misconfigurations before they cause outages.
Best Practices
- Use multi-AZ subnets for every tier. A single AZ is a failure domain, not an architecture.
- Keep data tiers fully private. Databases should have no route to the internet and no public IP.
- Deploy one NAT Gateway per AZ. Sharing a single NAT Gateway creates a cross-AZ failure point and adds data transfer costs.
- Prefer security group references over CIDRs. Referencing
source-groupkeeps rules stable as IP ranges change. - Use VPC endpoints for AWS service traffic. This reduces NAT costs and improves security posture.
- Plan CIDR ranges for peering and TGW. Overlapping CIDRs are painful to fix after the fact.
- Enable Flow Logs from day one. Retroactive visibility is impossible without them.
- Tag everything. Consistent tags drive cost allocation, access control, and automation.
- Manage the VPC as code. Terraform, CloudFormation, or Pulumi ensure repeatability across environments.
- Separate environments into separate VPCs or accounts. Never mix prod and non-prod in the same VPC.
Conclusion
A VPC is more than a network container; it is the security and reliability backbone of your cloud workloads. By planning your CIDR layout, distributing subnets across Availability Zones, configuring public and private route tables, deploying NAT Gateways per AZ, locking down traffic with security groups and NACLs, and leveraging VPC endpoints and Flow Logs, you create a network that is both flexible and hardened. Treat the VPC as code, version it alongside your application, and revisit the design as your connectivity needs grow into peering, Transit Gateway, and hybrid architectures. The upfront discipline pays off in fewer outages, lower costs, and a far simpler path to compliance.