The Art of Modularization in Terraform

Unlock the secrets of efficient infrastructure management βœ…

Modularization is like playing ‘Lego’

# General 🧩

HashiCorp Terraform has become a go-to choice for “infrastructure as code” (IaC) enthusiasts, providing a powerful and flexible way to define, manage, and provision infrastructure resources, not only on Microsoft Azure! But as your infrastructure codebase grows, keeping it organized, maintainable, and scalable can be a challenge. πŸ˜“

That is where modularization comes into play. In this blog post, I will explain why modularization is crucial in Terraform and show the best practices πŸ”for creating and developing efficient Terraform modules (“module library“).

# Why ‘Modularization’ in Terraform? πŸ“¦

Modularization in Terraform involves breaking down your infrastructure code into smaller, reusable components called ‘modules’. πŸ“¦πŸ“¦πŸ“¦

A module encapsulates specific functionality or resources and allows you to manage your infrastructure in a more organized and standardized manner. There are several compelling reasons to embrace modularization in Terraform:

1. Do one thing and do it as simply as possible 🀏

Modularization encourages the creation of small, self-contained and independently testable modules that each serve a specific purpose. This approach follows the philosophy of “doing one thing and doing it well“. By breaking your infrastructure down into discrete, specialized modules, you enhance clarity, maintainability, and reusability. Keep your modules focused on a single concern. If a module starts doing too much, consider breaking it into smaller, more specialized modules.

πŸ“’ Every single module is a separate repository! ➑️ Do not create ‘monster‘ πŸ‘Ή repos containing multiple modules! 🀒

Furthermore, avoid creating “thin wrappers“, which refers to modules that merely wrap or pass through the arguments and resources directly to underlying resources without providing much abstraction or additional functionality. A Terraform modules that contains only one single resource does not bring any additional value. It is generally recommended to design Terraform modules with a balance of abstraction, encapsulation, and documentation to create reusable and maintainable infrastructure code! πŸ‘

βœ… For example, before start programming a Terraform module to provision Azure Virtual Network (VNet) for Hub & Spoke, do a list which resources should be covered in the module. Ask yourself: “What characterizes an Azure VNet?” and “How the module should look like so it can be reused by several organizations, departments or workloads?“. The following example shows which resources are covered in my VNet module. Of course, yours can look different according to the given requirements.

Azure VNet module in Terraform.

2. Standardized Module Structure πŸ‘·β€β™‚οΈ

The standard module structure for Terraform, described by a file & directory layout, recommends the following elements:

Root module: The primary entry point located in the root directory, setting up the main functionality.
examples: Examples under the ‘examples/‘ subdirectory with READMEs for explanation. Ensure module blocks in examples use external addresses for source, not relative paths. The examples show the consumer how the module can be used.
.editorconfig: helps maintain consistent coding styles and formatting across different editors and IDEs.
.gitignore: To specify files and directories that should be ignored by Git (e.g. the ‘.terraform‘ directory).
.tflint.hcl: Helps to catch potential issues and best practices violations in the Terraform code before it will be applied.
LICENSE: A clear license file is recommended for public module adoption.
TF files: File names for a minimal module include ‘main.tf‘, ‘variables.tf‘ and ‘outputs.tf‘. Recommendation one file per type and resource. Do not use ‘endless’ variable files! Each single variable and output value must have a description!
README.md: Both root and nested modules should have README files for module descriptions. Include a visual diagram if necessary.
versions file: Helps to enforce a specific Terraform version and ensures that the module is compatible with that version.
⚠️ Nested modules: Try to avoid nested module as it gets complex and makes troubleshooting difficult. Focus on allowing users to selectively choose modules rather than creating a ‘deeply nested’ tree. Create a clear flat set of resources within the module to provide a simple Terraform configuration.
Terraform Module Structure

The following link on my GitHub account can be used as a module template. Feel free to use it! πŸ™

Provide a clear and standardized naming conventions for all module before you start programming. Use consistent and descriptive names for the modules and its variables makes it much more recognizable.

3. Provide options, but not too many πŸ—οΈ

Terraform modules should be flexible but not overwhelming. Offering a reasonable number of input/output variables and options gives users the ability to customize their infrastructure while avoiding unnecessary complexity.

βœ… For example, the previous screenshot is the list of resources that the module may create, but do not have to! The module can create zero or more of each of these resources depending on the ‘count‘ value (Link) or the ‘for_each‘ loop (Link). The ‘count‘ value is determined at runtime. The goal is to present the types of resources that may be created. It makes sense to provide the consumers of the Azure VNet module the option, either to create an Azure VNet (count = 1) or use an already existing VNet (count = 0) to configure additional settings on it like subnets, NSGs, route tables, etc.

count‘ meta-argument to add more flexibility.

4. Add value, do more than wrap a resource βž•

A good Terraform module should do more than just wrap a single resource or component. It should add value by simplifying complex configurations, providing sensible defaultvalues, and offering a higher-level abstraction to make infrastructure management easier.

βœ… For example, a module for an Azure Storage Account should provide standard values for the most common infrastructure scenarios, like the following screenshot illustrates. If the variable is not defined in the ‘.tfvars‘ file, the default value will be used automatically. This reduces the number of variables which are needed to define the configuration for the Azure Storage Account and reduces complexity.

Define ‘default‘ values to work with best-practice configurations.

5. Version appropriately πŸ†š

Versioning ensures consistency and allows users to depend on specific module versions. Use semantic versioning (‘SemVer‘) to communicate changes clearly, and always document what each version brings to the table. A version number is typically written as ‘MAJOR.MINOR.PATCH'. When making changes to a module, update the module’s version according to ‘SemVer‘ and use tags on the repository. After updating the version with the new tag (as the following screenshot illustrates), consumers can choose when to upgrade to the new version in the main configurationπŸ•œ.

Terraform Cloud using ‘tags’ on the repo to version modules in the registry.
Terraform upgrade in the main configuration.

6. Create great documentation πŸ“„

Documentation is vital for module adoption. Clear and comprehensive documentation should explain the module’s purpose, usage, input variables, outputs, and any caveats. πŸ“– Well-documented modules are easier for others to understand and use. πŸ’‘ Providing concise descriptions in ‘variables.tfandoutputs.tf‘ for documentation purposes is mandatory; same for the usage of the ‘examples/‘ in the subdirectory.

βœ… The following example, shows a description, defined in the ‘README.md‘ file to know more in detail what the module is about and can be used for.

Simple documentation of a Terraform module.

7. Keep them updated πŸ†•

Regularly update your modules to incorporate new features, bug fixes, and security patches. Keeping modules ‘up-to-date‘ is crucial for maintaining the health and security of your infrastructure. Follow the principal “Don’t get left behind!” with your module versions, using old providers. Keep in mind 🧠 that cloud is an ‘evergreen‘ approach. 🌳

βœ… For example, always keep an eye πŸ‘οΈ on new AzureRM providers. To find the latest information on updates, release schedules, and upcoming releases for the Terraform AzureRM provider, I recommend checking the official sources. Typically, this information is available on the official Terraform AzureRM provider GitHub repository. πŸ“

Terraform AzureRM provider changelog on GitHub.

8. Evangelize & Engage πŸ“’

Promote your Terraform modules within your organization across different departments/subsidiaries or the broader community. 🧊 Teams across an organization can collaborate more effectively when they share a common Terraform library of modules (called ‘registry‘). This collaboration can lead to knowledge sharing, improved workflows, and faster development cycles. Different teams can easily reuse existing, tested modules for common infrastructure patterns. Moreover, a centralized/standardized library allows for the incorporation of best practices and industry standards. Modules can be designed to follow security, compliance, and performance guidelines, ensuring that infrastructure is deployed in a secure and efficient manner (‘governance‘). Furthermore, collaborate with others and conduct code reviews to identify potential issues and improvements. 🏎️

βœ… For example, using a central Terraform library as registry helps organizations manage their infrastructure more efficiently and in accordance with best practices. The following chart below shows the main principal of collaboration within an organization.

How a central Terraform module library (registry) works.

9. Start simple and evolve πŸ”

Begin with a simple, functional module and then evolve it based on feedback and changing requirements. Starting simple allows for rapid iteration and improvement. Regrading modules, you can either strive for feature completion or start with a minimum set of needed arguments:

Feature completion: Involves defining a Terraform module with a comprehensive set of features and options. All possible configuration parameters and settings are included in the module definition. It is suitable when you want to provide a highly flexible and all-encompassing solution for a specific problem domain.
Minimum set: Starting with a minimum set of arguments involves creating a Terraform module with a minimalistic set of required parameters. Additional features and options are added only as needed. It is suitable when you want to provide a straightforward and easy-to-use solution for a specific use case, allowing users to extend functionality as needed (later), during the development process.

10. Make them easy to obtain πŸ›’

Modules should be easy to discover and obtain. Consider using Terraform Module Registries to simplify module distribution and discovery.

βœ… For example, using Terraform Cloud as a central registry for publishing modules is a common and effective approach in ‘infrastructure as code‘ (IaC). The Terraform Cloud provides a centralized location to store, publish and version your Terraform modules, making it easier to collaborate, share, and manage infrastructure code across teams.

Example of using Terraform Cloud as module registry.

11. Testing & Validation: πŸ§ͺ

Thoroughly test your modules to ensure they work as expected in various scenarios. Testing Terraform modules is an essential part of ensuring their reliability and correctness (Link). By combining the following testing approaches, you can create a robust testing strategy for your Terraform modules, helping you catch errors early and ensure the reliability of your infrastructure deployments:

Use a tool like Terratest for unit testing Terraform code. Terratest allows you to write Go code to test your Terraform modules.
Test your modules with different input parameters to cover a variety of scenarios. Parameterized testing helps ensure that your modules are flexible and can handle a range of use cases.
Employ a Terraform linter like tflint to catch syntax errors, stylistic issues, and potential problems in your Terraform code.
Terraform and Terraform Cloud improve developer usability and velocity with a test framework, generated module tests, and stacks.
Consider using tools like terraform-compliance or Goss to perform compliance testing and create mock tests. These tools allow you to define and test against expected outcomes without actually provisioning resources, ensuring that your Terraform code aligns with compliance requirements.
Set up a dedicated Azure test tenant or subscriptions to isolate your tests from production resources.
Integrate your Terraform modules into your CI/CD pipeline to automate testing, deployment, and versioning. Popular CI platforms like Jenkins, GitLab CI, and Travis CI can be configured to run Terraform tests.

# Conclusion 🏁

Terraform modularization is a fundamental technique for managing infrastructure as code (IaC) effectively. By adhering to the best practices outlined in this blog post, you can create and develop Terraform modules that are understandable, reusable, and maintainable. Modularization not only simplifies infrastructure management, but also encourages collaboration and ensures your infrastructure codebase remains robust and adaptable as it grows and evolves.

#HappyCoding 😊 #AzureRocks 🀘#IaC πŸ–₯️

Scroll to Top