WintelGuy.com

Terraform Modules FAQ (Part 1)

Terraform modules are a powerful way to structure and manage infrastructure as code, enabling reusability, consistency, and scalability across projects. In this FAQ, we'll cover common questions about Terraform modules, including their purpose, best practices, and how to use them effectively in real-world scenarios.

Introduction to Terraform Modules:

Structure and Organization of Terraform Modules:

Creating and Managing Terraform Modules:

See the FAQ Part 2 for the most common questions about versioning and troubleshooting of Terraform modules.

Introduction to Terraform Modules

What are Terraform modules?

Terraform modules are reusable code blocks similar in concept to subroutines in traditional programming languages. Modules are widely used to standardize infrastructure deployment and simplify complex configurations.

A Terraform module is a collection of Terraform configuration files organized in a directory. The files form a reusable unit of infrastructure code that defines a resource or a group of related resources. A module can be as simple as a single .tf file or can contain multiple files with many resource definitions, variables, and outputs.

Typically, a module consists of the following files:

  • main.tf - contains the core resources and logic.
  • variables.tf - declares input variables to make the module configurable.
  • outputs.tf - defines output values that other configurations can use.

Here is an example of a Terraform configuration with a module called vpc:

tf-project/ ├── main.tf ├── variables.tf ├── modules/ │ ├── vpc/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf

The tf-project/ folder (root module) contains two files - variables.tf and main.tf with the following content:

# variables.tf variable "project_id" { description = "The GCP project ID" type = string default = "my-project-id" }
# main.tf terraform { required_providers { google = { source = "hashicorp/google" version = "6.28.0" } } } provider "google" { project = var.project_id } module "vpc" { source = "./modules/vpc/" vpc_name = "my-vpc" project_id = var.project_id } output "vpc_name" { description = "The name of the created VPC" value = module.vpc.vpc_name }

This configuration utilizes Google GCP provider and calls the vpc module to provision a virtual private cloud (VPC) network with the name my-vpc. The module block specifies the location of the module files (source) and defines the VPC configuration parameters - vpc_name and project_id.

The vpc module creates an auto mode VPC network ("google_compute_network") with the name and location defined by the two input variables - vpc_name and project_id. The module consists of three files - variables.tf, main.tf, and outputs.tf located in the modules/vpc/ folder:

# vpc/variables.tf variable "project_id" { description = "The GCP project ID" type = string } variable "vpc_name" { description = "The VPC name" type = string }
# vpc/main.tf resource "google_compute_network" "vpc_network" { name = var.vpc_name project = var.project_id auto_create_subnetworks = true }
# vpc/outputs.tf output "vpc_name" { description = "The name of the created VPC" value = google_compute_network.vpc_network.name } output "vpc_self_link" { description = "The self_link of the created VPC" value = google_compute_network.vpc_network.self_link }

Back to Top

What is the difference between a Terraform module and a Terraform configuration?

A Terraform configuration is any set of .tf files that define infrastructure. Terraform configurations may or may not use modules.

A Terraform module is a specific type of configuration that is reusable and can be called from another module or configuration.

The Terraform configuration files placed directly within a project folder (excluding any sub-folders) form the root module. The root module may utilize other explicitly defined modules stored, for instance, in sub-folders under the main project directory. Such structure helps divide infrastructure into smaller, easier to manage reusable components.

Let's review the following folder structure as an example:

  • The Terraform configuration consists of all files and folder under the my-app/ folder.
  • The root module contains 3 files located directly under the my-app/ folder - main.tf, variables.tf, and backend.tf.
  • The configuration also includes two folders with modules - vpc/ and vm/.
my-app/ ├── main.tf ├── variables.tf ├── backend.tf └── modules/ ├── vpc/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── vm/ ├── main.tf ├── variables.tf └── outputs.tf

Back to Top

Why use Terraform modules?

Using Terraform modules offers several benefits:

  • Reusability - Modules allow you to define infrastructure components once and use them multiple times across projects.
  • Maintainability - By organizing infrastructure code into smaller, modular components, it's easier to manage and update.
  • Consistency - Modules help enforce best practices and uniform configurations across different applications and environments (e.g., development, staging, production).
  • Scalability - Modules streamline infrastructure expansion by simplifying the deployment of identical resources.
  • Collaboration - Teams can share and version-control modules, ensuring a structured approach to infrastructure provisioning.

By leveraging modules, teams can keep Terraform configurations organized, scalable, and easy to manage.

Back to Top

Structure and Organization of Terraform Modules

What is the recommended file and directory structure for a Terraform module?

A well-structured Terraform module typically follows this layout:

module-name/ │── main.tf # Defines resources │── variables.tf # Defines input variables │── outputs.tf # Defines output values │── providers.tf # (Optional) Specifies required providers │── README.md # (Recommended) Documents the module usage │── examples/ # (Optional) Contains example usage of the module │── tests/ # (Optional) Includes automated tests (e.g., using Terratest)

Such structure makes it easier to navigate, reuse, and maintain modules.

Back to Top

What are the recommended naming conventions for Terraform modules and variables?

Consistent naming conventions improve readability and usability. Follow these guidelines:

  • Module Names: Use hyphenated lowercase names for module directories, e.g., aws-vpc or gcp-network.
  • Variable Names: Use snake_case for variable names, e.g., instance_type instead of instanceType.
  • Resource Names: Use descriptive names and separate words with underscores, e.g., web_server. Use nouns for resource names and do not include the resource type in the name.
  • Output Names: Use meaningful names that describe the returned value, e.g., vpc_id or db_endpoint.
  • Tags and Labels: Standardize tags (e.g., environment = "prod"), especially when managing multiple environments.

Adhering to these conventions ensures clarity and consistency across Terraform configurations.

Back to Top

Can I use modules inside other modules (nested modules)?

Yes, Terraform allows nested modules, meaning one module can contain another modules inside it. This can be useful for organizing complex infrastructure by keeping all related resources together.

In the example below, the network module includes separate submodules for VPC, subnet, and security group:

modules/ │── network/ │ │── main.tf │ │── variables.tf │ │── outputs.tf │ │── modules/ │ │ ├── vpc/ │ │ │ ├── main.tf │ │ │ ├── variables.tf │ │ │ ├── outputs.tf │ │ ├── subnet/ │ │ │ ├── main.tf │ │ │ ├── variables.tf │ │ │ ├── outputs.tf │ │ ├── security-group/ │ │ │ ├── main.tf │ │ │ ├── variables.tf │ │ │ ├── outputs.tf

Best practices for using nested modules:

  • Avoid excessive nesting to prevent complexity.
  • Clearly document how submodules interact.
  • Use module outputs to pass data between modules.

Back to Top

Creating and Managing Terraform Modules

What are the best practices for building Terraform modules?

To ensure Terraform modules are easy to use, maintain, and scale, follow these best practices:

  • Keep modules focused - Each module should serve a single purpose, such as provisioning a network or a VM instance.
  • Implement the conventional module structure - A module should have a main.tf (resources) file, variables.tf (inputs), outputs.tf (outputs), and optionally a README.md file for documentation.
  • Use input variables wisely - Expose only necessary variables to keep the module flexible yet simple. Set default values for optional parameters.
  • Define clear outputs - Provide useful outputs to allow other configurations to consume module data.
  • Write documentation - Include a README.md file with usage instructions, variable descriptions, and example configurations.
  • Version control your modules - Use Git (or similar) repositories and version tags to track changes and avoid unintended updates.

Back to Top

How do I use input variables to make Terraform modules configurable?

Input variables allow modules to be flexible and customizable. The variable blocks withing a module define the module's configurable parameters. The calling module uses these variables to pass the desired values to the module.

Let'a review this example of a module that provisions an Ubuntu VM in GCP:

# vm/main.tf variable "vm_name" { description = "The VM name" type = string } variable "vm_type" { description = "The VM type" type = string default = "f1-micro" } variable "project_id" { description = "The GCP project ID" type = string } variable "network_name" { description = "The VPC name" type = string default = "default" } variable "zone_name" { description = "The zone name" type = string } resource "google_compute_instance" "vm_ubuntu" { name = var.vm_name machine_type = var.vm_type project = var.project_id zone = var.zone_name boot_disk { initialize_params { image = "ubuntu-os-cloud/ubuntu-2204-lts" } } network_interface { network = var.network_name } }

The vm module has five input parameters - two optional and three mandatory. The mandatory parameters (vm_name, project_id, and zone_name) do not have default values and must be explicitly defined when invoking the module. The calling module may also set values the optional parameters (vm_type and network_name) to override the defaults.

Here is an example of module invocation that overrides the default network_name value:

module "vm" { source = "./modules/vm/" vm_name = "my-vm" project_id = var.project_id zone_name = "us-east1-b" network_name = "my-vpc" }

Some best practices for using input variables:

  • Use default values where applicable to simplify module usage.
  • Enforce type constraints (e.g., string, list, map) and use custom conditions (validation block) to check whether input values are formatted properly.
  • Use ephemeral and sensitive if necessary.
  • Document variables in README.md for better usability.

Back to Top

How do I use output values for module interoperability?

Output values allow a module to expose resource attributes so they can be used by other modules or configurations.

The following code fragments illustrate how to pass information from one module to another.

The configuration shown below employs two modules - vpc and vm to provision a VM attached to a VPC network. First, it calls the vpc module to create a network. After completion, the vpc module returns a reference to the newly created VPC network in the vpc_self_link output variable. Then, the configuration invokes the vm module and supplies network details by referencing the output from the first module with the module.vpc.vpc_self_link construct, where vpc indicates the module name and vpc_self_link - the output name.

module "vpc" { source = "./modules/vpc/" vpc_name = "my-vpc" project_id = var.project_id } module "vm" { source = "./modules/vm/" vm_name = "my-vm" project_id = var.project_id zone_name = "us-east1-b" network_self_link = module.vpc.vpc_self_link } output "vpc_self_link" { description = "The self_link of the created VPC" value = module.vpc.vpc_self_link }

Here is the code for the vpc module that provisions a network and returns its self link reference in the output variable called vpc_self_link.

# vpc/main.tf resource "google_compute_network" "vpc_network" { name = var.vpc_name project = var.project_id auto_create_subnetworks = true } output "vpc_self_link" { description = "The self_link of the created VPC" value = google_compute_network.vpc_network.self_link }

Note: Terraform does not displays the outputs from modules. To show the values, add output blocks in the root module.

Best practices for using module outputs:

  • Use descriptive names for outputs.
  • Ensure sensitive values (e.g., passwords) are marked as sensitive = true to prevent logging.
  • Document outputs in README.md.

Back to Top

How do I use locals to simplify module logic?

Locals allow you to:

  • Define reusable expressions within a module.
  • Reduce repetition and improve maintainability.
  • Provide a single place to define computed values that can used across multiple resources.

The following example shows a locals block used to construct a firewall rule description with a heredoc string. The resulting string is later referenced in the google_compute_firewall block as local.description keeping the module's code easy readable and concise.

# fw-in/main.tf locals { description = <<-EOT Allow from ${join(", ", var.src_ip_ranges)} to ${join(", ", var.dest_ip_ranges)} on ports ${join(", ", var.ports)} EOT } resource "google_compute_firewall" "rule" { project = var.project_id name = var.name network = var.network description = local.description direction = "INGRESS" source_ranges = var.src_ip_ranges destination_ranges = var.dest_ip_ranges allow { protocol = "icmp" } allow { protocol = var.protocol ports = var.ports } }

Back to Top

Conclusion