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 ๐

Top comments (0)