Skip to main content

Modules

Modules are containers for multiple resources that are used together. They enable you to organize, encapsulate, and reuse Terraform configurations.

What is a Module?

A module is a collection of .tf files in a directory. Every Terraform configuration has at least one module, called the root module, which consists of the resources defined in the .tf files in the main working directory.

Module Structure

module/
├── main.tf          # Primary resources
├── variables.tf     # Input variable declarations
├── outputs.tf       # Output value declarations
└── README.md        # Documentation

Calling Modules

Use module blocks to call child modules:
module "<NAME>" {
  source = "<SOURCE>"
  
  # Input variables
  <VAR_NAME> = <VALUE>
}

Basic Example

module "web_app" {
  source = "./modules/web-app"
  
  instance_type = "t2.micro"
  instance_name = "WebServer"
}

Module Sources

The source argument specifies where the module code is located.

Local Paths

module "network" {
  source = "./modules/network"
}

module "compute" {
  source = "../shared-modules/compute"
}
Local paths must begin with ./ or ../ to distinguish them from module registry addresses.

Terraform Registry

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"
  
  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

Git Repositories

module "vpc" {
  source = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git"
}

# Specific branch
module "vpc" {
  source = "git::https://github.com/example/repo.git?ref=v1.2.0"
}

# Specific subdirectory
module "vpc" {
  source = "git::https://github.com/example/repo.git//modules/vpc"
}

HTTP URLs

module "vpc" {
  source = "https://example.com/vpc-module.zip"
}

S3 Buckets

module "vpc" {
  source = "s3::https://s3-eu-west-1.amazonaws.com/examplebucket/vpc.zip"
}

Module Arguments

source
string
required
Location of the module source code.
source = "./modules/web-app"
version
string
Version constraint for registry modules.
version = "~> 2.0"
count
number
Create multiple instances of the module.
count = 3
for_each
map or set
Create instances from a map or set.
for_each = var.environments
depends_on
list
Explicit dependencies.
depends_on = [aws_vpc.main]
providers
map
Pass provider configurations to the module.
providers = {
  aws = aws.west
}

Module Inputs

Child Module Variables

# modules/web-app/variables.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

variable "instance_name" {
  description = "Name tag for the instance"
  type        = string
}

Passing Values

module "web_app" {
  source = "./modules/web-app"
  
  instance_type = "t2.large"
  instance_name = "Production-Web"
}

Module Outputs

Declaring Outputs

# modules/web-app/outputs.tf
output "instance_id" {
  value       = aws_instance.app.id
  description = "ID of the EC2 instance"
}

output "public_ip" {
  value = aws_instance.app.public_ip
}

Using Module Outputs

module "web_app" {
  source = "./modules/web-app"
}

resource "aws_route53_record" "web" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "web.example.com"
  type    = "A"
  ttl     = 300
  records = [module.web_app.public_ip]
}

output "web_instance_id" {
  value = module.web_app.instance_id
}

Module Count and For Each

Using Count

module "web_app" {
  source = "./modules/web-app"
  count  = 3
  
  instance_name = "web-${count.index}"
}

# Reference specific instance
output "first_instance" {
  value = module.web_app[0].instance_id
}

# Reference all instances
output "all_instances" {
  value = module.web_app[*].instance_id
}

Using For Each

module "web_app" {
  source   = "./modules/web-app"
  for_each = var.environments
  
  instance_type = each.value.instance_type
  instance_name = "${each.key}-web"
}

# Reference specific instance
output "prod_instance" {
  value = module.web_app["production"].instance_id
}

Passing Providers

Explicitly pass provider configurations to modules:
provider "aws" {
  region = "us-west-2"
}

provider "aws" {
  alias  = "east"
  region = "us-east-1"
}

module "web_app_west" {
  source = "./modules/web-app"
  
  providers = {
    aws = aws
  }
}

module "web_app_east" {
  source = "./modules/web-app"
  
  providers = {
    aws = aws.east
  }
}

Module Best Practices

1. Use Variables for Flexibility

# modules/web-app/variables.tf
variable "environment" {
  type = string
}

variable "instance_type" {
  type = string
}

variable "tags" {
  type    = map(string)
  default = {}
}

2. Document with Descriptions

variable "vpc_cidr" {
  description = "CIDR block for the VPC. Must be a valid IPv4 CIDR."
  type        = string
  
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be a valid IPv4 CIDR block."
  }
}

3. Expose Useful Outputs

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "subnet_ids" {
  description = "List of subnet IDs"
  value       = aws_subnet.private[*].id
}

4. Use Semantic Versioning

For published modules:
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"  # Accept patch and minor updates
}

Module Composition

Build complex infrastructure by composing modules:
module "network" {
  source = "./modules/network"
  
  vpc_cidr = "10.0.0.0/16"
}

module "database" {
  source = "./modules/database"
  
  vpc_id         = module.network.vpc_id
  subnet_ids     = module.network.private_subnet_ids
  security_group = module.network.database_security_group_id
}

module "application" {
  source = "./modules/application"
  
  vpc_id         = module.network.vpc_id
  subnet_ids     = module.network.public_subnet_ids
  database_endpoint = module.database.endpoint
}

Example Module

Module Definition

# modules/web-app/main.tf
resource "aws_instance" "app" {
  ami           = var.ami_id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id
  
  tags = merge(
    var.tags,
    {
      Name = var.instance_name
    }
  )
}

resource "aws_security_group" "app" {
  name_prefix = "${var.instance_name}-"
  vpc_id      = var.vpc_id
  
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
# modules/web-app/variables.tf
variable "ami_id" {
  description = "AMI ID for the instance"
  type        = string
}

variable "instance_type" {
  description = "Instance type"
  type        = string
  default     = "t2.micro"
}

variable "instance_name" {
  description = "Name for the instance"
  type        = string
}

variable "vpc_id" {
  description = "VPC ID"
  type        = string
}

variable "subnet_id" {
  description = "Subnet ID"
  type        = string
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}
# modules/web-app/outputs.tf
output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.app.id
}

output "public_ip" {
  description = "Public IP of the instance"
  value       = aws_instance.app.public_ip
}

output "security_group_id" {
  description = "ID of the security group"
  value       = aws_security_group.app.id
}

Module Usage

module "web_app" {
  source = "./modules/web-app"
  
  ami_id        = "ami-12345678"
  instance_type = "t2.large"
  instance_name = "production-web"
  vpc_id        = aws_vpc.main.id
  subnet_id     = aws_subnet.public.id
  
  tags = {
    Environment = "production"
    Team        = "platform"
  }
}

Module Example from Source

From internal/configs/testdata/config-build/root.tf:
module "child_a" {
  source = "./child_a"
}

module "child_b" {
  source = "./child_b"
}

Module Registry

Public modules are available on the Terraform Registry:
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"
  
  name = "my-vpc"
  cidr = "10.0.0.0/16"
  
  azs             = ["us-west-2a", "us-west-2b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
}

Private Module Registry

Host private modules in a registry:
module "vpc" {
  source  = "app.terraform.io/example-corp/vpc/aws"
  version = "1.0.0"
}

Implementation Details

From internal/configs/module_call.go:18-41:
type ModuleCall struct {
    Name string
    
    SourceAddr      addrs.ModuleSource
    SourceAddrRaw   string
    SourceAddrRange hcl.Range
    SourceSet       bool
    
    Config hcl.Body
    
    Version VersionConstraint
    
    Count   hcl.Expression
    ForEach hcl.Expression
    
    Providers []PassedProviderConfig
    
    DependsOn []hcl.Traversal
    
    DeclRange hcl.Range
    
    IgnoreNestedDeprecations bool
}

Best Practices

Single Responsibility

Each module should have a single, well-defined purpose.

Minimal Inputs

Expose only necessary inputs. Provide sensible defaults.

Useful Outputs

Output values that other modules or configurations might need.

Version Constraints

Always specify version constraints for external modules.

Next Steps

Variables

Define module inputs

Outputs

Export module values

Build docs developers (and LLMs) love