VPC Module (Complete Code + Real Explanation)
Before touching EKS, Istio, or routing — everything depends on one thing:
👉 Your network
If the VPC is wrong:
- Nodes won’t join
- ALB won’t work
- Pods won’t get IPs
- Routing will fail in weird ways
So in this part, I’ll walk through the entire VPC module from my setup, using the exact code — and explain what each part is doing and why it exists.
📂 Module Files
This module consists of 3 files:
modules/vpc/
├── main.tf
├── variables.tf
└── outputs.tf
📄 variables.tf
This file defines what inputs the module expects.
variable "vpc_name" {
description = "Name of the VPC"
type = string
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "availability_zones" {
description = "Availability zones"
type = list(string)
}
variable "private_subnet_cidrs" {
description = "CIDR blocks for private subnets"
type = list(string)
}
variable "public_subnet_cidrs" {
description = "CIDR blocks for public subnets"
type = list(string)
}
variable "cluster_name" {
description = "Name of the EKS cluster"
type = string
}
🧠 What this means
This module is not hardcoded.
Everything is controlled from outside:
- CIDR ranges
- AZs
- Subnet layout
- Cluster name
👉 That’s what makes it reusable across:
- dev
- staging
- prod
📄 main.tf (Core Logic)
This is where actual infrastructure is created.
1. VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = var.vpc_name
}
}
Why this matters
-
cidr_block→ defines network size -
enable_dns_support→ required for internal communication -
enable_dns_hostnames→ required for:- EKS
- ALB
- service discovery
👉 If DNS is off, things break silently.
2. Private Subnets (EKS Nodes)
resource "aws_subnet" "private_subnets" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
What’s happening
-
countcreates multiple subnets - Each subnet is tied to an AZ
🔥 Important Tags
tags = {
Name = "${var.vpc_name}-private-${var.availability_zones[count.index]}"
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/${var.cluster_name}" = "owned"
}
Why these tags matter
These are not optional
internal-elb
→ used by AWS to place internal load balancerscluster tag
→ required for EKS to discover subnets
👉 Missing this = ALB or services fail later
3. Public Subnets (ALB Layer)
resource "aws_subnet" "public_subnets" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
Key difference
map_public_ip_on_launch = true
👉 Instances here get public IPs
Tag Difference
"kubernetes.io/role/elb" = "1"
👉 This tells AWS:
“Use this subnet for internet-facing load balancers”
4. Internet Gateway
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.vpc_name}-igw"
}
}
Purpose
Allows:
👉 Public subnet → Internet
5. Elastic IP (for NAT)
resource "aws_eip" "nat" {
count = length(var.public_subnet_cidrs)
domain = "vpc"
tags = {
Name = "${var.vpc_name}-nat-${count.index + 1}"
}
depends_on = [aws_internet_gateway.igw]
}
Why EIP?
NAT Gateway needs a public IP
6. NAT Gateway (Critical)
resource "aws_nat_gateway" "nat" {
count = length(var.public_subnet_cidrs)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public_subnets[count.index].id
What it does
Private Subnet → NAT → Internet
👉 Nodes can:
- pull Docker images
- access APIs
BUT:
👉 They don’t get public IP (secure)
7. Private Route Table
resource "aws_route_table" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat[count.index].id
}
}
Meaning
All outbound traffic → NAT
8. Public Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
Meaning
Public subnet → direct internet
9. Route Table Associations
resource "aws_route_table_association" "private" {
count = length(var.private_subnet_cidrs)
subnet_id = aws_subnet.private_subnets[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public_subnets[count.index].id
route_table_id = aws_route_table.public.id
}
Why needed
👉 Subnets don’t automatically get routing
You must attach:
- subnet → route table
📄 outputs.tf
This file exposes values for other modules.
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "IDs of the private subnets"
value = aws_subnet.private_subnets[*].id
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public_subnets[*].id
}
Why outputs matter
These are used by:
- EKS cluster
- Node groups
- ALB controller
Example:
private_subnet_ids = module.vpc.private_subnet_ids
👉 This creates dependency automatically.
🧠 Final Architecture
Internet
│
Internet Gateway
│
Public Subnets (ALB)
│
NAT Gateway
│
Private Subnets (EKS Nodes)
⚠️ Real Things That Break in Production
- Missing subnet tags → ALB won’t create
- No NAT → nodes can’t pull images
- Wrong AZ mapping → cluster unstable
- Public nodes → security issue
🧠 Key Takeaways
- VPC is the foundation of everything
- Subnet tagging is critical for EKS
- NAT enables private nodes
- Outputs drive Terraform dependencies
🚀 Next
In Part 2:
👉 IAM Module — IRSA explained properly
👉 How pods assume AWS roles
👉 Why OIDC is required
If you're building EKS in production, this part is not optional — this is where most issues actually start.
Top comments (0)