Fatskills
Practice. Master. Repeat.
Study Guide: TECH **Terraform Modules: Local Paths vs. Registry – Zero-Fluff Study Guide**
Source: https://www.fatskills.com/cloud-application-developer/chapter/tech-terraform-modules-local-paths-vs-registry-zero-fluff-study-guide

TECH **Terraform Modules: Local Paths vs. Registry – Zero-Fluff Study Guide**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~10 min read

Terraform Modules: Local Paths vs. Registry – Zero-Fluff Study Guide

(For engineers who need to ship, not just pass exams)


1. What This Is & Why It Matters

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:


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.

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.


2. Core Concepts & Components


1. Module

  • Definition: A self-contained Terraform configuration that packages resources (e.g., an entire VPC, a Lambda function, or a Kubernetes cluster).
  • Production insight: Modules should be stateless—they shouldn’t rely on external variables unless explicitly passed in. If your module reads aws_ssm_parameter directly, it’s not reusable.

2. source Argument

  • Definition: Tells Terraform where to find the module (local path, GitHub, Terraform Registry, etc.).
  • Production insight: Always pin versions (e.g., source = "terraform-aws-modules/vpc/aws" version = "3.14.0"). Without versioning, terraform init might pull breaking changes.

3. Local Module

  • Definition: A module stored in a subdirectory of your Terraform project (e.g., ./modules/vpc).
  • Production insight: Use local modules for company-specific infrastructure (e.g., a custom IAM role for your CI/CD pipeline). Avoid them for generic resources (e.g., a standard VPC)—use the registry instead.

4. Public Registry Module

  • Definition: A module published to the Terraform Registry (e.g., terraform-aws-modules/vpc/aws).
  • Production insight: Registry modules are battle-tested (e.g., the AWS VPC module has 100M+ downloads). But they may not fit your exact needs—fork them if you need customizations.

5. Private Registry Module

  • Definition: A module hosted in a private registry (e.g., Terraform Cloud, GitHub Packages, or a private Git repo).
  • Production insight: Use private registries for proprietary modules (e.g., a custom database setup for your SaaS product). Requires authentication (e.g., terraform login).

6. Module Inputs (variables.tf)

  • Definition: Parameters passed into a module (e.g., instance_type, vpc_id).
  • Production insight: Always validate inputs (e.g., validation { condition = var.instance_type != "t2.micro" }). Unvalidated inputs lead to runtime failures.

7. Module Outputs (outputs.tf)

  • Definition: Values returned by a module (e.g., vpc_id, security_group_id).
  • Production insight: Outputs are contracts. If you change an output, downstream modules break. Version your modules when outputs change.

8. Module Versioning

  • Definition: Tagging modules with semantic versions (e.g., v1.0.0).
  • Production insight: Never use latest in production. Pin versions to avoid breaking changes (e.g., version = "~> 3.0" for patch updates only).

9. Module Composition

  • Definition: Nesting modules inside other modules (e.g., a network module that calls a vpc module and a security-group module).
  • Production insight: Deeply nested modules are hard to debug. Limit nesting to 2-3 levels max.

10. terraform init with Modules

  • Definition: Downloads modules and providers when you run terraform init.
  • Production insight: If terraform init fails, check:
  • Network connectivity (for registry modules).
  • Git permissions (for private modules).
  • Version constraints (e.g., version = ">= 2.0.0").


3. Step-by-Step: Hands-On Module Deployment


Prerequisites

  • Terraform installed (>= 1.0.0).
  • AWS CLI configured (aws configure).
  • A GitHub account (for private modules).

Task: Deploy a VPC Using a Registry Module

We’ll use the official AWS VPC module from the Terraform Registry.


Step 1: Create a Terraform Project

mkdir tf-vpc-module && cd tf-vpc-module
touch main.tf

Step 2: Call the VPC Module

Paste this into 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 }

Step 3: Initialize and Apply

terraform init  # Downloads the module
terraform plan  # Verify the changes
terraform apply -auto-approve

Step 4: Verify the VPC

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"
} ] }

Step 5: Clean Up

terraform destroy -auto-approve


Task: Create and Use a Local Module

Now, let’s refactor a hardcoded EC2 instance into a local module.


Step 1: Create a Module Directory

mkdir -p modules/ec2-web-server
touch modules/ec2-web-server/{main.tf,variables.tf,outputs.tf}

Step 2: Define the Module

Paste this into 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:


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:


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
}

Step 3: Call the Local Module

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
}

Step 4: Apply the Changes

terraform init  # Re-initializes to detect the new module
terraform apply -auto-approve

Step 5: Verify the Instance

aws ec2 describe-instances --filters "Name=tag:Name,Values=my-web-server"

Expected output:


{
  "Reservations": [
{
"Instances": [
{
"InstanceId": "i-1234567890abcdef0",
"PublicIpAddress": "54.165.123.45"
}
]
} ] }

Step 6: Clean Up

terraform destroy -auto-approve


4. ? Production-Ready Best Practices


Security

  • Never hardcode secrets in modules. Use aws_ssm_parameter or vault_generic_secret.
  • Least privilege: Modules should only create resources they need. Avoid aws_iam_policy with * permissions.
  • Private modules: Use Terraform Cloud or GitHub Packages for proprietary modules. Never commit them to public repos.

Cost Optimization

  • Default to spot instances in non-production modules (e.g., spot_price = "0.02").
  • Enable auto-scaling in modules that create ASGs.
  • Use count or for_each to avoid creating unused resources (e.g., count = var.create_nat_gateway ? 1 : 0).

Reliability & Maintainability

  • Version everything. Use version = "~> 3.0" (allows patch updates only).
  • Tag all resources. Add a tags input variable to every module.
  • Document inputs/outputs. Use description in variables.tf and outputs.tf.
  • Test modules. Use Terratest or Terraform Cloud to validate modules before merging.

Observability

  • Output key metrics (e.g., vpc_id, security_group_id) so other modules can reference them.
  • Add CloudWatch alarms in modules that create critical resources (e.g., RDS, EKS).
  • Log module usage. Use terraform state list to track which modules are deployed.


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Not pinning module versions terraform apply breaks after terraform init pulls a new version. Always use version = "x.y.z" in source.
Hardcoding values in modules Module can’t be reused (e.g., hardcoded us-east-1). Use variables.tf for all configurable values.
Circular dependencies terraform plan fails with Error: Cycle detected. Avoid referencing outputs from one module in another if they depend on each other.
Not validating inputs terraform apply fails with InvalidParameterValue. Add validation blocks in variables.tf.
Over-nesting modules Debugging becomes impossible (e.g., module.a.module.b.module.c). Limit nesting to 2-3 levels. Use flat module composition instead.


6. ? Exam/Certification Focus


Typical Question Patterns

  1. "Which source syntax is correct?"
  2. source = "github.com/terraform-aws-modules/vpc"
  3. source = "terraform-aws-modules/vpc/aws" version = "3.14.0"

  4. "How do you reference a module’s output?"

  5. module.vpc.vpc_id
  6. aws_vpc.vpc.id (wrong resource type)

  7. "What happens if you don’t pin a module version?"

  8. terraform init may pull breaking changes.
  9. ❌ The module won’t work at all.

Key ⚠️ Trap Distinctions

  • Local vs. Registry Modules:
  • Local: Good for custom infrastructure (e.g., company-specific IAM roles).
  • Registry: Good for standard infrastructure (e.g., VPCs, EKS clusters).
  • Version Constraints:
  • version = "3.14.0"Exact version (safest).
  • version = "~> 3.14"Patch updates only (e.g., 3.14.1 but not 3.15.0).
  • version = ">= 3.0"Dangerous (allows major version updates).

Scenario-Based Question

"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).


7. ? Hands-On Challenge


Challenge: Refactor a Hardcoded S3 Bucket into a Module

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.

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.


8. ? Rapid-Reference Crib Sheet

Command/Concept Usage Notes
terraform init Downloads modules and providers. Run this first after adding a module.
source = "./modules/x" Local module path. Use for custom infrastructure.
source = "terraform-aws-modules/vpc/aws" version = "3.14.0" Registry module. Always pin versions.
module "name" { ... } Calls a module. Reference outputs with module.name.output.
variables.tf Defines module inputs. Always add description and type.
outputs.tf Defines module outputs. Useful for chaining modules.
version = "~> 3.0" Allows patch updates only. Safer than >= 3.0.
terraform state list Lists all resources/modules. Useful for debugging.
terraform get Updates modules (rarely needed). Usually terraform init is enough.
⚠️ Default module behavior Modules do not inherit variables from the root module. Pass variables explicitly.
⚠️ Module outputs Must be explicitly defined in outputs.tf. Terraform won’t auto-detect them.


9. ? Where to Go Next

  1. Terraform Module Registry – Browse official modules.
  2. Terraform Module Best Practices – How to structure modules.
  3. Terratest – Test your modules.
  4. Terraform Cloud Private Registry – Host private modules.


ADVERTISEMENT