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)