# terraform / modules / architecture
Building Blocks of Infrastructure: Reusable Modules
A Terraform module is a container for multiple resources that are used together. Instead of copy-pasting 200 lines of VPC, subnet, route table, and gateway definitions across every application repository, you package it as a reusable module with defined inputs and outputs.
Root Modules vs Child Modules
Every Terraform project has at least one module, known as the Root Module (the working directory where you run terraform apply). When the root module references another directory or repository using a module block, that called block is a Child Module.
| Type | Definition | Execution |
|---|---|---|
| Root Module | Top-level directory with backend.tf and CLI execution context | Runs terraform init & apply |
| Child Module | Reusable directory or Git repository instantiated by a root module | Called via module "name" { source = ... } |
| Published Module | Community or private verified module published to Terraform Registry | Pinned with exact version tags |
Encapsulation & Abstraction
Application developers don't need to know every nuance of AWS VPC Internet Gateways and NAT routing. They pass cidr = "10.0.0.0/16" to the VPC module, and the module provisions standard company-approved network architecture.
Standard Module Directory Layout
modules/aws-vpc/ ├── main.tf # Resources: VPC, subnets, route tables ├── variables.tf # Input parameters required by module ├── outputs.tf # Values exported to caller ├── versions.tf # Required Terraform & provider versions └── README.md # Usage examples and documentation
Golden Rules for Module Authors
- No Hardcoded Backend: Never declare a
backendblock inside a child module. - No Hardcoded Provider Configuration: Child modules should inherit providers configured in the root module.
- Explicit Inputs: Document all variables with clear descriptions and type constraints.
Calling a Module in the Root Project
module "vpc" { source = "./modules/aws-vpc" # Pass required input arguments vpc_cidr = "10.0.0.0/16" public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] environment = "production" } # Access exported outputs from the module resource "aws_instance" "web" { ami = "ami-0123456789abcdef0" instance_type = "t3.micro" subnet_id = module.vpc.public_subnet_ids[0] }
Run terraform init After Adding Modules
Whenever you add, modify, or change the source of a module block, you must run terraform init so Terraform can download or index the module into the local .terraform/modules/ cache.
Where Modules Live: Sources & Versioning
| Source Type | Source Syntax Example | Use Case |
|---|---|---|
| Local Directory | source = "./modules/vpc" |
Monorepo or project-specific sub-components |
| Git Repository | source = "git::https://github.com/myorg/tf-vpc.git?ref=v1.4.0" |
Private organization shared libraries with Git tag pinning |
| Terraform Registry | source = "terraform-aws-modules/vpc/aws" |
Battle-tested community modules for AWS, Azure, GCP |
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0" name = "prod-vpc" cidr = "10.0.0.0/16" azs = ["ap-south-1a", "ap-south-1b"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"] }
Always Pin Module Versions
Never omit the version or ?ref= tag when sourcing external modules. Without version pinning, upstream breaking changes could instantly break your production CI/CD builds.
Module Composition in Real Systems
Outputs:
vpc_id, subnetsTakes
vpc_id, outputs sg_idTakes
subnet_ids & sg_idEnterprise Best Practices
| Rule | Why It Matters |
|---|---|
| Single Responsibility | Keep modules focused (e.g. modules/s3 or modules/vpc, not one gigantic monomodule). |
| Expose Essential Outputs | Output ARNs and IDs so downstream resources can integrate smoothly. |
| Use Semantic Versioning | Tag Git repositories with v1.0.0, v1.1.0 for safe updates. |
| Document with terraform-docs | Auto-generate markdown tables for inputs and outputs in README.md. |