A guide to Terraform refactoring:
Step 2 — Start DRYing

Julien Plantier
Esker-Labs
Published in
4 min readJul 1, 2021

Hi everyone, in this series of articles we will see how to refactor Terraform code, from a basic file handling all the resources to a modular architecture. All the code used in the article can be found here : https://github.com/esker-software/terraform-refactoring.
The code uses the Azure provider because that’s what I work with everyday but the refactoring techniques described in the article are not specific to Azure.

Welcome back everyone, in the last article we saw how to split our Terraform files into multiple folders. The current state of our code at the start of the article can be found in the repository under 2_split_folders.
We will now start making our Terraform code DRY. DRY stands for “Dont Repeat Yourself”. It’s a software development practice stated as "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system". The idea is to reduce code duplication.
Let’s see how we can apply it to our Terraform code.
If we look at our web/main.tf file we can find this block:

locals {
hostname_web_vm = "my-web-vm"
location = "westeurope"
resource_group_name = "test_rg"
}

Now let’s check database/main.tf and we find:

locals {
hostname_db_vm = "my-db-vm"
location = "westeurope"
resource_group_name = "test_rg"
}

And you’ve guessed it but we find a very similar block in network/main.tf:

locals {
location = "westeurope"
resource_group_name = "test_rg"
}

Having our resource group name and location stated multiple times in our code clearly violates our DRY principle.
To fix that we’re going to regroup our common variables in a module. A module in Terraform is just any folder that will be called by some Terraform code elsewhere. If you’re not familiar with the concept, the Terraform documentation and tutorial on them are a must read.
We’re going to use output variables to define our resource group and location name.
Let’s create a new settings folder in our Terraform repository and add a main.tf file with this code :

output resource_group_name {
value = "test_rg"
}
output location {
value = "westeurope"
}

We can now replace our duplicated local variables, for example in database/main.tf:

module settings {
source = "../settings"
}
...
resource "azurerm_network_interface" "db-nic" {
name = "${local.hostname_db_vm}-nic"
location = module.settings.location
resource_group_name = module.settings.resource_group_name

...

In the same logic, we can also add those variables to settings/main.tf:

output vnet_name {
value = "my-vnet"
}
output subnet_name {
value = "my_subnet"
}

And consequently use module.settings.vnet_name and module.settings.subnet_name when we need them.

The settings folder represents our single source of truth for all variables that will be shared by our infrastructure. It is minimalist here but on a real project it can contain quite a lot of data so don’t hesitate to split it into files/subfolders if it is necessary.
One of the benefits of having this folder is that you could now recreate the same infrastructure in another location just by copy/pasting the current repository and modifying only variables in settings. This can be useful for example if you want to create a geographical redundancy of your infrastructure.

Continuing our efforts to DRY our code we’ll look at this code that’s present in both the database and web folders:

data "azurerm_subnet" "my_subnet" {
name = module.settings.subnet_name
virtual_network_name = module.settings.vnet_name
resource_group_name = module.settings.resource_group_name
}

When doing infrastructure as code there’s a lot of times where we are going to need network information to create our resources so let’s regroup them somewhere!
We will create a new module under data/network with a main.tf file containing this:

module "settings" {
source = "../../settings"
}
data "azurerm_subnet" "my_subnet" {
name = module.settings.subnet_name
virtual_network_name = module.settings.vnet_name
resource_group_name = module.settings.resource_group_name
}
output "my_subnet" {
value = data.azurerm_subnet.my_subnet
}

We can then use it in our other code like this:

module network {
source = "../data/network"
}
resource "azurerm_network_interface" "web-vm-nic" {
name = "${local.hostname_web_vm}-nic"
location = module.settings.location
resource_group_name = module.settings.resource_group_name
ip_configuration {
name = "${local.hostname_web_vm}-ipconfig"
subnet_id = module.network.my_subnet.id
private_ip_address_allocation = "Dynamic"
}
enable_accelerated_networking = true
}

If we need some other common data on our infrastructure we can create new subfolders in data.

We started using small modules to refactor our code and remove code duplication and that’s going to be enough work for now! In the next step we will continue to improve our code by creating a module a bit bigger that will handle the resources needed to create a VM.
See you soon!

--

--