When I first modularized my Terraform for EKS, everything looked clean…
Until it didn’t work.
Modules were correct.
Code was clean.
Folder structure looked “perfect”.
But Terraform was behaving in ways I didn’t expect.
Resources were creating in weird order.
Dependencies felt invisible.
And debugging became painful.
That’s when I realized:
👉 Understanding Terraform syntax is easy
👉 Understanding how Terraform thinks is the real game
This blog is about that shift.
🧠 The Biggest Misconception
Initially, I thought Terraform runs like a script:
Step 1 → Step 2 → Step 3
But that assumption is completely wrong.
Terraform doesn’t execute line by line.
👉 It builds a dependency graph first, and only then decides what to create and in what order.
Once this clicked, everything started making sense.
🧩 My Project Structure (Real Setup)
Here’s how I organized everything:
modularized/
├── eks/ # Root module (entry point)
│ ├── main.tf
│ ├── variables.tf
│ ├── providers.tf
│ └── backend.tf
├── modules/
│ ├── vpc/
│ ├── iam/
│ ├── eks-cluster/
│ ├── eks-nodes/
│ ├── aws-load-balancer-controller/
│ ├── istio-base/
│ ├── istiod/
│ ├── istio-gateway/
│ └── istio-manifests/
└── environments/
├── dev/
│ ├── terraform.tfvars
│ └── backend.hcl
At a glance, this looks like just folders.
But in reality:
👉 This is a system design mapped into code
🔗 The Real Role of main.tf
Think of main.tf not as a file…
👉 But as an orchestrator
It doesn’t “do” things directly.
It connects modules together using data.
🔹 Step 1: VPC — The Foundation
module "vpc" {
source = "../modules/vpc"
vpc_name = var.vpc_name
vpc_cidr = var.vpc_cidr
availability_zones = var.availability_zones
private_subnet_cidrs = var.private_subnet_cidrs
public_subnet_cidrs = var.public_subnet_cidrs
cluster_name = var.cluster_name
}
This module creates:
- VPC
- Public & private subnets
- Routing
But the important part is not creation…
👉 It’s what it exports
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
🧠 First Realization
Terraform modules don’t “talk” directly.
They communicate through:
👉 Outputs → Inputs
🔹 Step 2: IAM — Identity Layer
module "iam" {
source = "../modules/iam"
cluster_name = var.cluster_name
}
This creates:
- Cluster role
- Node role
- IRSA roles
And exposes:
output "eks_cluster_role_arn" {}
output "eks_nodes_role_arn" {}
🔗 Step 3: EKS Cluster — Where Things Clicked
module "eks_cluster" {
source = "../modules/eks-cluster"
cluster_name = var.cluster_name
cluster_version = var.cluster_version
cluster_role_arn = module.iam.eks_cluster_role_arn
private_subnet_ids = module.vpc.private_subnet_ids
public_subnet_ids = module.vpc.public_subnet_ids
}
This line changed everything for me:
module.iam.eks_cluster_role_arn
👉 This is not just a reference
👉 This is a dependency signal
💥 The Aha Moment
I didn’t define any order like:
- “Create IAM first”
- “Then VPC”
- “Then EKS”
Yet Terraform automatically knew.
Why?
Because:
👉 Dependencies define execution order
🔹 Step 4: Node Groups — Implicit Dependency
module "eks_nodes" {
source = "../modules/eks-nodes"
cluster_name = module.eks_cluster.cluster_id
node_role_arn = module.iam.eks_nodes_role_arn
subnet_ids = module.vpc.private_subnet_ids
}
Now Terraform understands:
- Nodes depend on cluster
- Cluster depends on IAM + VPC
So it builds a graph like:
VPC → IAM → EKS → Nodes
Without you ever writing that flow.
⚠️ Mistake I Made (Important)
At one point, I hardcoded subnet IDs inside my EKS module.
It worked… initially.
But the moment I tried another environment — everything broke.
That’s when I understood:
👉 Hardcoding breaks modular design
👉 Outputs make modules reusable
⚙️ Variables — The Real Power
variable "cluster_name" {
type = string
}
Values come from:
# environments/dev/terraform.tfvars
cluster_name = "dev-cluster"
👉 Same code
👉 Different environments
No duplication.
🧠 What Terraform Actually Does
When you run:
terraform apply
Terraform does NOT just “run code”.
It:
- Reads variables
- Resolves all references
- Builds dependency graph
- Plans execution order
- Applies resources
🔐 Backend — Silent but Critical
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "eks/dev/terraform.tfstate"
region = "us-east-1"
}
}
This enables:
- Remote state
- Team collaboration
- State locking
Without this, things get messy fast.
🧠 Final Shift in Thinking
At the beginning, Terraform felt like:
👉 “Writing infrastructure scripts”
Now it feels like:
👉 “Designing systems using data flow”
That shift changes everything.
💡 Key Takeaways
-
main.tfis not execution — it’s orchestration - Outputs are how modules communicate
- Dependencies are inferred, not written
- Variables make environments scalable
- Terraform is a graph engine, not a script runner
🔗 Repo
https://github.com/jayakrishnayadav24/istio-ip-based-routing
🚀 Final Thought
Once you stop thinking in terms of files…
And start thinking in terms of data flowing between modules…
Terraform stops being just infrastructure code.
👉 It becomes a system design tool.
If this helped you understand Terraform at a deeper level:
⭐ Star the repo
🔁 Share with others
💬 Let me know what confused you — I’ll write about it next
Happy building 🚀
