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)