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)