A VPC (Virtual Private Cloud) is the foundation for networking in AWS. It provides an isolated virtual network where AWS resources such as EC2 instances, databases, and application servers can communicate with each other and with external services in a controlled way.
In this project, I provisioned a custom VPC using Terraform and configured public and private subnets, routing, internet connectivity, security controls, an EC2 instance and an S3 Gateway VPC Endpoint.
More importantly, this project helped me understand how these networking components fit together rather than treating them as isolated AWS resources.
The Architecture
The infrastructure consists of:
- A custom VPC using
10.0.0.0/16 - A public subnet using
10.0.1.0/24 - A private subnet using
10.0.2.0/24 - Two Availability Zones
- An Internet Gateway
- Separate public and private route tables
- A security group allowing SSH only from my IP
- An EC2 instance in the public subnet
- An S3 Gateway VPC Endpoint connected to the private route table
- DNS support and DNS hostnames enabled
The architecture can be represented as:
INTERNET
│
│
┌────────▼────────┐
│ Internet Gateway│
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ VPC │
│ 10.0.0.0/16 │
│ │
│ ┌───────────────────────┐ │
│ │ Public Subnet │ │
│ │ 10.0.1.0/24 │ │
│ │ eu-west-2c │ │
│ │ │ │
│ │ EC2 │ │
│ │ │ │ │
│ │ Security Group │ │
│ └───────────────────────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ Private Subnet │ │
│ │ 10.0.2.0/24 │ │
│ │ eu-west-2b │ │
│ │ │ │
│ │ Private Route Table │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ S3 Gateway │ │
│ │ Endpoint │ │
│ └───────────────────────┘ │
│ │
└─────────────────────────────┘
1. Creating the VPC
The VPC is the top-level network boundary for the infrastructure.
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "main-vpc"
}
}
The VPC uses the CIDR block:
10.0.0.0/16
This provides the address space from which the subnets are created.
The /16 gives the VPC a large address range, while smaller /24 networks can be carved out for individual subnets.
I also enabled DNS support and DNS hostnames so resources inside the VPC can use AWS-provided DNS functionality.
2. Dividing the VPC into Subnets
A VPC is a large network, so it is common to divide it into smaller networks called subnets.
I created two:
Public subnet: 10.0.1.0/24
Private subnet: 10.0.2.0/24
The public subnet is located in eu-west-2c, while the private subnet is located in eu-west-2b.
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = "eu-west-2c"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidr
availability_zone = "eu-west-2b"
tags = {
Name = "private-subnet"
}
}
A subnet belongs to one Availability Zone.
Using different Availability Zones also introduces the basic concept of fault isolation: resources can be distributed across physically separate Availability Zones instead of depending entirely on one.
What makes a subnet public?
The name public-subnet does not make a subnet public.
A subnet becomes public when its associated route table provides a route to an Internet Gateway.
This distinction is important.
3. Internet Gateway
The Internet Gateway provides connectivity between the VPC and the internet.
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
However, simply attaching an Internet Gateway to the VPC does not make every subnet public.
The subnet must have a route that directs internet-bound traffic to the Internet Gateway.
That brings us to route tables.
4. Route Tables
A route table determines where network traffic should be sent.
For the public subnet, I created a route table with a default route:
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
}
tags = {
Name = "public-rt"
}
}
The important part is:
0.0.0.0/0 → Internet Gateway
0.0.0.0/0 represents all IPv4 destinations.
Therefore, this route essentially says:
If traffic is destined for somewhere outside the VPC, send it toward the Internet Gateway.
The public subnet is then associated with this route table:
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
The private subnet has a separate route table:
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = {
Name = "private-rt"
}
}
There is no direct route from this route table to the Internet Gateway.
Therefore, resources in the private subnet do not have a direct route to the public internet.
This is what makes the subnet private from a routing perspective.
5. Public vs Private Communication
One of the most useful things I learned from this project is that "private" does not mean "cannot communicate with anything."
Both subnets belong to the same VPC:
VPC: 10.0.0.0/16
Public subnet: 10.0.1.0/24
Private subnet: 10.0.2.0/24
The VPC has a local route that allows communication within the VPC.
Therefore, a resource in the private subnet can communicate with a resource in the public subnet using their private IP addresses, assuming security controls allow the traffic.
The difference is that the private subnet does not have a direct route to the public internet.
A simple way to think about the components is:
- Route table: Where should traffic go?
- Internet Gateway: Provides a path between the VPC and the internet.
- Security Group: Is the traffic allowed?
- Subnet: Which section of the VPC does the resource belong to?
6. Security Groups
I created a security group specifically for SSH access:
resource "aws_security_group" "allow_ssh" {
name = "allow_ssh"
description = "Allow SSH inbound, all outbound"
vpc_id = aws_vpc.main.id
}
The inbound rule allows TCP traffic on port 22 only from my IP address:
resource "aws_vpc_security_group_ingress_rule" "ssh_ipv4" {
security_group_id = aws_security_group.allow_ssh.id
cidr_ipv4 = var.my_ip
from_port = 22
ip_protocol = "tcp"
to_port = 22
}
My IP is represented as a /32 CIDR, meaning a single IPv4 address.
This is preferable to opening SSH to:
0.0.0.0/0
which would allow SSH attempts from any IPv4 address.
For outbound traffic, I allowed all IPv4 traffic:
resource "aws_vpc_security_group_egress_rule" "allow_all_ipv4" {
security_group_id = aws_security_group.allow_ssh.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
Security groups are stateful, meaning return traffic for an allowed connection is automatically permitted.
7. Launching the EC2 Instance
The EC2 instance is placed inside the public subnet:
resource "aws_instance" "main" {
ami = var.ami_id
instance_type = "t2.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.allow_ssh.id]
key_name = aws_key_pair.generated_key.key_name
tags = {
Name = "main-instance"
}
}
When the instance is launched, AWS creates a primary Elastic Network Interface (ENI) for it.
The ENI is essentially the virtual network adapter that connects the EC2 instance to the VPC.
It is associated with things such as:
- The subnet
- A private IP address
- Security groups
The EC2's network interface provides its connection to the VPC and carries its private network identity.
Because the instance is in the public subnet and the subnet is configured to assign public IPv4 addresses to launched instances, the instance can also receive a public IPv4 address.
This allows me to connect to it from outside the VPC using SSH, provided the route and security group rules allow the connection.
8. S3 Gateway VPC Endpoint
The private subnet doesn't have a direct route to the internet.
But I still wanted resources in the private subnet to be able to access Amazon S3 without requiring a NAT Gateway.
For this, I created an S3 Gateway VPC Endpoint:
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.eu-west-2.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [
aws_route_table.private.id
]
tags = {
Name = "s3-gateway-endpoint"
}
}
The endpoint is associated with the private route table.
This provides a private path from the VPC to S3 without requiring the private subnet to have general internet connectivity.
Conceptually:
Private subnet
│
▼
Private route table
│
▼
S3 Gateway Endpoint
│
▼
Amazon S3
This is different from giving the private subnet internet access through a NAT Gateway.
The subnet remains private while still being able to reach the AWS service it needs.
9. SSH Key Management
For SSH authentication, I used Terraform's TLS provider to generate an RSA private key:
resource "tls_private_key" "ec2_key" {
algorithm = "RSA"
rsa_bits = 4096
}
The corresponding public key is registered with AWS:
resource "aws_key_pair" "generated_key" {
key_name = "registry-vpc-key"
public_key = tls_private_key.ec2_key.public_key_openssh
}
Terraform then writes the private key to a local file:
resource "local_file" "private_key" {
content = tls_private_key.ec2_key.private_key_pem
filename = "${path.module}/registry-vpc-key.pem"
file_permission = "0400"
}
The 0400 permission ensures that the private key file is readable only by the file owner.
10. Following an SSH Connection Through the Architecture
Putting everything together, an SSH connection from my computer to the EC2 looks roughly like this:
My Computer
│
│ TCP 22
▼
Internet
│
▼
Internet Gateway
│
▼
Public Subnet
│
▼
EC2 Network Interface
│
▼
Security Group
│
│ Source IP allowed?
│ TCP 22 allowed?
▼
EC2
Several components have different responsibilities in this flow.
The route table provides the path.
The Internet Gateway provides connectivity between the VPC and the internet.
The network interface connects the EC2 to the VPC.
The security group determines whether the incoming connection is permitted.
Understanding these responsibilities makes the architecture much easier to reason about.
11. What I Built
The final infrastructure can be summarized as:
AWS Region: eu-west-2
│
└── VPC: 10.0.0.0/16
│
├── Availability Zone: eu-west-2c
│ └── Public Subnet: 10.0.1.0/24
│ ├── Public Route Table
│ │ └── 0.0.0.0/0 → Internet Gateway
│ │
│ └── EC2
│ └── SSH allowed from my IP
│
└── Availability Zone: eu-west-2b
└── Private Subnet: 10.0.2.0/24
├── Private Route Table
│
└── S3 Gateway VPC Endpoint
The project demonstrates the fundamental building blocks of AWS networking:
VPC → Subnets → Route Tables → Internet Gateway → Security Groups → Network Interfaces → EC2 → VPC Endpoints
The most important lesson for me was understanding that these components are not independent pieces of configuration. They work together to determine where resources live, where their traffic can go, and which traffic is allowed.
This foundation makes it much easier to understand more advanced AWS architectures such as load-balanced applications, private application tiers, NAT Gateways, RDS deployments, ECS, and multi-AZ infrastructure.
Top comments (0)