<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mayank Pratap Singh</title>
    <description>The latest articles on DEV Community by Mayank Pratap Singh (@mayank_pratapsingh_854e9).</description>
    <link>https://dev.to/mayank_pratapsingh_854e9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3847874%2Fb5ccb98b-2fd7-4d33-a1a9-ed5dee05a040.jpg</url>
      <title>DEV Community: Mayank Pratap Singh</title>
      <link>https://dev.to/mayank_pratapsingh_854e9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mayank_pratapsingh_854e9"/>
    <language>en</language>
    <item>
      <title>The Terraform Module Structure I Use for Every AWS Project</title>
      <dc:creator>Mayank Pratap Singh</dc:creator>
      <pubDate>Sat, 28 Mar 2026 15:18:38 +0000</pubDate>
      <link>https://dev.to/mayank_pratapsingh_854e9/the-terraform-module-structure-i-use-for-every-aws-project-cak</link>
      <guid>https://dev.to/mayank_pratapsingh_854e9/the-terraform-module-structure-i-use-for-every-aws-project-cak</guid>
      <description>&lt;p&gt;If you've worked on more than one AWS project with Terraform, you've probably hit this wall: what starts as a clean main.tf slowly turns into hundreds of lines of spaghetti — mixing networking, IAM, compute, and databases in one place.&lt;br&gt;
I've seen this pattern repeated across multiple teams. The fix isn't working harder — it's structuring your modules the right way from day one.&lt;br&gt;
In this article, I'll walk you through the exact Terraform module structure I use for every AWS project, why each decision was made, and the mistakes it prevents.&lt;/p&gt;

&lt;p&gt;Why module structure matters more than you think&lt;br&gt;
Terraform is infrastructure-as-code, which means all the same software engineering principles apply: separation of concerns, reusability, and avoiding duplication. A poor module structure leads to:&lt;/p&gt;

&lt;p&gt;State file conflicts when multiple engineers work simultaneously&lt;br&gt;
Inability to reuse code across environments (dev/staging/prod)&lt;br&gt;
Blast radius issues — a change in one area accidentally destroying another&lt;br&gt;
Slow terraform plan times because everything lives in one giant state&lt;/p&gt;

&lt;p&gt;Real example: At a previous project, a single monolithic main.tf file had grown to 2,400 lines. A junior engineer changed a security group rule and accidentally modified an RDS parameter group in the same apply. Good module structure would have isolated these completely.&lt;/p&gt;

&lt;p&gt;The folder structure&lt;br&gt;
Here's the top-level layout I start every project with:&lt;/p&gt;

&lt;p&gt;project-infra/&lt;br&gt;
├── environments/&lt;br&gt;
│   ├── dev/&lt;br&gt;
│   │   ├── main.tf&lt;br&gt;
│   │   ├── variables.tf&lt;br&gt;
│   │   └── terraform.tfvars&lt;br&gt;
│   ├── staging/&lt;br&gt;
│   └── prod/&lt;br&gt;
├── modules/&lt;br&gt;
│   ├── networking/&lt;br&gt;
│   ├── eks/&lt;br&gt;
│   ├── rds/&lt;br&gt;
│   ├── iam/&lt;br&gt;
│   └── s3/&lt;br&gt;
└── global/&lt;br&gt;
    ├── backend.tf&lt;br&gt;
    └── providers.tf&lt;/p&gt;

&lt;p&gt;Each folder under modules/ is self-contained — it has its own main.tf, variables.tf, and outputs.tf. Nothing leaks between modules except through explicit outputs and inputs.&lt;/p&gt;

&lt;p&gt;Breaking it down: the networking module&lt;br&gt;
The networking module is always the foundation — every other module depends on it. Here's what mine looks like:&lt;/p&gt;

&lt;p&gt;hclmodule "networking" {&lt;br&gt;
  source = "../../modules/networking"&lt;/p&gt;

&lt;p&gt;vpc_cidr             = var.vpc_cidr&lt;br&gt;
  public_subnet_cidrs  = var.public_subnet_cidrs&lt;br&gt;
  private_subnet_cidrs = var.private_subnet_cidrs&lt;br&gt;
  availability_zones   = var.availability_zones&lt;br&gt;
  environment          = var.environment&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Inside modules/networking/outputs.tf, I export the VPC ID, subnet IDs, and route table IDs. Every other module consumes these outputs — nobody hardcodes subnet IDs.&lt;/p&gt;

&lt;p&gt;hcloutput "vpc_id" {&lt;br&gt;
  description = "The ID of the VPC"&lt;br&gt;
  value       = aws_vpc.main.id&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;output "private_subnet_ids" {&lt;br&gt;
  description = "List of private subnet IDs"&lt;br&gt;
  value       = aws_subnet.private[*].id&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Environment-specific configuration&lt;br&gt;
Each environment folder (dev/, staging/, prod/) contains only the configuration that differs per environment. The actual infrastructure logic lives in the shared modules.&lt;/p&gt;

&lt;p&gt;hcl# environments/prod/terraform.tfvars&lt;/p&gt;

&lt;p&gt;environment          = "prod"&lt;br&gt;
vpc_cidr             = "10.0.0.0/16"&lt;br&gt;
public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]&lt;br&gt;
private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]&lt;br&gt;
eks_node_count       = 5&lt;br&gt;
eks_instance_type    = "m5.xlarge"&lt;br&gt;
db_instance_class    = "db.r5.large"&lt;/p&gt;

&lt;p&gt;For dev, the same variables get smaller values — but the same module code runs. This is where you get real reusability.&lt;/p&gt;

&lt;p&gt;Remote state and the backend&lt;br&gt;
Each environment gets its own S3 backend with a DynamoDB lock table. This is non-negotiable on a team:&lt;/p&gt;

&lt;p&gt;hclterraform {&lt;br&gt;
  backend "s3" {&lt;br&gt;
    bucket         = "mycompany-tf-state"&lt;br&gt;
    key            = "prod/terraform.tfstate"&lt;br&gt;
    region         = "us-east-1"&lt;br&gt;
    dynamodb_table = "terraform-lock"&lt;br&gt;
    encrypt        = true&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Tip: Use a separate AWS account (or at minimum a separate S3 bucket with strict IAM policies) for storing state files. State files contain sensitive data including database passwords and resource IDs.&lt;/p&gt;

&lt;p&gt;Three rules I never break&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Modules never call other modules directly. Only the environment-level main.tf calls modules. This prevents circular dependencies and keeps the dependency graph flat and readable.&lt;/li&gt;
&lt;li&gt;No hardcoded values inside modules. Every value that could differ between environments must be a variable with a description and type constraint. If I'm tempted to hardcode something, that's a signal it belongs in tfvars.&lt;/li&gt;
&lt;li&gt;Every output has a description. Outputs without descriptions are useless to future you and your teammates. A one-line description of what the output is and when to use it saves hours of confusion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Putting it together&lt;br&gt;
This structure has served me across projects ranging from simple 3-tier web apps to multi-account EKS platforms. The upfront investment in structure pays back within weeks — especially when onboarding new engineers or debugging a production incident at 2am.&lt;br&gt;
Start with this layout even for small projects. It costs almost nothing extra and eliminates an entire class of infrastructure bugs before they happen.&lt;/p&gt;

&lt;p&gt;In the next article, I'll go deeper into the EKS module specifically — how I configure node groups, IRSA roles, and the Helm chart integration that makes it production-ready from day one.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
