All guides
Practice now
D5 · Terraform Modules

Modules: structure, inputs and outputs

Modules are the main mechanism for reuse and encapsulation in Terraform. Understanding the difference between the root module and child modules, how data flows between them, and the standard file structure is essential for the exam.

What is a module?

A is simply a directory containing one or more .tf files. There is nothing special about it: any directory with .tf files is a module.

Modules let you encapsulate and reuse configuration. Instead of repeating the same VPC configuration across 10 projects, you define it once in a module and call it with different parameters.

Encapsulation

Hides internal complexity. The user only needs to know the inputs and outputs.

Reuse

The same module can be used with different variables for dev, staging and prod.

Consistency

All projects follow the same pattern because they use the same module.

Root module vs child modules

Root module

  • • The directory where you run terraform init/plan/apply
  • • Always exists — it's the entry point
  • • Holds the of the entire managed infrastructure
  • • Can call child modules
  • • Its outputs are shown in the apply
  • /infra/prod/ ← root module

Child modules

  • • Modules called by the root module or other modules
  • • Have no state of their own — they share the root module's state
  • • Receive data via (inputs)
  • • Expose data via
  • • Can be local or from the registry
  • /modules/vpc/ ← child module
# Typical structure with modules:
infra/
├── prod/
│   ├── main.tf         ← root module (calls child modules)
│   ├── variables.tf
│   └── outputs.tf
└── modules/
    ├── vpc/            ← child module (networking)
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    └── eks/            ← child module (cluster)
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Calling a module

You use the module block with a label (local name), the source argument (required), and the module's inputs as additional arguments.

# Call a local module
module "vpc" {
  source = "./modules/vpc"      # source required

  # Module inputs (variables defined in modules/vpc/variables.tf)
  name       = "my-vpc"
  cidr       = "10.0.0.0/16"
  azs        = ["us-east-1a", "us-east-1b"]
  environment = var.environment
}

# Call a module from the registry (version required)
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "my-cluster"
  cluster_version = "1.29"
  vpc_id          = module.vpc.vpc_id         # output from the vpc module
  subnet_ids      = module.vpc.private_subnet_ids
}

# Accessing module outputs:
# module.<local_name>.<output_name>
output "cluster_endpoint" {
  value = module.eks.cluster_endpoint
}

💡 terraform get vs terraform init

Both download modules into the .terraform/modules/ directory. The difference is that terraform init also installs providers and configures the backend, whereas terraform get only downloads modules. In practice you always use init.

Module inputs and outputs

Modules communicate with their environment through variables (inputs) and outputs. This is the module's contract.

modules/vpc/variables.tf (inputs)

variable "name" {
  type        = string
  description = "VPC name"
}

variable "cidr" {
  type        = string
  default     = "10.0.0.0/16"
  description = "VPC CIDR block"
}

variable "azs" {
  type        = list(string)
  description = "List of availability zones"
}

modules/vpc/outputs.tf (outputs)

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

output "private_subnet_ids" {
  value       = aws_subnet.private[*].id
  description = "IDs of the private subnets"
}

output "public_subnet_ids" {
  value       = aws_subnet.public[*].id
  description = "IDs of the public subnets"
}

⚠️ You can only access what the module exposes as an output

From the parent module, you cannot access internal of the child module directly (for example, module.vpc.aws_vpc.main.id does not work). You can only use the outputs explicitly defined in the child module (module.vpc.vpc_id). This guarantees encapsulation.

Standard file structure

HashiCorp defines a recommended standard structure for modules. Modules on the Terraform Registry must follow it.

main.tf

Recommended

The module's main resources. It is the conceptual entry point. For complex modules, it can be split into multiple files (network.tf, compute.tf, etc.).

variables.tf

Recommended

All variable {} declarations. Defines the module's input contract.

outputs.tf

Recommended

All output {} blocks. Defines what the module exposes to the outside.

README.md

Registry

Module documentation: what it does, inputs, outputs, usage examples. Required to publish to the registry.

versions.tf

Recommended

A terraform {} block with required_version and required_providers. Separates version requirements from the main code.

locals.tf

Optional

A locals {} block for computed values. Only needed if there are many locals.

CHANGELOG.md

Registry

Change history by version. Important for published modules.

examples/

Registry

A directory with usage examples for the module. Each subdirectory is a complete, working example.

Did you get this topic?

Test what you just learned

The root module calls a child module 'networking' that creates a VPC and defines an output named 'vpc_id'. How does the root module access that value?

Ready to test "Modules: Structure, Inputs and Outputs"?

Put what you learned to the test with original Terraform Associate exam questions.

Start Terraform Associate mock exam

Unlimited mock exams with the Pro plan

Log in to track your progress.