Terraform Module Structure
Introduction
Terraform modules are containers for multiple resources that are used together. They help you organize and reuse your Terraform code effectively. A well-structured module improves maintainability, encourages collaboration, and simplifies complex infrastructure deployments.
In this guide, we'll explore how to structure your Terraform modules following best practices, understand the standard directory layout, and learn how to create modules that are reusable across different projects.
Module Basics
A Terraform module is simply a set of Terraform configuration files in a directory. Even your root configuration is a module. Let's understand the fundamental structure of a Terraform module.
Root Module
Every Terraform configuration has at least one module, called the root module, which consists of the .tf
and .tf.json
files in your main working directory.
my-terraform-project/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
└── terraform.tfstate
This basic structure can be expanded into a more organized layout as your project grows.
Standard Module Structure
The standard module structure is a layout recommended by HashiCorp for reusable modules. Following this structure makes your modules more consistent and easier to work with.
Let's break down each component:
Core Files
- main.tf - Contains the primary resource declarations
- variables.tf - Input variable declarations
- outputs.tf - Output value declarations
- README.md - Documentation about the module
- LICENSE - License under which the module is distributed
Example Usage
These core files form the backbone of any module structure. Here's a simple example of a module for managing an AWS S3 bucket:
main.tf:
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
acl = var.acl
tags = var.tags
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
variables.tf:
variable "bucket_name" {
description = "Name of the S3 bucket"
type = string
}
variable "acl" {
description = "The ACL policy of the bucket"
type = string
default = "private"
}
variable "tags" {
description = "A map of tags to add to all resources"
type = map(string)
default = {}
}
outputs.tf:
output "bucket_id" {
description = "The ID of the bucket"
value = aws_s3_bucket.this.id
}
output "bucket_arn" {
description = "The ARN of the bucket"
value = aws_s3_bucket.this.arn
}
Module Organization Patterns
Let's explore different organizational patterns for modules, depending on your project needs.
Simple Module
For small projects or components, a simple structure works well:
simple-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── README.md
Complex Module with Examples
For more extensive modules that need documentation and examples:
complex-module/
├── main.tf
├── variables.tf
├── outputs.tf
├── README.md
├── LICENSE
└── examples/
├── basic/
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── advanced/
├── main.tf
├── outputs.tf
└── variables.tf
Multi-Layer Module
For large infrastructure components with nested modules:
multi-layer-module/
├── main.tf
├── variables.tf
├── outputs.tf
├── README.md
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── compute/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── examples/
└── complete/
└── main.tf
Practical Example: Creating a Web Server Module
Let's create a practical module for deploying a web server on AWS. This will demonstrate how to structure a module for real-world use.
First, let's set up our directory structure:
aws-webserver-module/
├── main.tf
├── variables.tf
├── outputs.tf
├── security-groups.tf
├── userdata.tpl
└── README.md
Now let's populate each file:
main.tf:
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
subnet_id = var.subnet_id
vpc_security_group_ids = [
aws_security_group.web.id
]
user_data = templatefile("${path.module}/userdata.tpl", {
server_name = var.server_name
})
tags = merge(
{
Name = var.server_name
},
var.tags
)
}
security-groups.tf:
resource "aws_security_group" "web" {
name = "${var.server_name}-sg"
description = "Security group for web server ${var.server_name}"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow HTTP traffic"
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.ssh_allowed_cidr
description = "Allow SSH from trusted IPs"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = var.tags
}
variables.tf:
variable "ami_id" {
description = "The AMI ID to use for the web server"
type = string
}
variable "instance_type" {
description = "The instance type to use for the web server"
type = string
default = "t3.micro"
}
variable "vpc_id" {
description = "The VPC ID where the web server will be deployed"
type = string
}
variable "subnet_id" {
description = "The subnet ID where the web server will be deployed"
type = string
}
variable "server_name" {
description = "The name of the web server"
type = string
default = "web-server"
}
variable "ssh_allowed_cidr" {
description = "CIDR blocks allowed to SSH into the instance"
type = list(string)
default = ["0.0.0.0/0"]
}
variable "tags" {
description = "A map of tags to add to all resources"
type = map(string)
default = {}
}
outputs.tf:
output "instance_id" {
description = "The ID of the web server instance"
value = aws_instance.web.id
}
output "public_ip" {
description = "The public IP address of the web server"
value = aws_instance.web.public_ip
}
output "security_group_id" {
description = "The ID of the security group"
value = aws_security_group.web.id
}
userdata.tpl:
#!/bin/bash
echo "Hello from ${server_name}" > /var/www/html/index.html
service apache2 restart
Using the Module
Now that we've created our module, here's how to use it in a project:
module "web_server" {
source = "./modules/aws-webserver-module"
ami_id = "ami-0c55b159cbfafe1f0"
vpc_id = "vpc-12345678"
subnet_id = "subnet-12345678"
server_name = "production-web"
ssh_allowed_cidr = ["10.0.0.0/16"]
tags = {
Environment = "production"
Project = "website"
}
}
output "web_server_ip" {
value = module.web_server.public_ip
}
Best Practices for Module Structure
To create maintainable and reusable modules, follow these best practices:
-
Keep modules focused: Each module should do one thing and do it well.
-
Use consistent naming: Name your files and resources consistently to make your code more predictable.
-
Provide sensible defaults: Use default values for variables where appropriate but require critical inputs.
-
Document thoroughly: Include a comprehensive README.md that explains:
- Module purpose
- Example usage
- Input variable descriptions
- Output descriptions
-
Version your modules: Tag releases of your modules to provide stable versions for consumers.
-
Test your modules: Create examples that serve as both documentation and tests.
-
Use conditional creation: Allow resources to be optionally created using the
count
orfor_each
meta-arguments. -
Handle dependencies properly: Use
depends_on
when necessary for resources with implicit dependencies.
Module Composition Patterns
There are several patterns for composing modules together:
Composition Pattern
Combine multiple smaller modules to create a larger solution:
module "vpc" {
source = "./modules/vpc"
// vpc variables
}
module "webserver" {
source = "./modules/webserver"
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.public_subnet_ids[0]
// webserver variables
}
Layer Pattern
Organize modules in layers according to their lifecycle and dependency:
- Foundation Layer: VPC, subnets, routing
- Platform Layer: ECS clusters, Kubernetes, databases
- Application Layer: Applications, services
Sharing and Publishing Modules
Once you've created well-structured modules, you can share them:
Private Module Registry
For team or organization use, consider setting up a private module registry in Terraform Cloud or Terraform Enterprise.
Public Registry
For open-source modules, publish to the Terraform Registry by:
- Hosting your module on GitHub
- Following the standard module structure
- Using semantic versioning with git tags
- Including documentation and examples
Summary
Creating well-structured Terraform modules is essential for maintainable infrastructure code. The key points to remember are:
- Follow the standard module structure with main.tf, variables.tf, and outputs.tf
- Group related resources together in modules
- Document your modules thoroughly
- Use consistent naming and organization patterns
- Provide examples to demonstrate proper usage
- Consider module composition for complex infrastructure
By following these guidelines, your Terraform code will be more maintainable, easier to understand, and more reusable across different projects.
Additional Resources
Exercises
- Create a simple AWS S3 bucket module following the standard module structure.
- Extend the webserver module to include an Application Load Balancer.
- Refactor an existing Terraform configuration into a modular structure.
- Create a module that can be conditionally deployed based on input variables.
- Build a multi-layer infrastructure using the composition pattern with separate networking, compute, and application modules.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)