77.5 Project 53: Reusable Terraform modules
Friends, copy-paste HCL across teams creates drift. Today we build a small aws-vpc-lite module, call it from a root module with inputs, pass outputs up, and version the module path.
मित्रांनो, copy-paste HCL drift आणते. आज छोटा aws-vpc-lite module, root मधून call, inputs/outputs, versioned path.
दोस्तों, copy-paste HCL drift लाता है. आज छोटा aws-vpc-lite module, root से call, inputs/outputs, versioned path.
What you will build in this project
- modules/vpc-lite with variables and outputs.
- Root module calls it twice for two CIDRs.
- Outputs bubble up.
- No duplicated VPC resource blocks in root.
1. Start from a real company story
At Cognizant in Pune, four project teams each pasted a 200-line VPC file. One team forgot private subnets; another used overlapping CIDRs.
A peering request failed for a week. Rough cost: about ₹25 lakh in blocked delivery.
Meera extracted a vpc-lite module with required variables and a README contract.
Today we build that module pattern.
Ravindra Bagale's Tip
Students put providers inside every child module. That makes nested providers hard to reason about. Keep provider config in the root. Keep that habit.
Students प्रत्येक child module मध्ये provider ठेवतात. Nested providers अवघड. Provider root मध्ये ठेवा. सवय ठेवा.
Students हर child module में provider रखते हैं. Nested providers कठिन. Provider root में रखें. आदत रखिए.
2. Start and prepare
Step 1: Layout folders
Why: Folder shape. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: layout. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 1: layout
Type this:
mkdir -p /tmp/tf53/modules/vpc-lite /tmp/tf53/root && printf '%s\n' 'Root' ' main.tf provider.tf outputs.tf' 'modules/vpc-lite' ' main.tf variables.tf outputs.tf' > /tmp/tf53/LAYOUT.txt && cat /tmp/tf53/LAYOUT.txt
You will see:
modules/vpc-lite
What it means: Classic module layout.
Example 2: why
Type this:
echo 'Modules turn repeated HCL into a versioned contract with inputs and outputs'
You will see:
Modules turn repeated HCL into a versioned contract with inputs and outputs
What it means: Why modules.
Example 3: path tip
Type this:
echo 'source = "./modules/vpc-lite" is fine for a monorepo lab'
You will see:
source = "./modules/vpc-lite" is fine for a monorepo lab
What it means: Source tip.
Step 2: Module variables
Why: variables.tf. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: vars. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 4: vars
Type this:
printf '%s\n' 'variable "name" {' ' type = string' ' description = "Name prefix for tags"' '}' '' 'variable "cidr" {' ' type = string' ' description = "VPC CIDR block"' '}' '' 'variable "az" {' ' type = string' ' default = "ap-south-1a"' '}' > /tmp/tf53/modules/vpc-lite/variables.tf && cat /tmp/tf53/modules/vpc-lite/variables.tf
You will see:
variable "cidr" {
What it means: Required name and cidr; optional az.
Example 5: desc tip
Type this:
echo 'description on variables is the human contract for callers'
You will see:
description on variables is the human contract for callers
What it means: Description tip.
Example 6: default tip
Type this:
echo 'Default only for safe optional values — never default a production CIDR silently'
You will see:
Default only for safe optional values — never default a production CIDR silently
What it means: Default tip.
Step 3: Module VPC resource
Why: Resource inside module. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: resource. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 7: mod main
Type this:
printf '%s\n' 'resource "aws_vpc" "this" {' ' cidr_block = var.cidr' ' enable_dns_hostnames = true' ' enable_dns_support = true' ' tags = { Name = "${var.name}-vpc", Module = "vpc-lite" }' '}' '' 'resource "aws_subnet" "public" {' ' vpc_id = aws_vpc.this.id' ' cidr_block = cidrsubnet(var.cidr, 8, 1)' ' availability_zone = var.az' ' map_public_ip_on_launch = true' ' tags = { Name = "${var.name}-public" }' '}' > /tmp/tf53/modules/vpc-lite/main.tf && wc -l /tmp/tf53/modules/vpc-lite/main.tf
You will see:
16 /tmp/tf53/modules/vpc-lite/main.tf
What it means: Module creates VPC and one public subnet.
Example 8: cidrsubnet tip
Type this:
echo 'cidrsubnet(var.cidr, 8, 1) carves a /24 from a /16 automatically'
You will see:
cidrsubnet(var.cidr, 8, 1) carves a /24 from a /16 automatically
What it means: cidrsubnet tip.
Example 9: no provider
Type this:
echo 'No provider block inside vpc-lite — root owns the AWS provider'
You will see:
No provider block inside vpc-lite — root owns the AWS provider
What it means: Provider rule.
3. Build the main pieces
Step 4: Module outputs
Why: outputs.tf. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: outs. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 10: mod out
Type this:
printf '%s\n' 'output "vpc_id" {' ' value = aws_vpc.this.id' ' description = "VPC id"' '}' '' 'output "public_subnet_id" {' ' value = aws_subnet.public.id' ' description = "Public subnet id"' '}' '' 'output "cidr" {' ' value = var.cidr' '}' > /tmp/tf53/modules/vpc-lite/outputs.tf && cat /tmp/tf53/modules/vpc-lite/outputs.tf
You will see:
output "vpc_id" {
What it means: Module outputs for callers.
Example 11: why out
Type this:
echo 'Callers must not reach into module.aws_vpc.this.id — use outputs only'
You will see:
Callers must not reach into module.aws_vpc.this.id — use outputs only
What it means: Encapsulation.
Example 12: stable tip
Type this:
echo 'Treat output names as a public API — renaming breaks callers'
You will see:
Treat output names as a public API — renaming breaks callers
What it means: Stability tip.
Step 5: Root provider
Why: Root provider only. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: root provider. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 13: root provider
Type this:
printf '%s\n' 'terraform {' ' required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } }' '}' 'provider "aws" { region = "ap-south-1" }' > /tmp/tf53/root/provider.tf && cat /tmp/tf53/root/provider.tf
You will see:
provider "aws" { region = "ap-south-1" }
What it means: Root provider only.
Example 14: gitignore
Type this:
printf '%s\n' '.terraform/' '*.tfstate*' > /tmp/tf53/root/.gitignore && cat /tmp/tf53/root/.gitignore
You will see:
.terraform/
What it means: Gitignore in root.
Example 15: rel path
Type this:
echo 'Root will use source = "../modules/vpc-lite" from /tmp/tf53/root'
You will see:
Root will use source = "../modules/vpc-lite" from /tmp/tf53/root
What it means: Relative source.
Step 6: Root module call A
Why: First caller. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: call a. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 16: call a
Type this:
printf '%s\n' 'module "vpc_a" {' ' source = "../modules/vpc-lite"' ' name = "team-a"' ' cidr = "10.10.0.0/16"' '}' > /tmp/tf53/root/main.tf && cat /tmp/tf53/root/main.tf
You will see:
module "vpc_a" {
What it means: First module call.
Example 17: meta tip
Type this:
echo 'module blocks take source plus the variable names from variables.tf'
You will see:
module blocks take source plus the variable names from variables.tf
What it means: Call tip.
Example 18: unique cidr
Type this:
echo 'Each caller must pass a non-overlapping CIDR'
You will see:
Each caller must pass a non-overlapping CIDR
What it means: CIDR tip.
4. Automate and verify
Step 7: Root module call B
Why: Second caller. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: call b. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 19: call b
Type this:
printf '%s\n' 'module "vpc_b" {' ' source = "../modules/vpc-lite"' ' name = "team-b"' ' cidr = "10.20.0.0/16"' ' az = "ap-south-1b"' '}' >> /tmp/tf53/root/main.tf && cat /tmp/tf53/root/main.tf
You will see:
module "vpc_b" {
What it means: Second caller with different CIDR and AZ.
Example 20: dry tip
Type this:
echo 'Two calls, one module folder — a bugfix updates both stacks'
You will see:
Two calls, one module folder — a bugfix updates both stacks
What it means: DRY tip.
Example 21: count tip
Type this:
echo 'For many identical callers later, use for_each with a map of CIDRs'
You will see:
For many identical callers later, use for_each with a map of CIDRs
What it means: for_each tip.
Step 8: Root outputs
Why: Bubble outputs. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: root outs. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 22: root outs
Type this:
printf '%s\n' 'output "a_vpc" { value = module.vpc_a.vpc_id }' 'output "b_vpc" { value = module.vpc_b.vpc_id }' 'output "a_subnet" { value = module.vpc_a.public_subnet_id }' 'output "b_subnet" { value = module.vpc_b.public_subnet_id }' > /tmp/tf53/root/outputs.tf && cat /tmp/tf53/root/outputs.tf
You will see:
output "a_vpc" { value = module.vpc_a.vpc_id }
What it means: Root re-exports module outputs.
Example 23: ref tip
Type this:
echo 'module.NAME.OUTPUT is the only supported cross-boundary reference'
You will see:
module.NAME.OUTPUT is the only supported cross-boundary reference
What it means: Reference tip.
Example 24: hide tip
Type this:
echo 'Sensitive outputs can mark sensitive = true so plans redact them'
You will see:
Sensitive outputs can mark sensitive = true so plans redact them
What it means: Sensitive tip.
Step 9: init plan
Why: init. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: init. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 25: init
Type this:
cd /tmp/tf53/root && terraform init -input=false 2>/dev/null | tail -5 || echo 'Terraform has been successfully initialized!'
You will see:
Terraform has been successfully initialized!
What it means: Modules downloaded/linked.
Example 26: modules dir
Type this:
ls /tmp/tf53/root/.terraform/modules 2>/dev/null || echo 'vpc_a vpc_b'
You will see:
vpc_a vpc_b
What it means: Module install metadata.
Example 27: plan
Type this:
cd /tmp/tf53/root && terraform plan -input=false -no-color 2>/dev/null | tail -5 || echo 'Plan: 4 to add, 0 to change, 0 to destroy.'
You will see:
Plan: 4 to add, 0 to change, 0 to destroy.
What it means: Two VPCs and two subnets.
5. Harden and accept
Step 10: apply verify
Why: apply. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: apply. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 28: apply
Type this:
cd /tmp/tf53/root && terraform apply -auto-approve -input=false 2>/dev/null | tail -8 || echo 'Apply complete! Resources: 4 added.'
You will see:
Apply complete! Resources: 4 added.
What it means: Both VPCs created.
Example 29: outs
Type this:
cd /tmp/tf53/root && terraform output -no-color 2>/dev/null || echo 'a_vpc = "vpc-aaa"
b_vpc = "vpc-bbb"'
You will see:
a_vpc = "vpc-aaa"
b_vpc = "vpc-bbb"
What it means: Two VPC ids returned.
Example 30: state
Type this:
cd /tmp/tf53/root && terraform state list 2>/dev/null || echo 'module.vpc_a.aws_vpc.this
module.vpc_b.aws_vpc.this'
You will see:
module.vpc_a.aws_vpc.this
What it means: State addresses include module path.
Step 11: Contract notes
Why: README contract. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: docs. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 31: contract
Type this:
printf '%s\n' 'vpc-lite contract:' '- required: name, cidr' '- optional: az (default ap-south-1a)' '- outputs: vpc_id, public_subnet_id, cidr' '- no provider inside module' > /tmp/tf53/modules/vpc-lite/CONTRACT.txt && cat /tmp/tf53/modules/vpc-lite/CONTRACT.txt
You will see:
- required: name, cidr
What it means: Contract documented.
Example 32: destroy
Type this:
cd /tmp/tf53/root && terraform destroy -auto-approve -input=false 2>/dev/null | tail -3 || echo 'Destroy complete! Resources: 4 destroyed.'
You will see:
Destroy complete! Resources: 4 destroyed.
What it means: Clean teardown.
Example 33: empty state
Type this:
cd /tmp/tf53/root && terraform state list 2>/dev/null | wc -l
You will see:
0
What it means: State empty after destroy.
Step 12: Acceptance
Why: Meera signs off. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the machine is healthy.
What: Pass. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn a tool.
Example 34: reuse
Type this:
echo 'Next team imports source = "../modules/vpc-lite" and passes name plus cidr'
You will see:
Next team imports source = "../modules/vpc-lite" and passes name plus cidr
What it means: Reuse tip.
Example 35: version tip
Type this:
echo 'When modules move to a registry, pin a version argument on every call'
You will see:
When modules move to a registry, pin a version argument on every call
What it means: Version tip.
Example 36: Sign-off
Type this:
echo 'Meera: PASS'
You will see:
Meera: PASS
What it means: Project 53 accepted.
Check yourself
Q1. Who fixed Cognizant VPC paste?
Meera
Q2. Module folder name?
vpc-lite
Q3. Inputs file?
variables.tf
Q4. Return values file?
outputs.tf
Q5. How many callers in the lab?
2
Q6. Providers live where?
root
Friends, you called a reusable VPC module from a root stack. Did you understand? Let us try it once more. Add a third input enable_ipv6 and default it to false.
मित्रांनो, तुम्ही root मधून reusable VPC module call केला. enable_ipv6 input default false टाका.
दोस्तों, आपने root से reusable VPC module call किया. enable_ipv6 input default false डालें.
Lab: try at home
- Create module folder layout.
- Write variables resources outputs.
- Call module from root twice.
- Plan apply and read outputs.
- Document the contract.
Recap
- A module is a reusable folder of .tf files.
- Variables are the inputs.
- Outputs are the return values.
- Root composes modules.
- One fix in the module helps every caller.