Terraform Module Inputs Outputs and Calling Modules
The previous topic introduced what modules are and why they matter. This topic goes deeper — covering exactly how data flows into a module through inputs, how results come back out through outputs, and the patterns professionals use when composing multiple modules together into a complete system.
How Data Flows Through a Module
Diagram: Data Flow In and Out
Root configuration
|
| passes inputs (variable values)
v
+----------------------------+
| Child Module |
| variables.tf (receives) |
| main.tf (creates things) |
| outputs.tf (returns) |
+----------------------------+
|
| returns outputs (computed values)
v
Root configuration uses:
module.MODULE_NAME.OUTPUT_NAME
The module is a sealed unit. The root configuration cannot directly reference resources inside the module — only the values the module explicitly exports through its outputs.tf.
Module Inputs in Detail
When you call a module, you pass values for every variable that the module declares without a default. These are the module's required inputs.
# Module declaration (modules/compute/variables.tf)
variable "instance_type" {
description = "EC2 instance size"
type = string
}
variable "name_prefix" {
description = "Prefix for resource names"
type = string
}
variable "enable_monitoring" {
description = "Enable detailed CloudWatch monitoring"
type = bool
default = false # optional input — has a fallback
}
# Root module calling the compute module
module "api_servers" {
source = "./modules/compute"
instance_type = "t3.medium" # required — no default
name_prefix = "api" # required — no default
enable_monitoring = true # optional — overrides default
}
Module Outputs in Detail
Outputs in a child module are the only channel through which the module communicates results back to its caller. Define outputs carefully — they form the module's public interface.
# Module outputs (modules/compute/outputs.tf)
output "instance_ids" {
description = "IDs of all created EC2 instances"
value = aws_instance.servers[*].id
}
output "private_ips" {
description = "Private IP addresses of all instances"
value = aws_instance.servers[*].private_ip
}
# Root module reading the compute module's outputs
module "api_servers" {
source = "./modules/compute"
instance_type = "t3.medium"
name_prefix = "api"
}
resource "aws_lb_target_group_attachment" "api" {
count = length(module.api_servers.instance_ids)
target_group_arn = aws_lb_target_group.api.arn
target_id = module.api_servers.instance_ids[count.index]
}
output "api_server_ips" {
value = module.api_servers.private_ips
}
Chaining Modules Together
In real architectures, modules depend on each other. A compute module needs network IDs from a networking module. A database module needs subnet IDs. Pass module outputs directly as inputs to other modules.
module "network" {
source = "./modules/networking"
vpc_cidr = "10.0.0.0/16"
environment = var.environment
}
module "database" {
source = "./modules/database"
subnet_ids = module.network.private_subnet_ids # output from network module
vpc_id = module.network.vpc_id # output from network module
}
module "compute" {
source = "./modules/compute"
subnet_ids = module.network.public_subnet_ids # output from network module
db_endpoint = module.database.endpoint # output from database module
}
Diagram: Chained Module Data Flow
module "network"
vpc_id -------------------> module "database"
private_subnet_ids -------> module "database"
public_subnet_ids --------> module "compute"
|
module "database" |
endpoint -----------------------+-> module "compute"
Module Versioning
When you use a module from the Terraform Registry or a Git source, pin a specific version. This prevents a future update to the module from unexpectedly changing your infrastructure.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.2.0" # pinned version
name = "production-vpc"
cidr = "10.0.0.0/16"
}
Change the version number intentionally, review the module's changelog, then test in a non-production environment before updating production. This is the same discipline as managing application library versions.
Module meta-arguments
Module calls support the same meta-arguments as resource blocks:
| Meta-argument | Purpose |
|---|---|
count | Create multiple instances of the same module |
for_each | Create one module instance per map or set item |
depends_on | Declare explicit module-level dependencies |
providers | Pass specific provider configurations to the module |
# Create the same module for three environments
module "environment" {
for_each = toset(["dev", "staging", "prod"])
source = "./modules/environment"
environment = each.key
}
Key Points
- Module inputs are variable values passed in the
moduleblock; required inputs have no default in the module'svariables.tf. - Module outputs are the only way to expose internal resource attributes to the calling configuration.
- Reference module outputs with
module.MODULE_NAME.OUTPUT_NAME. - Chain modules by passing one module's outputs directly as another module's inputs.
- Always pin a version when using registry or remote modules to prevent unexpected infrastructure changes.
