Well-Architected Framework
Create reusable infrastructure modules
A Terraform module is a container for multiple infrastructure resources that you manage as a single unit. Modules let you package infrastructure configurations into reusable components that teams can share across your organization. These reusable patterns reduce deployment time, enforce security standards, and remove configuration duplication. You can design modules to comply with organizational best practices and let you deploy consistent infrastructure across environments.
You should use reusable modules in the following scenarios:
- You deploy the same group of resources multiple times with minor variations
- You need to enforce organizational standards and security policies across deployments
- Multiple teams need to provision similar infrastructure
- You need to maintain consistency across development, staging, and production environments
Why create reusable modules
Creating reusable infrastructure modules addresses the following strategic operational and security challenges:
Remove deployment inconsistencies: Manual infrastructure provisioning creates configuration drift between environments. Modules enforce consistent configuration across all deployments.
Reduce security policy violations: Teams deploying infrastructure without standardized security controls increase your organization's risk exposure. Each manual deployment requires security review and validation. Modules encode security best practices once, allowing you to reuse them across deployments.
Increase developer productivity: Developers don't have to write infrastructure as code from scratch. They can reuse existing infrastructure code to provision infrastructure instead of spending time writing new code.
Create Terraform modules
A Terraform module is a set of Terraform configuration files in a single directory. Modules are reusable and customizable and you can wrap modules with configurations to fit your organization's standards. Wrapping a module means creating your own module that calls an existing module and adds organization-specific configurations on top.
Module structure
A basic Terraform module consists of three core files that define inputs, resources, and outputs. The following example shows a module that creates a web server with configurable instance type and environment tags:
modules/web-server/main.tf
# Query for latest Ubuntu AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# Create security group for web server
resource "aws_security_group" "web" {
name = "${var.environment}-web-sg"
description = "Security group for web server"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}
# Create EC2 instance
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = var.subnet_id
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "${var.environment}-web-server"
Environment = var.environment
ManagedBy = "Terraform"
}
}
modules/web-server/variables.tf
variable "instance_type" {
description = "EC2 instance type for the web server"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Environment name (development, staging, production)"
type = string
}
variable "vpc_id" {
description = "VPC ID where resources will be created"
type = string
}
variable "subnet_id" {
description = "Subnet ID where the EC2 instance will be launched"
type = string
}
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 address of the web server"
value = aws_instance.web.public_ip
}
output "security_group_id" {
description = "ID of the security group"
value = aws_security_group.web.id
}
The module accepts four input variables: instance_type for configuring instance size, environment for resource tagging, vpc_id for network placement, and subnet_id for instance placement. The module creates a security group allowing HTTP traffic, provisions an EC2 instance with the latest Ubuntu AMI, and outputs the instance ID, public IP, and security group ID for use by calling configurations.
Teams can call this module from their root configuration to deploy web servers consistently across environments:
main.tf
module "production_web" {
source = "./modules/web-server"
instance_type = "t3.medium"
environment = "production"
vpc_id = "vpc-0123456789abcdef0" # Replace with your VPC ID
subnet_id = "subnet-0123456789abcdef0" # Replace with your subnet ID
}
module "development_web" {
source = "./modules/web-server"
instance_type = "t3.micro"
environment = "development"
vpc_id = "vpc-0123456789abcdef0" # Replace with your VPC ID
subnet_id = "subnet-abcdef0123456789" # Replace with your subnet ID
}
# Use module outputs
output "production_server_ip" {
value = module.production_web.public_ip
}
The calling configuration uses the web-server module twice with different parameters, demonstrating how modules enable consistent infrastructure with environment-specific variations. Module outputs become accessible through module.<name>.<output> syntax, allowing infrastructure dependencies to flow between modules.
Organize modules in your repository
Organize modules in a dedicated directory structure within your repository:
project-root/
├── main.tf # Root configuration
├── variables.tf # Root variables
├── outputs.tf # Root outputs
└── modules/
├── web-server/ # Web server module
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── database/ # Database module
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── networking/ # Networking module
├── main.tf
├── variables.tf
└── outputs.tf
The root configuration calls modules using relative paths like source = "./modules/web-server", keeping all module code within your repository for version control and team collaboration.
Test modules before deployment
You can use Terraform's built-in testing framework that validates your configurations by creating ephemeral infrastructure. Terraform tests let you validate that module configuration updates do not introduce breaking changes. Tests run against test-specific, short-lived resources, preventing any risk to your existing infrastructure or state.
Use the Terraform Registry
The Terraform Registry provides a centralized source for providers, modules, policy libraries, and run tasks. You can find publicly available Terraform modules for configuring common infrastructure such as networking, databases, and monitoring, that are free to use. Terraform downloads these modules automatically when you specify the source and version in your configuration, making it easy to leverage community-maintained infrastructure patterns.
Your organization can create private modules. HCP Terraform and Terraform Enterprise include private module registries, which are secure, internal repositories for sharing modules within your organization without publishing them publicly.
Version your modules
Module versioning ensures stability and predictability in your infrastructure deployments. When you version your modules, you can update them without breaking existing deployments.
Use the following best practices for module versioning:
- Use semantic versioning, such as v1.2.3. Semantic versioning uses three numbers: major version for breaking changes, minor version for new features, and patch version for bug fixes
- Tag releases in your version control system to mark stable versions
- Pin module versions in production environments to prevent unexpected updates
Specify module versions
When calling modules from a registry, you can use version constraints to specify the version to ensure consistent deployments. The following shows two examples of version constraints in a module block. The first example pins the module to an exact version, while the second example uses a pessimistic constraint to allow patch updates only.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
}
This configuration pins the VPC module to version 5.0.0 exactly. Terraform always downloads this specific version from the registry, ensuring consistent infrastructure across all deployments.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0.0"
# Allow patch updates only.
name = "my-vpc"
cidr = "10.0.0.0/16"
}
The pessimistic constraint operator ~> allows Terraform to automatically download compatible updates. The version ~> 5.0.0 permits updates to 5.0.1, 5.0.2, and so on, but blocks updates to 5.1.0 or 6.0.0.
HashiCorp resources
- Define infrastructure as code to understand IaC principles before creating modules
- Map your current workflows to identify repeated patterns for modules
- Store code in version control to track and version modules
Learn about Terraform modules:
- Get started with Terraform tutorials and read the Terraform documentation
- Follow the Terraform style guide for naming and formatting conventions
- Learn about module development for structure, inputs, and outputs and the module tutorials to build and publish modules
- Browse the Terraform Registry for verified and community-maintained modules
- Learn about Sentinel policies for mandatory module requirements
- Use the HCP Terraform private registry for internal module distribution
External resources:
- Learn about semantic versioning to version your modules effectively.
Next steps
In this section of Define your processes, you learned about using Terraform modules to standardize your infrastructure deployments. This topic is part of the Define and automate processes pillar.
Refer to the following documents to learn more about infrastructure automation:
- Implement CI/CD to automate your standardized workflows
- Automate your infrastructure and application testing
- Package your applications with Packer