=======================
== blog.shellcode.in ==
=======================
My corner of the Internet

Structuring Terraform

terraform iac

Terraform is a great tool for infrastructure as code (IaC). I for one do not like pointing and clicking to configure things. With Terraform I no longer need to point and click unless it is to reference something. What I mean by this is that I can visit the Azure portal (I mainly work in Azure for my day job) look at the settings for a specific thing such as a virtual machine (VM). I do this primarly because the documentation for Terraform is somewhat lacking and looking in the portal gives me better insights into what I am configuring.

One of the biggest struggles with getting started with Terraform is how to structure your repository. Structuring your repository is not a big deal if what you are building is small then you just have a single root Terraform module. Once you have multiple Azure subscriptions, environments, and resource groups a single root module in a single repository no longer works. I am going to explain the approach I have taken recently with handling the growing complexity of what is managed with Terraform.

Let’s start with the simplest solution and get more complex as we go along.

The most basic solution as described above is a single repository with a single root module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ mkdir single
$ cd single
$ touch main.tf
$ touch outputs.tf
$ touch variables.tf

$ ls single
main.tf
outputs.tf
variables.tf

We have created our basic root module in the directory called single. A very important thing about HCL, the language you write Terraform, in is that everything is a module.

Inside of the main.tf we can have our provider azurerm then have our basic VM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=2.46.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "rg-single-001" {
  name     = "rg-single-001"
  location = "East US"
}

# put vm stuff here

We will leave the other two files empty becuase this is a root module and we do not need to take any inputs for this module so far and do not have any outputs for another module.

Again this works well for when you do not have much to set up. We only have a single VM in this instance. What if we have more than one environment though? What if we need more VMs or we need another resource group? Well we can still put it in this single root module and that will work well until the file starts to grow in length and number of objects. At some point it will be too much to handle for a single file.

This is where the idea of using more modules comes in. Before we start adding more environments we can split up everything we have into more composable pieces. A good example of a would include everything necassary to set up a function app, app service plan, storage account, app insights, plus maybe on or two other things depending on what you app is doing. The idea is that we are not wrapping a single piece but we are making a group of components that represent what we think a function app is. A good example is the Hashicorp Consul Terraform code. The Consul Terraform code bundles together everything Consul needs to work, vualt, database, etc.

Even after we start breaking up large blocks of code up we still will end up with anothe large main.tf file. This is when we start to break up the single large main.tf into smaller pieces. To do this we will split up the main.tf by new logical groups. An example of this would be splitting up by apps. If an app takes a cosmosdb, function app, and a virtual network then group all of those together into a new module.

There are a few different things we can do with our new module. One of the things is putting it into a new repository or we could put it in a nested module inside our current repository. If we nest it in our current repository we would then import the module from out root module. We then could, for example, split up by resource group and subscription. Each resource group would be a nested module. Then the repository would be our subscription. What this also means is that we have a single state file for all of a subscription. Thus our blast radius should something go wrong would be the whole subscription.

It may be favorable to break up by resource group per repository but then you have the problem of “repo sprawl”, or the creation of too many repositories which become hard to manage. It is a death by a thousand cuts. We are damned if we do and damned if we don’t it seems. The solution is to always try to find a happy medium. Chances are your soltuion will never be perfect and there may always be things that could be better or you want to change. The key is to learn to be content with what you have.

When first starting out keep everything in a monorepo. When you are small you don’t need to over complicate things with adding in multiple repositories. It is an anti-pattern at this point. As you start to add code always ask yourself if this getting to big? Can there be things that are broken out? Nothing may come to you at first or maybe somthing does but even then it may not be the perfect solution. You should ponder the idea and try it if the idea still seems good. Maybe try it if it seems bad. Trying it will solidfy the idea in your head.

Resource group modules can live in your monorepo at the start too. The nice thing about composability is if you do not nest any of your modules than one layer deep you can split them out of your monorepo at any time. In the exmaple below both module rg1 and rg2 can use the function-app module. If we want to split these modules out of the monorepo at some point because things are getting to big or we want to limit a blast radius on some idvidual module we can easily move any of the modules to a new repository. There will be a need to update source values but that is not too terrible. Always imagine the alternative of being in Azure portal.

1
2
3
4
5
6
7
8
.
|_
| `main.tf
|-
| ` function-app
|_
|  `rg1
|  `rg2

Terraform solves some issues but brings other issues into the mix. There is no perfect solution for managing infrastructure. Find a solution that works for you and your team. Just remember that using IaC from the start can help you later down the road. Adding IaC halfway through a project will be a big pain. Adding IaC after the project is solidified in prod is a nightmare but doable.

The nice thing at the end of the day if you do your IaC right destorying and creating environments and resources should be only a few commands or clicks. Being able to bring environments up and down so easily with complete reproducibility makes life wonderful.