DEV Community

POTHURAJU JAYAKRISHNA YADAV
POTHURAJU JAYAKRISHNA YADAV

Posted on

Terraform Modular EKS + Istio — Part 1

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
Enter fullscreen mode Exit fullscreen mode

📄 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
}
Enter fullscreen mode Exit fullscreen mode

🧠 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

What’s happening

  • count creates 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"
}
Enter fullscreen mode Exit fullscreen mode

Why these tags matter

These are not optional

  • internal-elb
    → used by AWS to place internal load balancers

  • cluster 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
Enter fullscreen mode Exit fullscreen mode

Key difference

map_public_ip_on_launch = true
Enter fullscreen mode Exit fullscreen mode

👉 Instances here get public IPs


Tag Difference

"kubernetes.io/role/elb" = "1"
Enter fullscreen mode Exit fullscreen mode

👉 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What it does

Private Subnet → NAT → Internet
Enter fullscreen mode Exit fullscreen mode

👉 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Why outputs matter

These are used by:

  • EKS cluster
  • Node groups
  • ALB controller

Example:

private_subnet_ids = module.vpc.private_subnet_ids
Enter fullscreen mode Exit fullscreen mode

👉 This creates dependency automatically.


🧠 Final Architecture

Internet
   │
Internet Gateway
   │
Public Subnets (ALB)
   │
NAT Gateway
   │
Private Subnets (EKS Nodes)
Enter fullscreen mode Exit fullscreen mode

⚠️ 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)