Table of Contents
- Overview
- Attribute Access Examples
- Module Structure and Conventions
- Calling and Composing Modules
- Common Module Patterns
- Initializing and Applying Modules
- Module Example: Usage Pattern
- Conclusion
Terraform Modules: Overview
What is a Terraform Module?
A Terraform module is a collection of reusable Terraform configuration files grouped in a directory. Each module encapsulates a set of resources that together perform a discrete task—such as creating a network, deploying servers, or managing security policies. At a minimum, a module includes one or more .tf
files that define infrastructure resources and logic.
- The root module is your main working directory, containing your primary configuration.
- Child modules are directories within your repository (or externally sourced) that are referenced by the root module or by other modules.
- Publicly shared modules can also be found on the Terraform Registry or private repositories.
Why You Need to Know About Terraform Modules
- Reusability: Modules allow you to define common infrastructure patterns once and reuse them across environments or projects.
- Maintainability: By encapsulating resource logic and configurations, modules make it easy to update or fix infrastructure components in one place.
- Consistency: Standardizing infrastructure with modules reduces errors and enforces best practices across teams.
- Collaboration: Modules simplify onboarding, code reviews, and knowledge sharing among teams by offering clear, documented interfaces.
How Terraform Modules Work
Concept
Modules behave like functions in programming: they take input parameters, perform infrastructure provisioning (using resources, data sources, etc.), and return useful outputs.
- Definition: You create a module by grouping
.tf
files within a directory. - Input Variables: Modules accept variables you define in a
variables.tf
file, making the module flexible. - Outputs: Modules expose outputs using
outputs.tf
, allowing information from resources to be shared with the calling code. - Calling a Module: In your root configuration or another module, you use the
module
block, specify the source and pass required variables. - Composition: You can call multiple modules and pass data between them, building complex infrastructures from simple building blocks.
Example Workflow
- Define a VPC module that provisions a VPC, subnets, and route tables.
- In your root configuration, reference this VPC module, providing CIDR blocks and names as inputs.
- Use outputs from the VPC module (such as subnet IDs) as inputs to other modules (like EC2 or database modules).
- Maintain and version the module independently, applying changes as your organization’s needs evolve.
In Summary
Terraform modules are foundational for scaling, automating, and standardizing infrastructure deployment. They enable teams to build complex and reliable cloud environments efficiently, while ensuring best practices and reducing redundant code. Whether managing networks, compute, security, or multi-cloud architectures, modules are the “Lego bricks” for your infrastructure-as-code journey.
Attribute Access Examples
This section walks through common ways to access and reference values from Terraform resources, data sources, modules, variables, and locals. These patterns are fundamental for building maintainable and modular Terraform configurations.
-
Accessing Resource Attributes
resource "aws_s3_bucket" "my_bucket" { bucket = "example-bucket" } # Reference the bucket name output "bucket_name" { value = aws_s3_bucket.my_bucket.bucket }
Use
resource_type.resource_name.attribute
to reference a specific resource’s attribute. -
Using Data Source Attributes
data "aws_ami" "selected" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["amzn2-ami-hvm-*-x86_64-gp2"] } } # Reference the AMI ID output "ami_id" { value = data.aws_ami.selected.id }
Data sources are referenced as
data.resource_type.resource_name.attribute
. -
Accessing Module Outputs
module "vpc" { source = "terraform-aws-modules/vpc/aws" # other arguments } # Get a value exported by the module output "vpc_id" { value = module.vpc.vpc_id }
Use
module.module_name.output_variable
for reusable module outputs. -
Referencing Variables
variable "environment" { default = "dev" } # Use the variable output "env" { value = var.environment }
Access input variables using
var.variable_name
. -
Using Local Values
locals { region = "us-west-2" } # Use a local value output "tf_region" { value = local.region }
Access local values via
local.local_name
.
Module Structure and Conventions
Structuring your Terraform modules in a logical and consistent way makes them easier to maintain, reuse, and understand. Follow these steps to set up a reliable module directory structure:
-
Create a dedicated directory for your module.
Place all files related to the module inside a uniquely named folder. For example:
modules/network/
. -
Add the essential Terraform configuration files:
- main.tf: Contains the main logic and resource definitions.
- variables.tf: Declares all input variables accepted by the module.
- outputs.tf: Specifies the output values that the module will return to the calling root configuration.
- README.md: Documents the module’s purpose, inputs, outputs, and examples of use.
modules/ └── network/ ├── main.tf ├── variables.tf ├── outputs.tf └── README.md
-
Maintain consistency in naming conventions:
-
Use
snake_case
for all resource and variable names. -
Names should clearly describe the purpose or function (e.g.,
primary_subnet_id
).
-
Use
-
Organize for reusability and scalability:
-
For complex modules, you can add a
modules/
subfolder for nested modules and anexamples/
directory to showcase usage patterns.
modules/ └── network/ ├── main.tf ├── variables.tf ├── outputs.tf ├── README.md ├── modules/ │ └── subnet/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── examples/ └── simple/ └── main.tf
-
For complex modules, you can add a
-
Include documentation in each module:
Every module, including any nested ones, should have a concise
README.md
describing its inputs, outputs, and usage. -
Example: Minimal Module Structure
my-module/ ├── main.tf ├── variables.tf ├── outputs.tf └── README.md
-
Example: Complete Module Structure
my-module/ ├── main.tf ├── variables.tf ├── outputs.tf ├── providers.tf ├── README.md ├── modules/ │ └── ... ├── examples/ │ └── ... └── test/ └── ...
Following these conventions ensures that your Terraform modules are well-organized, maintainable, and easy to consume or extend in any infrastructure project.
Calling and Composing Modules
Composing infrastructure with multiple Terraform modules makes your configurations modular, maintainable, and reusable. The following steps demonstrate how to call modules, pass outputs between them, and use best practices for module composition.
-
Call a Module from the Root Configuration
Use the
module
block in your rootmain.tf
to bring in a module and supply its required inputs:module "network" { source = "./modules/network" base_cidr_block = "10.0.0.0/16" }
-
Define Outputs in the Source Module
In the module directory (for example,
modules/network/outputs.tf
), specify outputs that can be used by other modules:output "vpc_id" { value = aws_vpc.main.id } output "subnet_ids" { value = aws_subnet.main.*.id }
-
Pass Output from One Module as Input to Another
In your root configuration, use outputs from one module as the input for another. This allows different modules to be composed together:
module "compute" { source = "./modules/compute" subnet_ids = module.network.subnet_ids }
-
Chain Multiple Modules for Complex Environments
Build layered infrastructure by chaining outputs and inputs, creating links between modules:
module "database" { source = "./modules/database" subnet_id = module.network.subnet_ids[0] } module "app" { source = "./modules/app" subnet_id = module.network.subnet_ids[0] db_id = module.database.db_id }
-
Best Practices When Composing Modules
- Keep module composition as flat as possible—call multiple modules from the root module instead of deeply nesting them.
- Use output variables only for values that should be shared.
- Avoid tightly coupling modules; pass only the needed outputs as explicit inputs.
- Document input/output usage in each module’s
README.md
file for clarity.
-
Complete Example: Composing Modules
# main.tf in root module module "network" { source = "./modules/network" base_cidr_block = "10.0.0.0/16" } module "app" { source = "./modules/app" vpc_id = module.network.vpc_id subnet_ids = module.network.subnet_ids } output "app_url" { value = module.app.app_url }
By composing modules this way, you separate responsibilities, enable reuse, and allow for scalable infrastructure deployments as your requirements grow.
Common Module Patterns
Recognizing and using common module patterns can accelerate infrastructure builds and ensure your Terraform configurations are scalable, modular, and easy to maintain. Here are the most widely used patterns:
-
Root Module Pattern
The root module is the entry point for your Terraform configuration. It typically focuses on orchestrating and composing multiple child modules, supplying them with required inputs and coordinating outputs.
# main.tf in the root module module "network" { source = "./modules/network" cidr = "10.0.0.0/16" } module "compute" { source = "./modules/compute" subnet_ids = module.network.subnet_ids }
-
Reusable Child Module Pattern
A child module encapsulates a set of related resources (such as VPC, EC2, Database) within a folder and exposes only the necessary inputs/outputs. Child modules can be reused across different projects.
# modules/network/variables.tf variable "cidr" { description = "The network range" type = string } # modules/network/main.tf resource "aws_vpc" "main" { cidr_block = var.cidr } # modules/network/outputs.tf output "vpc_id" { value = aws_vpc.main.id }
-
External Module Pattern
Reuse well-tested community or organizational modules directly from the Terraform Registry or source control. This reduces boilerplate and speeds up deployment.
module "eks" { source = "terraform-aws-modules/eks/aws" version = "20.4.0" cluster_name = "my-cluster" subnets = ["subnet-1", "subnet-2"] # ...other parameters }
-
Nested Module Pattern
Group complex or related child modules under a parent module directory. Nesting allows teams to break down responsibilities and helps with organizing large codebases.
modules/ └── app/ ├── main.tf ├── variables.tf ├── outputs.tf └── modules/ ├── backend/ └── frontend/
-
Wrapper Module Pattern
Use a wrapper module to enforce organizational policies, standardize inputs, or combine several modules into a higher-level abstraction.
# modules/wrapper/main.tf module "standard_network" { source = "../network" cidr = var.cidr tags = merge(var.tags, { Environment = var.env }) }
Select the pattern that best fits your scenario, keeping your modules clean, composable, and easy for others to understand and reuse.
Pattern Name | When to Use | Example Directory |
---|---|---|
Root Module | Orchestrate and compose environment; top-level configuration | main.tf at project root |
Reusable Child Module | Encapsulate and reuse related resources | modules/network/ |
External Module | Consume public or shared modules for common infra | terraform-aws-modules/vpc/aws |
Nested Module | Decompose complex infra into submodules | modules/app/modules/backend/ |
Wrapper Module | Apply standardization or composition across modules | modules/wrapper/ |
Initializing and Applying Modules
Before Terraform can provision your infrastructure using modules, you must properly initialize your working directory and then apply your configuration. Follow these step-by-step instructions for a smooth workflow:
-
Navigate to Your Project Directory
Open your terminal and change to the directory containing your Terraform root configuration (where your
main.tf
and module blocks reside).cd /path/to/your/terraform/project
-
Initialize the Directory
Run the initialization command to set up the backend, install necessary provider plugins, and download any modules referenced in your configuration.
terraform init
This command prepares your environment by:
- Configuring the backend for storing state.
- Downloading and installing required provider plugins.
- Fetching all referenced modules and locking version dependencies.
-
Create an Execution Plan (Optional but Recommended)
Generate a plan to preview the changes Terraform intends to make. This helps you verify the expected actions before making any changes.
terraform plan
Review the output to ensure the proposed changes align with your intentions.
-
Apply the Configuration
Use the apply command to execute the planned actions and provision or update your infrastructure according to your modules and configuration files.
terraform apply
Terraform will prompt you for approval before making changes. If you want to auto-approve (for automation or scripts), use:
terraform apply -auto-approve
-
Verify Outputs
Once apply completes, Terraform displays output values you defined in your root configuration. These often include useful values such as resource IDs, URLs, or connection info exported from modules.
Outputs: vpc_id = "vpc-01234567" app_url = "https://myapp.example.com"
-
Managing Changes
Whenever you update module sources, variables, or provider requirements, rerun
terraform init
before applying changes to ensure your environment is in sync.
By initializing and applying your Terraform modules in this order, you guarantee that all dependencies are installed and that your infrastructure state is managed reliably as you scale or evolve your configurations.
Module Example: Usage Pattern
Applying modules in Terraform follows a clear workflow—from module definition to integration in your root configuration. This step-by-step example demonstrates how to create a reusable module and use it in a project.
-
Define Your Module
Create a folder (e.g.,
modules/vpc/
) and add the necessary files.modules/ └── vpc/ ├── main.tf ├── variables.tf └── outputs.tf
-
main.tf: Defines the resource.
resource "aws_vpc" "main" { cidr_block = var.cidr_block tags = { Name = var.name } }
-
variables.tf: Declares input variables.
variable "cidr_block" { description = "The CIDR block for the VPC." } variable "name" { description = "The name of the VPC." }
-
outputs.tf: Exposes important outputs.
output "vpc_id" { value = aws_vpc.main.id }
-
main.tf: Defines the resource.
-
Use the Module in Your Root Configuration
Reference the module in your root module (e.g.,
main.tf
) and provide required variables:module "my_vpc" { source = "./modules/vpc" cidr_block = "10.0.0.0/16" name = "example-vpc" }
-
Output Module Values
To make use of values produced by the module, define outputs in your root configuration:
output "vpc_id" { value = module.my_vpc.vpc_id }
-
Initialize and Apply
Run Terraform commands from your project root directory:
terraform init terraform apply
On successful execution, Terraform will display output values such as your newly created VPC ID.
-
Result
You have now modularized your infrastructure, allowing you to reuse and share standard patterns across environments and projects with minimal code changes.
This approach streamlines infrastructure deployment and encourages best practices for maintainability and scalability in Terraform projects.
Conclusion
Throughout this blog post, we've taken a guided journey through the core concepts and practical patterns of using Terraform modules. Whether you're building a single reusable component or composing a complex infrastructure from many pieces, modules help you scale infrastructure-as-code efficiently and consistently.
🔑 Key Takeaways:
- Terraform Modules bring structure, organization, and reusability to your infrastructure code. They promote consistency and reduce duplication by enabling you to encapsulate logic into reusable components.
- Attribute Access is essential when stitching together configurations. Understanding how to reference variables, outputs, and data sources allows modules to interact seamlessly.
- A well-defined module structure—with clear
main.tf
,variables.tf
,outputs.tf
, and documentation—ensures clarity and maintainability. - Use module composition and chaining to build advanced environments by passing outputs from one module as inputs to the next.
- Recognize and apply common module patterns like root modules, child modules, nested modules, and wrapper modules to fit different use cases and team structures.
- Properly initialize and apply modules using Terraform CLI commands like
terraform init
,terraform plan
, andterraform apply
to create reliable and repeatable deployments. - With a practice-focused usage example, you now have the foundation to build and consume modules effectively in your own projects.
Thanks for following along! Terraform modules empower teams to move faster with confidence. Whether you're managing a simple VPC or orchestrating a multi-tier application stack, modules help you stay DRY, scalable, and secure.
Feel free to clone, customize, and reuse the examples shared here—and keep building awesome infrastructure the modular way 🚀💻
Happy Terraforming! 🌍⚙️