← Back to DevBytes

VPC: Complete Setup and Configuration Guide

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:

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles