Modules: reusable infrastructure
DRY infrastructure you can share.
Once you have written the same VPC-plus-subnets-plus-security-groups block three times, you want to write it once and reuse it. A module is exactly that: a folder of .tf files with inputs (variables) and outputs, called from other configurations. Every Terraform configuration is already a module — the root module — and calling other modules is how you build infrastructure from reusable, tested components instead of copy-pasted blocks.
module "network" {source = "./modules/network" # a local module foldercidr = "10.0.0.0/16"environment = var.environment}module "web" {source = "./modules/web"subnet_ids = module.network.private_subnet_ids # wire modules together via outputs}
Inputs, outputs, and wiring
A module declares variables for what its caller must supply and outputs for what it exposes back. You pass values in as arguments on the module block, and read results as module.<name>.<output>. That input/output contract is what lets you wire modules together — the network module outputs subnet IDs, the web module takes them as input — and lets you change a module’s internals without touching its callers, as long as the contract holds.
Where modules come from
The source argument can point at a local path (./modules/network), the public Terraform Registry (a versioned, community or vendor module), a private registry, or a Git URL. Registry and Git sources take a version, which you should always pin — an unpinned module can change under you exactly like an unpinned provider. Public modules like terraform-aws-modules/vpc are battle-tested and a good way to avoid reinventing a correct VPC.
module "vpc" {source = "terraform-aws-modules/vpc/aws" # public registry moduleversion = "5.8.1" # PIN itname = "prod-vpc"cidr = "10.0.0.0/16"azs = ["us-east-1a", "us-east-1b"]private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]}