Modules¶
Overview¶
This document covers Terraform modules including creation, usage, versioning, the module registry, and composition patterns. Modules account for 10% of the exam (Domain 5) and are fundamental to building reusable, maintainable infrastructure configurations.
π Modules Overview - Module concepts and usage
Module Concepts¶
What is a Module?¶
- A container for multiple resources that are used together
- Every Terraform configuration is a module (the root module)
- Modules can call other modules (child modules)
- Enables code reuse, organization, and encapsulation
- Can be sourced from local directories, registries, or version control
Root Module vs Child Module¶
- Root module: The main working directory where you run
terraformcommands - Child module: A module called from within another module using a
moduleblock - The root module can call multiple child modules
- Child modules can call their own child modules (nesting)
Why Use Modules?¶
- Reusability: Write once, use across multiple projects
- Organization: Group related resources logically
- Encapsulation: Hide implementation details
- Consistency: Enforce standards across teams
- Versioning: Track and control module changes
Module Structure¶
Standard Module Layout¶
modules/
my-module/
main.tf # Primary resource definitions
variables.tf # Input variable declarations
outputs.tf # Output value declarations
README.md # Documentation
versions.tf # Provider version constraints (optional)
locals.tf # Local values (optional)
data.tf # Data sources (optional)
Minimal Module Example¶
# modules/web-server/variables.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "ami_id" {
description = "AMI ID for the instance"
type = string
}
variable "name" {
description = "Name tag for the instance"
type = string
}
# modules/web-server/main.tf
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
tags = {
Name = var.name
}
}
# modules/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
}
π Module Development - Creating modules
Module Sources¶
Local Paths¶
module "web" {
source = "./modules/web-server"
# ...
}
./ or ../ prefix - No version constraint (always uses current files) - Changes take effect immediately without terraform init Terraform Registry¶
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
# ...
}
<NAMESPACE>/<NAME>/<PROVIDER> - Version constraint is required for registry modules (best practice) - Downloaded during terraform init - Public registry: registry.terraform.io π Terraform Registry - Browse public modules
GitHub¶
module "vpc" {
source = "github.com/hashicorp/example"
# ...
}
# With specific ref (branch, tag, or commit)
module "vpc" {
source = "github.com/hashicorp/example?ref=v1.0.0"
# ...
}
Generic Git Repository¶
module "vpc" {
source = "git::https://example.com/module.git?ref=v1.0.0"
# ...
}
# SSH
module "vpc" {
source = "git::ssh://git@example.com/module.git"
# ...
}
S3 Bucket¶
module "vpc" {
source = "s3::https://s3-eu-west-1.amazonaws.com/bucket/module.zip"
# ...
}
GCS Bucket¶
module "vpc" {
source = "gcs::https://www.googleapis.com/storage/v1/bucket/module.zip"
# ...
}
π Module Sources - All supported source types
Module Inputs and Outputs¶
Passing Inputs to Modules¶
module "web" {
source = "./modules/web-server"
instance_type = "t3.small"
ami_id = "ami-12345678"
name = "production-web"
}
- Module inputs correspond to
variableblocks in the child module - Required variables must be provided by the calling module
- Default values are used when inputs are not specified
Accessing Module Outputs¶
# In root module, reference child module outputs
resource "aws_eip" "web" {
instance = module.web.instance_id
}
output "web_ip" {
value = module.web.public_ip
}
- Outputs from child modules are accessed via
module.<NAME>.<OUTPUT> - Only declared outputs are accessible (encapsulation)
- Outputs serve as the module's public interface
Variable Scope¶
- Variables declared in a module are scoped to that module
- Child modules cannot access parent module variables directly
- Values must be explicitly passed through module arguments
- Outputs must be explicitly declared to be accessible from outside
π Module Inputs - Passing values to and from modules
Module Versioning¶
Version Constraints¶
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0" # Allows 5.x, not 6.0
}
module "rds" {
source = "terraform-aws-modules/rds/aws"
version = ">= 5.0, < 6.0" # Explicit range
}
module "ec2" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "= 5.2.1" # Exact version
}
- Version constraints only work with registry modules
- Local modules do not support version constraints
- Git sources use
?ref=for version control - Always pin versions in production environments
Version Update Process¶
- Update version constraint in module block
- Run
terraform init -upgradeto download new version - Run
terraform planto review changes - Apply if changes are acceptable
Module Registry¶
Public Registry¶
- URL:
registry.terraform.io - Thousands of community and verified modules
- Verified modules are maintained by HashiCorp partners
- Each module has documentation, inputs, outputs, and examples
Private Registry¶
- Available in Terraform Cloud and Enterprise
- Host organization-specific modules
- Version control and access management
- Same source format as public registry
π Private Registry - Terraform Cloud private registry
Publishing to Public Registry¶
- Module must be in a public GitHub repository
- Repository name format:
terraform-<PROVIDER>-<NAME> - Must have standard module structure (main.tf, variables.tf, outputs.tf)
- Must use semantic versioning via Git tags
- Must have a README.md
π Publishing Modules - Publishing to the registry
Module Composition¶
Flat Modules¶
# Single level of modules
module "vpc" { source = "./modules/vpc" }
module "web" { source = "./modules/web-server" }
module "db" { source = "./modules/database" }
Nested Modules¶
# modules/app-stack/main.tf calls sub-modules
module "network" { source = "./modules/network" }
module "compute" { source = "./modules/compute" }
module "storage" { source = "./modules/storage" }
Passing Providers to Modules¶
module "web_west" {
source = "./modules/web-server"
providers = {
aws = aws.west
}
}
- Required when module needs a non-default provider
- Maps provider configuration from parent to child module
π Module Composition - Design patterns for modules
Module Best Practices¶
Design Principles¶
- Single responsibility: Each module should do one thing well
- Sensible defaults: Minimize required inputs with good defaults
- Documentation: Document all variables and outputs
- Validation: Use
validationblocks on input variables - Examples: Include example configurations
Anti-Patterns¶
- Wrapping a single resource in a module (unnecessary complexity)
- Deep nesting of modules (hard to debug and maintain)
- Hardcoding values inside modules (reduces reusability)
- Not pinning module versions (leads to unexpected changes)
- Exposing too many internal details through outputs
Module Testing¶
- Use
terraform validateto check syntax - Use
terraform planto verify expected behavior - Consider Terratest or Terraform's built-in testing framework
- Test with multiple input combinations
Key Exam Points¶
Must-Know Topics¶
- Module source formats: local, registry, GitHub, S3, GCS
- Version constraints work only with registry modules
- Variable scope - child modules cannot access parent variables directly
- Outputs are the public interface of a module
terraform initdownloads modules from remote sourcesterraform init -upgradeupdates to newer module versions- Module calls use
module.<NAME>.<OUTPUT>for references - The root module is always the working directory
Common Exam Questions¶
- What is the correct source format for a Terraform Registry module?
- How do you access outputs from a child module?
- What happens when you change a module source and run terraform init?
- How are providers passed to child modules?
- What is the difference between local and registry module sources?