Terraform, being an Infrastructure as Code (IaC) tool, enables you to write declarative code that is then used to provision and manage resources using providers for the resource types that your project requires. It’s a powerful tool enables you to write a single project of Terraform code that manages resources across one or more clouds (Azure, AWS, Google, etc); as well as any other infrastructure that has a Terraform provider like Kubernetes and Helm.

Since Terraform is a coding language (via HCL) it also supports the ability to create reusable blocks of code called Modules. This article provides a deep dive into writing Terraform Modules so you can take your Terraform projects to the next level with code reusability by authoring modules that can be used within multiple Terraform projects.

We gotta keep our code DRY (Don’t Repeat Yourself), right?! Let’s dig in!



What are Terraform Modules?

Terraform Modules are a way to create blocks of Terraform HCL (HashiCorp Configuration Language) code that is reusable either within a single Terraform project, or even reusable across multiple Terraform projects. A developer analogy when thinking about Terraform Modules is to compare them to a function or method of code.

At first, when you start coding a Terraform project, you create one or more Terraform (.tf) code files within a single folder. This is similar to writing all your code for a program in a single 1,000+ line source code file. With Modules, you can break that code up in to blocks that are used throughout your program. This provides for a way to create code reuse and adhere to the DRY (Don’t Repeat Yourself) best practice in coding, as well as make your code base easier to read and maintain over time.

Modules in Terraform offer benefits for several use cases:

  • Terraform Modules reduce code duplication in a project.
  • Terraform Modules allow code reuse across multiple projects.
  • Terraform Modules can increase the overall human readability of the project.
  • Terraform Modules can ease the burden of code maintenance over time.
  • Terraform Modules can improve efficiency of making configuration changes to multiple resources that are provisioned the same way.
  • Terraform Modules can help standardize compliance and security standards across various infrastructure and projects.

At the most basic level a Terraform Module is a collection of Terraform code files (.tf) in the same folder. This is the same as a normal Terraform project, except with a Terraform Module you consume the module from another module or project to get infrastructure code reuse. When thinking about code reuse in comparison to other development technologies, it helps to think about a Terraform Module like a library of code that is referenced and called from another library or application of code.


Why use Terraform Modules?

Code reuse is the first reason to think about when deciding whether to build Terraform Modules. This is the same as reusable methods in other programming languages, and this enables increases reuse of the Infrastructure as Code (IaC) part of a solution. This also helps to modularize the Terraform code for a single Terraform project to make it easier to read and maintain. These are all examples of how Terraform Modules can be useful, but not necessary the core reasons to build and use modules in the first place.

When writing any source code for a project, including Terraform Projects, there are certain blocks of code that are written multiple times. When you have a code block that gets duplicated this way, it’s best practice to isolate out that block of code and make it a separate method or “module” in the case of Terraform. This enables reuse and prevents you from repeating yourself in a code base.

There are also times went coding conventions and best practices need to be enforced in the code base of a Terraform Project, or even across multiple Terraform Projects managed within your organization. This could be in the form of ensuring compliance and security standards are being met by “default” rather than just trusting the DevOps Engineers and/or Site Reliability Engineers (SREs) in your organization are writing the code they should be.

A great reason to create Terraform Modules is to isolate common blocks of Terraform code that are used in one or more projects many times, and enhance it with coding conventions, compliance requirements, best practices, and other security standards that are defined requirements to adhere to within your team and organization. This helps ensure these requirements are met regardless of what Terraform Project manages the resources a specific Terraform Modules is coded to manage.

A couple of security or compliance requirements that may need to be adhered to are supporting only the latest version of TLS or connecting an Azure Network Security Group (NSG) to a Virtual Network (VNet) Subnet to lock down network traffic flow. These are types of “convention over configuration” that could be implemented in a Terraform Project, or across multiple Terraform Projects, through the use of a set of shared Terraform Modules.

When the Terraform Module implements the conventions of configuring resources a certain way, then any Terraform project using that module will automatically get those benefits. Then, when the Terraform Module is updated to a change in compliance or security standards, the Terraform Projects that use that module may automatically benefit from those changes or be easily upgraded to use a newer version of that module for intentional use of the new version.

Convention over configuration” is an idea that comes from software development, largely originating in Ruby on Rails and adopted elsewhere, that makes a preference of adhering to conventions by default in a software system instead of requiring the custom configuration of settings on every use. Essentially, applied to Terraform Modules, the “default” parameters, argument and configuration of the resources managed by the module will generally meet the compliance, security or other requirements without needing to explicitly set those configurations in the Terraform Project that consumes the module. This enables an automatic win of meeting defined team and organizational standards on the resources managed by the Terraform Project.


Terraform Module Basics

A Terraform Module is a collection of Terraform code files (.tf) in the same folder, so the files in the module will generally be broken out in a similar arrangement of file names to any other Terraform Project. As a result of the simplicity of Terraform, the Terraform Modules and Project can be broken out into any folder structure you wish.

As an example, the location of a module to create an Azure Web App using the azurerm Terraform Provider may be structured within a /modules/azure-web-app folder within the main Terraform Project. Within this module, the Terraform code (.tf files) is organized in files named main.tf, outputs.tf, and variables.tf. This is similar to the file structure organization used in any standard Terraform Project with the exception that this is a Terraform Module that will be reused.

Image: Folder structure of a Terraform module as well as 'dev' and 'tst' environments
Image: Folder structure with a module as well as ‘dev’ and ‘tst’ environments.

The above screenshot shows a typical layout of what may be defined for a Terraform Project that contains Terraform code files (.tf) along with a modules folder with multiple modules, along with /env/dev and /env/tst infrastructure environments. Terraform projects and modules could be arranged in a different folder layout, but this is an example of a pattern commonly used to organize Terraform Modules within a project.

In addition to the previous organization of a /modules folder to easily write reusable infrastructure modules, you can also write nested modules within either a single Terraform Module or a Terraform Project. This is done by simply creating subfolders within the Terraform Module or Project to create nested modules within any level.

Terraform Modules: Create Reusable Infrastructure as Code 4
Image: Nested Module within Terraform /env/dev infrastructure environment project

The Terraform code within the infrastructure environment Terraform Project folders can then use the module block to reference the Terraform Modules to pull them in and reuse their code to manage infrastructure in a reusable fashion.

module "<name>" {
  source = "<source>"

  <inputs / config>
}

With the module block the <name> is an identifier that specifies the name within the Terraform code to refer to this module. This is similar to how resources have names too. The <source> specifies the PATH where the module code is located, and the <inputs / config> represents any input variables or arguments to pass to the module.

The previous screenshot examples include a Terraform Module located within the /modules/azure-webapp folder. Below is an example of defining a Terraform Module within the /env/dev environment Terraform Project that references the azure-web-app module using a relative path so Terraform knows where the module code is located:

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

The previous screenshot examples also include a Terraform Module named region nested within a subfolder of the /env/dev infrastructure project folder. This nested Terraform Module can be used from the /env/dev infrastructure project using a relative path too. Below is an example of defining the usage of this nested region module within the /env/dev project:

module "primary_region" {
  source = "./region"
}

Terraform Module Inputs Variables

The use of Terraform Input Variables can be defined within a Terraform Module to allow for the consuming Terraform code to pass input parameters to the module being used. This is done using the Terraform variable block to define the Input Variable name and type. Defining these variables is done using the same syntax as defining Input Variables for a Terraform Project. The difference is that this time the Terraform Module is being parameterized.

Below is an example of defining an Input Variable for a Terraform Module:

variable "name" {
    type = string
    description = "The name of the Web App to create."
}

Related: To learn more about using Input Variables in Terraform, please go read the “Use Terraform Input Variables to Parameterize Infrastructure Deployments” article written by Chris Pietschmann

It’s common practice to place the Input Variable (or Parameter) definitions in the Terraform code file named variables.tf within the Terraform Module. Also, consuming these Input Variables within the Terraform Module is performed using the same var.<variable-name> syntax as referencing Input Variables within a Terraform Project. Just replace the <variable-name> placeholder with the name defined for the Input Variable.

A full example of defining a couple Input Variables on a Terraform Module, along with consuming those properties in some Terraform code to manage a resource is shown in the following screenshot:

Terraform Modules: Create Reusable Infrastructure as Code 5
Image: main.tf and variables.tf files showing Terraform code for Terraform Modules Input Variables

For clarity, the following Terraform code shows managing a resource and using the previously defined Terraform Module Input Variable to set the name attribute of the resource, along with another Input Variable to set the resource_group_name too.

resource "azurerm_service_plan" "appserviceplan" {
  name                = "${var.name}-asp"
  location            = var.azure_region
  resource_group_name = var.resource_group_name
  os_type             = "Linux"
  sku_name            = "F1"
}

Terraform Module Outputs

Terraform Output Variables can be defined in a Terraform Module to enable passing values out from the module to the consuming Terraform Project.

The following is an example of defining an Output Variable named appserviceplan in a Terraform Module that returns the azurerm_service_plan resource managed by the Terraform Module.

output "appserviceplan" {
    value = azurerm_service_plan.appserviceplan
}

The following screenshot shows the contents of the outputs.tf code file for an example Terraform Module that has two Output Variables defined.

Terraform Modules: Create Reusable Infrastructure as Code 6
Image: outputs.tf file showing Terraform code for Terraform Modules Output Variables

Once the Terraform Module has Output Variables defined, those variables can be referenced by consuming Terraform code the same was parameters of a Terraform resource are accessed within the Terraform code. This is done with the syntax of module.<module-name>.<output-variable>, where the <module-name> placeholder is the name of the Terraform Module and the <output-variable> placeholder is the name of the Output Variable of the Terraform Module being accessed. The following is an example of this:

module.azure-web-app-1.appserviceplan

The following is an example of using a Terraform Module named azure-web-app-1, then accessing the Output Variable named appserviceplan and using it to define an attribute on a different resource being managed by the Terraform code.

module "azure-web-app-1" {
    source = "../../modules/azure-web-app"

    name     = "E1-B59-WebApp1"
    locatiom = local.azure_region
    resource_group_name = azurerm_resource_group.azure_rg.name
}

resource "azurerm_linux_web_app" "webapp2" {
  name                  = "E1-B59-WebApp2-app"
  location              = local.azure_region
  resource_group_name   = azurerm_resource_group.azure_rg.name

  # Access the id attribute of the appserviceplan Output Variable
  # from the module named azure-web-app-1
  service_plan_id       = module.azure-web-app-1.appserviceplan.id

  https_only            = true
  site_config { 
    minimum_tls_version = "1.2"
  }
}

Using External Shared Modules

In addition to defining Terraform Modules and having them located within the same folder structure or code repository as the Terraform Project itself, the Terraform Project is able to consume Shared Terraform Modules that are hosted in other locations. The source attribute of the module block is still used in this case, it’s just set to the external, URL address of the Terraform Module instead of the local, relative file path to the module folder.

Terraform supports several external source types for installing Terraform Modules:

The most commonly used module sources for Terraform Modules are probably repositories in Git, GitHub, Azure DevOps, as well as the Terraform Registry.

GitHub Repository

The source URL for referencing Terraform Modules within a GitHub Repository is the path to the GitHub repository. There are two syntaxes to the source URL of referencing Terraform Modules from GitHub depending on if the module will be downloaded over HTTPS or SSH.

The following is the syntax of the HTTPS and SSH URLs for referencing Terraform Modules contained within a GitHub Repository named build5nines/terraform-modules:

# HTTPS URL
github.com/build5nines/terraform-modules

# SSH URL
git@github.com:build5nines/terraform-modules.git

Git Repository

Any Git repository, such as Azure DevOps Git Repos, can be used as sources for Terraform Modules. The source URL is defined with the git:: prefix followed by SSH or HTTPS URL for the Git repository. This enables any Git repository to be used to host Terraform Modules for your Terraform Project to use.

When a Git repository is used as the source for Terraform Module, the Terraform tool will run git clone on the Git repository to pull down the Terraform Module code. This requires local Git configuration for authenticating with the Git repository on the machine running Terraform so it is able to perform the git clone operation.

The following are examples of using the HTTPS and SSH URLs for a Git repository as the source:

# Git SSH URL
module "WebApp1" {
  source = "git::https://build5nines.com/repo.git"
}
 # Git HTTPS URL
module "WebApp1" {
  source = "git::ssh://username@build5nines.com/repo.git"
}

When using the Git SSH URL as the Terraform Module source, Terraform will automatically use a locally configured SSH keys. SSH is the most common and most highly secured method for automated systems, like build agents running Terraform, to access Git repositories.

It can be common to host multiple Terraform Modules within the same Terraform Project with a sub-folder in the root of the Git repository to be a different Terraform Module. If you have a Git repository with multiple Terraform Modules in it within sub-folders, then you can suffix the source URL with // followed by the sub-folder name containing the Terraform Module.

The following is an example of a Git SSH URL referencing a Terraform Module named azure-web-app.

git::ssh://username@build5nines.com/repo.git//azure-web-app

To reference a Git hosted Terraform Module, the URL will be set as the value of the source attribute of the module block, as follows:

module "WebApp1" {
  source = "git::ssh://username@build5nines.com/repo.git//azure-web-app
}

Azure DevOps Git Repository

Since Azure DevOps Git Repositories are really just Git repositories, they can also be used as a Terraform Module source using the same URL syntax to support both HTTPS and SSH URLs.

The following URLs are examples of the SSH and HTTPS URLs for Azure DevOps Git repositories named terraform-modules within the terraform-modules Git repository of the Azure DevOps build5nines organization.

# Azure DevOps Git Repositories
# SSH
git@ssh.dev.azure.com:v3/build5nines/webapp1/terraform-modules
# HTTPS
https://build5nines@dev.azure.com/build5nines/webapp1/_git/terraform-modules

Just as with referencing Terraform Modules from any other Git repository, multiple Terraform Modules can be hosted within a single Azure DevOps Git Repository with each module in a sub-folder at the root of the Git repository. These individual Terraform Modules within the Git Repository can be referenced by appending the source URL for the Git repository with // followed by the Terraform Module sub-folder name.

The following is an example of referencing a Terraform Module using a Git SSH URL hosted within an Azure DevOps Git Repository contained within a sub-folder of the Git repository.

module "WebApp1" {
  source = "git@ssh.dev.azure.com:v3/build5nines/webapp1/terraform-modules//azure-web-app
}

Related: If you need some assistance configuring an SSH key to be used to authenticate with an Azure DevOps Git Repository, please check out the “Azure DevOps: Create SSH Key to Authorize Git on macOS” article written by Chris Pietschmann.

Terraform Registry

The public Terraform Registry can be referenced when using Terraform Modules that are published within the registry. The Terraform Module source attribute is set to the URL address for Terraform Module in the Terraform Registry.

The Terraform Registry source URL uses the following syntax:

<namespace>/<name>/<provider>

In the Terraform Registry, the URL address placeholders are replaced with the following values:

  • <namespace> – The namespace within Terraform Registry that contains the module.
  • <name> – The name of the Terraform Module within the namespace.
  • <provider> – The Terraform Provider targeted by the module.

When referencing Terraform Modules from the Terraform Registry, the version attribute is also used to specify the version the Terraform Module to reference. This is important, since the Terraform Registry supports multiple versions of the Terraform Modules, and specifying the version will keep it from auto-upgrading to a newer version unintentionally.

The following is an example of referencing a module within the public Terraform Registry:

module "cosmosdb" {
  source  = "Azure/cosmosdb/azurerm"
  version = "1.0.0"
  # Also define required attributes
}

This method of referencing Terraform Modules from the Terraform Registry can also be used to reference modules in a Private Registry. This includes both hosting Terraform Modules within Terraform Cloud, as well as a custom, private registry that implements the module registry protocol.

Microsoft MVP

Chris Pietschmann is a Microsoft MVP (Azure & IoT) and HashiCorp Ambassador (2021) with 20+ years of experience designing and building Cloud & Enterprise systems. He has worked with companies of all sizes from startups to Fortune 100. He is also a Microsoft Certified Azure Solutions Architect and developer, a Microsoft Certified Trainer (MCT), and Cloud Advocate. He has a passion for technology and sharing what he learns with others to help enable them to learn faster and be more productive.