What Is Terraform Reusability and How to Achieve It

Cemal Can Seyhan
5 min readApr 1, 2024

--

Today, we’ll continue our discussion about Terraform modules. What is reusability? No, I’m seriously asking, what does it mean to be reusable? We often talk about Terraform and its modules being reusable. While we agree on this on paper, I realize there is confusion about how to achieve this and to what extent we want our modules to be reusable. In this article, I’ll share the questions in my mind and the answers I’ve found, if any.

Dividing the concept of reusability in Terraform into two can be beneficial for us;

first is parameterization, meaning assigning all our values in the Terraform configuration files to variables, allowing the code to be rerun with different parameters. This method is very useful in completely equivalent environments and might require state management or different software supports like Terraspace, but those are topics for another lengthy article… Parameterization, or assigning all inputs to variables, is necessary not only for reusability but also for readability, which is why many of us see this step as essential in most scenarios. However, creating two environments that can rerun the exact same code with different parameters often turns into a management and architectural issue rather than a Terraform puzzle. Unfortunately, we rarely see identical testing and production environments in cloud environments. This ultimately becomes a barrier to reusing Terraform with parameterization, despite its widespread use.

The second method is modularization, meaning our Terraform code can be divided into logical blocks, yes, I’m talking about modules. Modules are actually the fundamental feature that enables Terraform to be reusable. We can write our Terraform code in either a monolithic or modular structure. As you can guess, writing in a monolithic manner, apart from being non-reusable, also brings challenges with complexity, scalability, and collaboration, making it a less preferred option.

Thus, we face the question of how to structure our modules, which we find logical to use in any case. It’s important to remember that as cloud engineers using IaC, we design our code according to the needs, preferences, and importantly, the capabilities of the company we serve or work for, so claiming there is only one correct way would be misleading. With this mindset, we must realize that sometimes the ones who will reuse the code are our clients, not us, and we need to design the module structure accordingly.

We know that modules can consist of a single resource or multiple resources. First, let’s talk about modules with just one resource. For example, let’s create a scenario for writing a small subnet module: you were asked to write a module to add a subnet to an existing virtual network. The required properties for the subnet such as name, resource_group_name, virtual_network_name, and address_prefixes were provided, and you were also asked to include a service endpoint for Azure Key Vault. In this case, you simply wrote:

resource “azurerm_subnet” “subnet” {
name = var.name
resource_group_name = var.resource_group_name
virtual_network_name = var.virtual_network_name
address_prefixes = var.address_prefixes
service_endpoints = var.service_endpoints
}

And designed your service_endpoints variable as follows:

variable “service_endpoints” {
description = <<EOD
(Optional) The list of Service endpoints to associate with the subnet.
Possible values include: Microsoft.AzureActiveDirectory, Microsoft.AzureCosmosDB, Microsoft.ContainerRegistry, Microsoft.EventHub, Microsoft.KeyVault, Microsoft.ServiceBus, Microsoft.Sql, Microsoft.Storage, Microsoft.Storage.Global and Microsoft.Web.
EOD
type = set(string)
}

Thus, the user can simply assign the desired value to the variable to use the module. What if you’re later asked for a subnet without a service endpoint? Instead of writing a new module and multiplying your workload, you can simply turn your variable into one with a default value, allowing Terraform to use it when no value is entered. The new code will be:

variable “service_endpoints” {
description = <<EOD
(Optional) The list of Service endpoints to associate with the subnet.
Possible values include: Microsoft.AzureActiveDirectory, Microsoft.AzureCosmosDB, Microsoft.ContainerRegistry, Microsoft.EventHub, Microsoft.KeyVault, Microsoft.ServiceBus, Microsoft.Sql, Microsoft.Storage, Microsoft.Storage.Global and Microsoft.Web.
EOD
default = null
type = set(string)
}

Thus, you can use the same module in the way you want.

Many of us probably knew this very simple method, but I have refreshed it here. There are many simple built-in methods like this in our modules, providing us with endless freedom. Terraform expressions might be challenging at first if you’re not coming from a software background, but this challenge will change your perspective and take Terraform beyond a monotonous Excel sheet at work. As an example, let’s look at another subnet module:# Manages a subnet. Subnets represent network segments within the IP space defined by the virtual network.

resource "azurerm_subnet" "subnet" {
name = var.name
resource_group_name = var.resource_group_name
virtual_network_name = var.virtual_network_name
address_prefixes = var.address_prefixes

dynamic "delegation" {
for_each = var.delegations
content {
name = delegation.value.name
service_delegation {
name = delegation.value.service_delegation.name
actions = delegation.value.service_delegation.actions
}
}
}

private_endpoint_network_policies_enabled = var.private_endpoint_network_policies_enabled
service_endpoints = var.service_endpoints
}

Reminder: The right path is the one that benefits you and your work the most. If you will only have two types of subnets and you want to call these modules from a separate repository with version tags, even keeping some inputs fixed to avoid repetition, then you are correct; the above example may not suit you. But both perspectives are valid :)

Now, let’s talk about modules that contain multiple resources. Modules with multiple resources typically consist of interconnected resources, like a storage account and its endpoint. It’s common for the endpoint to always be created in scenarios where the storage account is private, but if we want to design this module to be suitable for both scenarios, Terraform’s meta-arguments like for_each and count combined with conditional expressions will give us the freedom to choose whether to create this resource or not.

# Storage Account
module "st" {
source = "./MODULES/storage_account"
name = var.name
resource_group_name = var.resource_group_name
location = var.location
account_tier = var.account_tier
account_replication_type = var.account_replication_type
public_network_access_enabled = var.public_network_access_enabled
tags = var.tags
}

# Private Endpoint
module "pep_st" {
source = "./MODULES/private_endpoint"
count = var.public_network_access_enabled == false ? 1 : 0
name = var.endpoint.storage_account_name
resource_group_name = var.endpoint.resource_group_name
location = var.location
subnet_id = var.endpoint.subnet_id

private_service_connection = {
name = var.private_service_connection.name
is_manual_connection = var.private_service_connection.is_manual_connection
private_connection_resource_id = module.st.id
subresource_names = ["blob"]
}

private_dns_zone_group = {
name = var.private_dns_zone_group.name
private_dns_zone_ids = var.private_dns_zone_group.private_dns_zone_ids
}

tags = var.tags
}

These are just some of the simplest things Terraform can do. I can’t cover everything at once, nor do I believe you can learn it all in one reading. It’s hard to understand some things without getting our hands dirty but trying them on a rest day can be simple and educational. I know we work a lot, but learning doesn’t count as work. :) Terraform is not just a flat inventory management application; it can be a helpful assistant with many features, and sometimes even a friend.

--

--