Helpful HashiCorp Links For Resilient Code
- Standard Module Structure: This is a HashiCorp guide on how to organize your Terraform modules in a consistent and readable way. It also explains how Terraform tooling can use the standard module structure to generate documentation, index modules for the module registry, and more.
- Module Composition: This is a HashiCorp guide on how to use multiple modules as building blocks to create larger systems. It also discusses some composition patterns and best practices, such as dependency inversion, module encapsulation, and module abstraction.
- Module Creation – Recommended Pattern or Module Creation – Recommended Pattern: These are two versions of the same HashiCorp guide on how to write composable, sharable, and reusable infrastructure modules. They cover some module architecture principles, such as input and output design, naming conventions, and testing strategies.
- Modules Overview: This is a HashiCorp tutorial on how to use and create modules in Terraform. It covers the basics of modules, such as what they are, why they are useful, and how to use them in your configuration.
Overview
At some point, you’re going to find a good reason to depart from the official or unofficial Terraform modules available in public module repositories. This will become especially important if multiple teams will be expected to use the same module, and those modules need to be strictly controlled, versioned, and maintained. We’ll discuss the potential reasons for this decision but know that building your modules without proper standards and testing can lead to messy, unmanageable, and error-prone codebases.
You’re going to want a well-defined strategy, a standardized module framework, unit testing, code linting, and versioning strategy. This is your guide.
A note about language: In the documentation below, we’ll refer to officially-hosted modules, publicly-available modules, public repository modules and/or third-party modules interchangeably. These terms all refer to modules that are not owned or maintained by you or your team. Likewise, we also refer to your private module registry, or owned modules interchangeably. These refer to modules that you own and maintain and are responsible for.
Owning & Maintaining Your Own Terraform Modules
There’s a few reasons why you might want to do something crazy like owning your own Terraform modules. The decision is usually done to customize a module or modules to meet specific requirements or standards that cannot be achieved by using an original or officially hosted module. Here are the typical reasons:
Customization
The ability to customize a module to meet specific requirements or standards that cannot be achieved by using the original module.
Hard-coded service configurations in officially-hosted modules may conflict with your organization’s standards and provide limited user customization. Owning your own modules gives you greater flexibility in customizing that infrastructure configuration.
Flexibility
This is the ability to change the module to suit your needs over time, without having to rely on a third-party provider or new version release of the officially-hosted module. At some point, you may need to make an important change to a module that you are currently sourcing from a public repository. When this happens, it may be a situation where owning the module beforehand could mean the difference between a 2 day change cycle vs. a 2 week sprint change cycle to perform the entire clone and own and change. In other words, owning a module early on that you suspect may need updates provides the ability to make changes quickly thereafter without a lot of up front work.
Consistency and Usability
Your organization may want the ability to ensure consistency in the module’s design, naming conventions, and output interface, which can help simplify the integration of multiple modules. Also, you may want a consistent experience for users for Terraform modules across platforms. This means standardizing trainings, documentation, and communications across modules and platforms, which, if done right, can improve user experiences across the board.
Improved visibility
Owning your own modules allows you the ability to better understand and manage the resources created by the module, as well as any dependencies or interactions with other resources.
Validated Testing in Your Specific Environment
Your organization’s cloud platform organizations may have significant requirements for users who are deploying to those ecosystems such as complicated or non-intuitive networking configurations, org policies, naming conventions, and other standards that may require very specific and unique configurations for users. Building modules to these specifications will help facilitate cloud deployments.
The Challenges of Public Repository-Sourced Modules
Complex Module Nesting: Wrapping a parent module around third-party-hosted modules often leads to an complex module nesting hierarchy. This can result in unanticipated IaC behavior due to the many layers of logic across modules. For optimal code, enhanced readability, and reduced intricacy, it’s recommended to flatten module nesting as much as possible within IaC.
Lack of Variable Validation: Ensuring the appropriate validation of user inputs is vital for the successful deployment of infrastructure. The absence of variable validation in third-party-hosted modules may lead to unforeseen errors during the apply phase, without any prior alerts. Additionally, these modules might lack clear input and output descriptions, further complicating matters for users.
Limited Customization: Rigid service configurations in third-party-hosted modules may not align with organizational standards, thus potentially curbing user customization options. To address this, there’s a growing trend towards adopting and managing these modules directly, allowing for greater infrastructure customization.
Inconsistent and Incompatible Modules: Third-party modules may present inconsistencies in inputs, outputs, and naming conventions, posing challenges in maintaining a unified approach to module utilization.
Restricted Output Interaction: Limited outputs from third-party-hosted modules might hinder the capability to expand the use of individual modules and interact with others. This constraint can minimize potential advantages for end-users. Simply put, if an output is not available from a third-party module, it can’t be accessed unless by a separate data source (which maintains an entry in space in your state file) or a change and version update of the publicly available module.
The Big Decision: Downsides of Maintaining Your Own Modules
I want to be clear: it is a significant responsibility to take on when deciding to own one or more Terraform modules. It’s a decision not to be taken lightly. Here are some downsides of maintaining your own private module registry.
Issue Tracking and Updates: Owning your own Terraform modules means taking on the responsibility of tracking, addressing, and resolving any issues or bugs that arise. Without a third-party provider’s dedicated support, this can lead to potential bottlenecks and delays in addressing critical infrastructure issues.
Determining Breaking Changes: Identifying and managing breaking changes becomes a challenge. While third-party providers often release detailed changelogs and documentation with their updates, managing internal modules requires extra diligence to ensure that changes don’t inadvertently disrupt existing infrastructure.
Additional Overhead for Versioning: Version management is essential for infrastructure reliability. Owning modules means you’re in charge of versioning, which adds an extra layer of complexity, especially when ensuring backward compatibility or planning future releases.
Dependencies from Other Teams: When owning and managing your own Terraform modules, there’s the potential for becoming a single point of dependency. Other teams, especially those unfamiliar with Terraform, module structure, strategy, and versioning methods, might overly rely on one team for all updates, enhancements, and fixes. This centralization can create bottlenecks, increase pressure on the managing team, and reduce the agility of the organization as a whole.
Keeping Up with Provider Updates: Cloud providers continuously evolve and update their services. Without the backing of a third-party module provider who is dedicated to updating modules in tandem with these changes, there’s an added burden to ensure your custom modules remain compatible and up-to-date with the latest offerings.
Avoid Creating Wrappers Around Third-Party Modules
Wrapping your own module(s) around publicly available modules is absolutely a valid approach to the issues we’ve surfaced above. However, it is highly recommended that you not take that approach if you can avoid it.
Utilizing nested module blocks can turn a straightforward configuration into a highly layered hierarchy where each module might have its own resources and possibly additional sub-modules, potentially leading to a complicated web of resource configurations. Our goal is, whenever possible, to streamline module structure to just one level of sub-modules if at all possible.
Why Do We Advocate for a Simplified/Flattened Module Structure?
Transparency: A flattened structure ensures clear visibility into module resource creation. This prevents “hidden” actions in modules, ensuring users understand the complete resource picture and expected behaviors.
Stability: Fewer layers reduce the chances of unexpected changes, version updates, or breaking changes in a sub-module leading to deployment failures.
Usability: A straightforward structure means modules are more intuitive to use and adjust. There’s no need to navigate through a convoluted module pathway.
Tracking Errors in Highly-Layered Terraform Configurations
If you haven’t already, you will experience a time where you are helping a team track down a Terraform error. The difference between a flattened module structure and a highly layered or highly nested module structure is wildly evident in the error messages that are returned for both types of configurations. I’ve provided 2 example errors below to illustrate the complexity of tracking an error in both.
Flattened Structure
Error: InvalidAMIID.NotFound: The image id '[ami-incorrect]' does not exist
on resource "aws_instance.example" at line 6, in configuration.tf:
6: ami = "ami-incorrect"
Layered/Nested Structure
Error: InvalidAMIID.NotFound: The image id '[ami-wrongvalue]' does not exist
on module.compute_module.aws_instance.nested_example at modules/compute_module/main.tf line 2:
2: ami = "ami-wrongvalue"
There goes your afternoon.
Creating a Standard Module Structure
When owning your own modules, you’re 100% going to want your own standard module structure.
The Benefits of a Standard Module Structure
Uniformity: With a uniform set of configuration templates and workflows, every module adheres to a predictable design and structure. This uniformity fosters ease of maintenance across multiple modules.
Efficiency: A templated repository acts as an initial blueprint for crafting new modules, cutting down on the setup time and effort.
Reliability: Beginning with a template that includes preset testing and integrations ensures modules are robust and standardized from the get-go.
Simplicity: A standard module repository template streamlines the module creation process, encapsulating some of the intricate details.
Collaborative Spirit: A shared template repository across teams maintaining modules encourages teamwork in module development and maintenance, promoting the exchange of insights and best practices.
Structured Management: Centralizing module codes in a single repository facilitates a more organized approach to overseeing updates and modifications.
Best Practices Around Standard Module Structure
You’ll want a standard structure for every module. This includes things like the following:
Module Repository Template Structure
Using a pre-defined repository template can significantly streamline the module creation process, ensuring consistency across different modules. Here’s a link on how to make one. Github: Creating a Template Repository
Team Access and Permissions: Differentiate between contributors and read-only users to maintain security and control over your modules.
Workflow Standards: Opt for established reusable workflows that include the following:
- Versioning: Track and manage various versions of your module effectively. We talk more about this later in the post
- Testing: Ensure each module undergoes testing to identify potential issues – we talk about this more later as well.
- Issue Management: Centralize and streamline issue tracking for more effective management.
Documentation Standards: A consistent and clear README.MD
is crucial for user understanding and ease of module implementation.
File Structure: Organize your module files in a way that enhances navigation and clarity, and should be consistent with publicly available modules for cloud platforms.
Consistent Version Constraints: Ensure compatibility and smooth performance by specifying supported terraform versions and provider versions. Opt for pessimistic version constraints with regards to provider versions. With Terraform versions, be aware that modules made with newer Terraform features, such as the optional variable feature, will not be able to be used by users employing an earlier Terraform version. Updating Terraform within the 1.x version range should not introduce breaking changes, but if a team is using another module in their configuration that is locked to an earlier Terraform module than the one employing newer features, there will be pain.
Automating Module Repository Creation
The Terraforming/automation of module repository creation is crucial to maintaining a well-structure module strategy.
Module Repository Naming Standards: Absolutely make sure this jives with Terraform Cloud’s PMR naming standards as well as Github’s naming standards. Here’s a link: Publishing Private Modules.
Make use of Terraform and the Github provider to automate the creation of new module repositories, ensuring consistent settings and quick setup.
YAML “Vending Machine” Structure: This approach allows for quick and consistent module additions. Users can simply add a new YAML file, for example, in a data/modules_repos
directory, to introduce a new module.
Example Module Repository Creation Automation Terraform Working Directory
terraform-repo-automation/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│ ├── repo_creation/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
└── data/
└── module_repos/
├── network_module.yaml
├── compute_module.yaml
└── storage_module.yaml
Sample Terraform main.tf
Configuration
locals {
module_repos = fileset("./data/module_repos", "*.yaml")
module_data = [for f in local.module_repos : yamldecode(file(f))]
}
module "repo_creation" {
source = "./modules/repo_creation"
for_each = { for repo in local.module_data : repo.name => repo }
name = each.value.name
description = each.value.description
team_access = each.value.team_access
branch_protections = each.value.branch_protections
}
Sample YAML file (tf-awd-networking.yaml
)
name: "tf-aws-networking-module"
description: "Module to set up networking resources"
team_access:
- user: "teamA"
permission: "write"
- user: "teamB"
permission: "read"
branch_protections:
main:
required_status_checks:
- "test"
required_reviewers: 1