By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
(For engineers who need to ship, not just pass exams)
You’re a cloud engineer at a startup. Your team just inherited a Terraform repo with 12,000 lines of spaghetti code—duplicate VPC setups, hardcoded AMIs, and security groups copy-pasted across environments. Your CTO says: “We need to standardize this in 2 weeks or we’re switching to Pulumi.”
Modules are your escape hatch.They let you package reusable infrastructure (like a VPC, EKS cluster, or RDS instance) into a single, versioned unit. Instead of rewriting the same aws_instance block 50 times, you call a module like this:
aws_instance
module "web_server" { source = "./modules/ec2-web-server" # Local path # or source = "terraform-aws-modules/ec2-instance/aws" # Public registry instance_type = "t3.micro" }
Why this matters in production:- Avoid drift: If you hardcode resources, every environment (dev/stage/prod) diverges. Modules enforce consistency.- Security: A single security-group module can enforce least-privilege rules across all teams.- Cost control: A cost-optimized-eks module can default to spot instances and auto-scaling.- Compliance: Audit one module instead of 500 files.- Disaster recovery: If AWS changes an API, you update one module instead of grepping 100 files.
security-group
cost-optimized-eks
Real-world scenario:You’re migrating from a monolithic Terraform repo to a modular design. Some teams need local modules (for custom internal tools), while others should use public registry modules (for AWS best practices). You need to: 1. Refactor existing code into modules.2. Decide when to use local vs. registry modules.3. Version modules to avoid breaking changes.
aws_ssm_parameter
source
source = "terraform-aws-modules/vpc/aws" version = "3.14.0"
terraform init
./modules/vpc
terraform-aws-modules/vpc/aws
terraform login
variables.tf
instance_type
vpc_id
validation { condition = var.instance_type != "t2.micro" }
outputs.tf
security_group_id
v1.0.0
latest
version = "~> 3.0"
network
vpc
version = ">= 2.0.0"
>= 1.0.0
aws configure
We’ll use the official AWS VPC module from the Terraform Registry.
mkdir tf-vpc-module && cd tf-vpc-module touch main.tf
Paste this into main.tf:
main.tf
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-east-1" } module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "3.14.0" # Pin to a specific version name = "my-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"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"] enable_nat_gateway = true single_nat_gateway = true # Cost optimization: one NAT for all private subnets }
terraform init # Downloads the module terraform plan # Verify the changes terraform apply -auto-approve
aws ec2 describe-vpcs --filters "Name=cidr,Values=10.0.0.0/16"
Expected output:
{ "Vpcs": [ { "VpcId": "vpc-12345678", "CidrBlock": "10.0.0.0/16", "State": "available" } ] }
terraform destroy -auto-approve
Now, let’s refactor a hardcoded EC2 instance into a local module.
mkdir -p modules/ec2-web-server touch modules/ec2-web-server/{main.tf,variables.tf,outputs.tf}
Paste this into modules/ec2-web-server/main.tf:
modules/ec2-web-server/main.tf
resource "aws_instance" "web" { ami = var.ami_id instance_type = var.instance_type subnet_id = var.subnet_id tags = { Name = var.instance_name } }
Paste this into modules/ec2-web-server/variables.tf:
modules/ec2-web-server/variables.tf
variable "ami_id" { description = "AMI ID for the EC2 instance" type = string } variable "instance_type" { description = "EC2 instance type" type = string default = "t3.micro" } variable "subnet_id" { description = "Subnet ID to launch the instance in" type = string } variable "instance_name" { description = "Name tag for the instance" type = string default = "web-server" }
Paste this into modules/ec2-web-server/outputs.tf:
modules/ec2-web-server/outputs.tf
output "instance_id" { description = "ID of the EC2 instance" value = aws_instance.web.id } output "public_ip" { description = "Public IP of the EC2 instance" value = aws_instance.web.public_ip }
Update main.tf:
module "web_server" { source = "./modules/ec2-web-server" ami_id = "ami-0c55b159cbfafe1f0" # Amazon Linux 2 in us-east-1 instance_type = "t3.micro" subnet_id = module.vpc.public_subnets[0] # Use the VPC module's output instance_name = "my-web-server" } output "web_server_public_ip" { value = module.web_server.public_ip }
terraform init # Re-initializes to detect the new module terraform apply -auto-approve
aws ec2 describe-instances --filters "Name=tag:Name,Values=my-web-server"
{ "Reservations": [ { "Instances": [ { "InstanceId": "i-1234567890abcdef0", "PublicIpAddress": "54.165.123.45" } ] } ] }
vault_generic_secret
aws_iam_policy
*
spot_price = "0.02"
count
for_each
count = var.create_nat_gateway ? 1 : 0
tags
description
terraform state list
terraform apply
version = "x.y.z"
us-east-1
terraform plan
Error: Cycle detected
InvalidParameterValue
validation
module.a.module.b.module.c
source = "github.com/terraform-aws-modules/vpc"
✅ source = "terraform-aws-modules/vpc/aws" version = "3.14.0"
"How do you reference a module’s output?"
module.vpc.vpc_id
❌ aws_vpc.vpc.id (wrong resource type)
aws_vpc.vpc.id
"What happens if you don’t pin a module version?"
version = "3.14.0"
version = "~> 3.14"
3.14.1
3.15.0
version = ">= 3.0"
"You need to deploy a highly available RDS PostgreSQL instance with encryption at rest. Which module should you use?"- ✅ terraform-aws-modules/rds/aws (supports encryption, multi-AZ, and parameter groups).- ❌ Writing a custom module (reinventing the wheel).- ❌ Using aws_db_instance directly (no built-in best practices).
terraform-aws-modules/rds/aws
aws_db_instance
Given this code:
resource "aws_s3_bucket" "logs" { bucket = "my-company-logs-${random_id.suffix.hex}" acl = "private" versioning { enabled = true } server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } }
Task:1. Move this into a local module (./modules/s3-log-bucket).2. Make the bucket name configurable via a variable.3. Add an output for the bucket ARN.4. Call the module from main.tf.
./modules/s3-log-bucket
Solution:
# modules/s3-log-bucket/main.tf resource "aws_s3_bucket" "this" { bucket = var.bucket_name acl = "private" versioning { enabled = true } server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } } # modules/s3-log-bucket/variables.tf variable "bucket_name" { description = "Name of the S3 bucket" type = string } # modules/s3-log-bucket/outputs.tf output "bucket_arn" { description = "ARN of the S3 bucket" value = aws_s3_bucket.this.arn } # main.tf module "log_bucket" { source = "./modules/s3-log-bucket" bucket_name = "my-company-logs-${random_id.suffix.hex}" } output "log_bucket_arn" { value = module.log_bucket.bucket_arn }
Why it works:- The module encapsulates the S3 bucket logic.- The bucket name is configurable via var.bucket_name.- The ARN is exposed as an output for other modules to use.
var.bucket_name
source = "./modules/x"
module "name" { ... }
module.name.output
type
>= 3.0
terraform get
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.