19. May 2023 By Manuel Tilgner
Terraform for enterprise projects
A template for complex Terraform projects
Writing Terraform code is simple. Managing it can be tricky.
That is how I would sum up my experience with Terraform after three years of using it on the job. Truth be told, writing good Terraform code can be tricky as well. But the bigger challenge in my view is organizing the code in a way that is clean, accessible, and maintainable.
This is especially true for what I call enterprise projects. These are large projects that span multiple accounts, regions, services, even clouds. Such projects quickly grow in size and complexity. If you find yourself in such a project, you can shoot yourself in the foot if you do not think about your Terraform project structure carefully.
I decided to write up this post to share some lessons I learned after being involved in several enterprise projects. What I outline below is a Terraform project structure that has proven useful as a starting point for organizing code and config files. My hope is that it provides orientation for developers seeking to professionalize their Terraform code, especially those just starting their journey with Infrastructure as Code.
A way to structure Terraform projects
The first step towards building clean, accessible, and maintainable Terraform projects is to use modules.
Modules are reusable configurations of Terraform code. As such, modules represent units of infrastructure like servers, databases, or managed services. To get the most benefit out of them, modules should be as focused and generic as possible. Modules often correspond to a specific service such as GCP BigQuery or Azure SQL Server.
In our sample repository (https://github.com/tilgner/enterprise-terraform), there is a folder called modules which is subdivided by cloud provider, in our case azure and gcp. Inside these subfolders, you will find the generic Terraform modules for each provider.
Every module consists of exactly three files: main.tf, variables.tf and outputs.tf. I recommend sticking to this convention even if your module has no outputs for instance. In that case, just include an empty file.
Modules are so fundamental because they help us break down our infrastructure configuration into standardized buildings blocks that we reuse across different projects or deployments. Moreover, they can abstract away a lot of the complexity of setting up infrastructure. Irrespective of whether you are building modules for your own team or others, you can tailor them exactly to the needs of the module consumers.
The point of this section is a simple one: By following a consistent modular approach in writing Terraform code, you will build Terraform projects that are much cleaner as you avoid code duplication and abstract code over different requirements.
Another step towards building clean, accessible, and maintainable Terraform projects comes from the way you configure modules.
There are many ways to configure Terraform modules. You could hardcode config values in Terraform code directly or organize them in tfvar, YAML or JSON files. I recommend YAML files because they have three big advantages:
- They are easy to read which means that even team members not familiar with Terraform can quickly understand and update them.
- They bundle all relevant configuration in a single, fixed place. This means you do not have to hunt around in the Terraform code for information. Also unlike with JSON files you can include comments and instructions.
- Terraform detects any changes you make to them. Need to add another dataset, user, or label? It is as simple as adding another block to your YAML. Anyone can do it and do it fast. Terraform rolls out the changes without someone having to touch the underlying code.
When you peer into the config folder, you will notice that it is subdivided by cloud provider and module. Just like the folder modules! So, each generic Terraform module has its corresponding set of config files for different clouds and environments. For example, there is a cloud-storage Terraform module that has a corresponding configuration file in the gcp cloud-storage config folder. That goes back to modules being the central unit of organization.
Here is how a typical YAML file might look like. This one rolls out two resource groups in our Azure production environment:
By using YAML files rather than tfvar files or hard-coded values, you will do yourself and others a big favor by creating Terraform projects that are much more accessible to both technical and non-technical collaborators.
Let us talk about the ultimate step for creating clean, accessible and maintainable Terraform projects: Terragrunt. According to the website (https://terragrunt.gruntwork.io), Terragrunt "...is a thin wrapper that provides extra tools for keeping your configurations DRY [Don't Repeat Yourself], working with multiple Terraform modules, and managing remote state."
As far as we are concerned, Terragrunt helps us create leaner Terraform projects by drastically cutting down the number of files you have to manage. This not only reduces clutter but also the risk of introducing configuration drift in your environments by not updating all Terraform files in a consistent way. It is free to use, open source and well documented.
Some of the things Terragrunt does for us include:
- dynamically generating a backend config.
- dynamically generating a provider config.
- automatically completing Terraform CLI arguments.
- defining a module once and reusing it everywhere without duplicating code.
If you are managing three buckets and a VM, you do not have to bother using Terragrunt. But if you start adding more and more modules (cloud services) to your stack, Terragrunt will prove its worth. Where you really see it pay off is when your Terraform code goes multi-environment, multi-region, or multi-cloud.
There are three kinds of Terragrunt files that I use in this project structure. By ‘kind’, I mean that these Terragrunt files live at different levels of the folder hierarchy and thus serve different purposes. Together, they spare you a load of duplicated code and help streamline the way you configure, deploy, and manage your infrastructure.
The root Terragrunt file
Most important is the terragrunt.hcl in the cloud-provider folder or the root Terragrunt file. In it, you can centrally specify the Terraform providers you want to use, their versions and your remote backend. Then, each time to you deploy a module, Terragrunt dynamically creates the corresponding Terraform files and places them in the directory from which you run the code. This spares you the annoying and error-prone task of creating these boilerplate files again and again.
The terragrunt.hcl also allows you to set global variables that are valid across all environments and modules, e.g., the tag managed_by = terraform or location = westeurope. This is great for defining variables once and then using them everywhere.
The env Terragrunt file
Aside from the the root terragrunt.hcl, there is an env.hcl in each environment folder, i.e., dev, tst and prd. Here, you can set variables that apply only at the environment level and are shared across modules therein. They are great for specifying project or subscription IDs, name prefixes or environment-specific settings such as compute and network options.
The module Terragrunt file
Ultimately, there is the terragrunt.hcl within the individual modules. This is the file that marries the generic Terraform modules with their respective config files to create a real-world instantiation of the module in your cloud environment. Here, it is helpful to think of Terraform modules as callable functions with distinct arguments and the config files as corresponding argument values. Each module terragrunt.hcl in this analogy is the place where the function (the generic module) gets called with the argument values (config files) to produce an output (infrastructure).
Another stellar feature of the terragrunt.hcl files is that they allow you to declare dependencies between modules. This not only makes it transparent as to how modules relate to each other, but it also makes it much easier to join modules together.
Terragrunt is a powerful tool that helps you create Terraform projects which are much more easily maintainable. This is especially true once your project grows in size and complexity. While it might take some getting used to, you will notice that Terragrunt will make your Infrastructure as Code ventures all around better and easier.
As Terraform projects grow, they have a bad habit of getting messy. That is because it simply gets harder to manage all the code and its configuration. To keep an overview of your infrastructure and manage it in an effective, flexible, and consistent manner, you need a project structure that you can rely on. Depending on the size and complexity of your project, this is no trivial task since the decisions that you make come with trade-offs that may turn around and bite you later.
This article outlined a project structure that has served me and my colleagues well in managing these types of projects, which often occur in an enterprise project. I invite you to take away what you like from this post and make it your own, for each project setup is unique in some way. Do check out the sample repo under https://github.com/tilgner/enterprise-terraform and let me know your experiences and suggestions! You can reach me by e-mail: email@example.com
You can find more exciting blog posts from the adesso world in our previously published blog posts.