Three Reasons to Write a Terraform Module
Every Terraform module you write should have a reason to exist. Engineers have a terrible habit of creating layers of abstraction that don't add value. The most obvious expression of this phenomenon is the 'thin wrapper' Terraform module around the underlying Azure resource, which exposes the same API surface area. The engineer who wrote that wrapper would have been better off using the underlying resource directly. However, there are cases when writing a Terraform module makes sense. The reasons to do so all come down to expressing an architectural decision in code. Let's look at three decisions that naturally lead to creating Terraform modules.
Combining Resources into a Cohesive Component
Think of a component as a type of module that combines several related resources. To find groups of closely related cohesive resources, ask:
- Are these resources related?
- Do the resources share the same lifecycle?
- Are the resources shared?
In many cases, resources in a subscription have no direct dependencies on each other. So, think about your dependency trees, and consider decomposing the resources into modules along those dependency lines.
Resources that depend on each other and are created and destroyed simultaneously are more likely to be highly cohesive. Suppose my task is to create an Azure App Services component, and our organization policy is to always enable the Azure Monitor diagnostics on each service we make (architectural decision!). The diagnostics depend on the App Service, diagnostics without the App Service are meaningless. So, when we destroy the App Service, we no longer need the diagnostics configuration. Bundle the diagnostics and the App Service into the same component module to enforce our decision that all App Services should have diagnostics. It follows that the diagnostics cleanup is easy and automatic when the App Service is no longer needed.
Likewise, App Services cannot exist without an App Service Plan, but this dependency is different because the App Service Plan is shareable across many services. The plan will likely have a longer lifecycle than any one App Service. Services may come and go, but you won't remove the plan until you remove all the services using it. Shared resources should not go into a component. Instead, create the plan separately and pass the resource identifier to the services as an argument (dependency inversion).
Removing Options
Finding cohesion goes a long way toward grouping resources into the correct abstractions. The next important architecture consideration involves deciding which options you want available to your engineers. If the answer is "all of them," then you don't need a module because the underlying resource already provides all the options. However, if the answer is "all but one," you have a fair case for writing your custom module.
Consider the "minimum TLS" setting on an App Service. As of this writing, the best practice is to require TLS 1.2. However, App Services allow TLS versions less than 1.2 because allowing the option ensures the broadest compatibility across Microsoft’s customers. As an enterprise architect, you don't need to consider compatibility for Microsoft's customers, only your own use case! If TLS 1.2 should be the only option then express this intention by writing a module that has this value hard-coded.
Hard-coding options for security and compliance make it easier for your engineers to get things right the first time, but hard-coding API values are not the only way to remove options. The Azure API will not require you to configure diagnostics because some customers may not want them. However, your organization has decided they want diagnostics, so bundling them in a component removes the option of not deploying diagnostics.
Changing the Shape of a Resource API
The Terraform docs explain the arguments and attributes for a resource. However, this API might be more complicated than needed for our organization's specific use case and we can change it by writing a module.
In an Application Service, we configure the minimum TLS setting within the resource's "site_config" block. There are other values in the site config block we may wish to leave to our developers. For example, our applications may not use a consistent dotnet version—we can provide an argument to let developers specify the needed version. This argument doesn’t need to mirror the underlying option. We can create a string variable on our module and then reference that value in the site config block. In doing so, we have changed the shape of the API by flattening it.
Note that removing options also changes the shape of the API. In the dotnet version example, we only exposed one part of the site config, not all the possible options. I would recommend only surfacing arguments when needed. Then, take the defaults or hardcode something appropriate for the remaining unexposed values.
For the arguments you expose, you can shape how your module consumes them. In some cases, the desire to shape the API may be the only motivation to create a module, and that's OK. Reshaping the API may make your module more straightforward than the underlying resource in the context of your organization.
Conclusion
Adding a custom Terraform Module to your code base creates a level of indirection to your code. When introducing a new layer of indirection, make sure you also introduce a layer of abstraction to justify the work required to make and maintain that module. Over time you will be able to think and talk about cohesive components that make sense in your architecture instead of discussing the resources provided natively by the Azurerm provider. So instead of talking about "I'm going to create 3 VMs and a Load Balancer for this application," you can start to talk about "I'm going to use our cluster module to spin up a cluster for this application." The decisions that go into making a standard cluster will be baked into the cluster module, and you will save time by working at a higher level while also avoiding many common mistakes.
The first step starts with you
Get in touch!
The bigger the challenge, the more excited we get. Don’t be shy, throw it at us. We live to build solutions that work for you now and into the future. Get to know more about who we are, what we do, and the resources on our site. Drop a line and let’s connect.