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-argumentPurpose
countCreate multiple instances of the same module
for_eachCreate one module instance per map or set item
depends_onDeclare explicit module-level dependencies
providersPass 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 module block; required inputs have no default in the module's variables.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.

Leave a Comment