Terraform Tips: Scalable and Reliable Infrastructure-as-Code – Part 2
This is the second part of the Terraform series, offering tips and best practices for efficient cloud infrastructure management. In OTTO IT, the powerful open-source tool from Hashicorp is utilized to effectively orchestrate, automate, and manage cloud infrastructure via Infrastructure-as-Code (IaC). After discussing the main phases of the Terraform workflow, the Terraform lifecycle, and Terraform modules in the first part, this section will present specific techniques for optimizing directory structure and resource organization, as well as handling input and output values.
It is critical to plan ahead for infrastructure needs and have a long-term vision for scaling services. Both individual use cases and potential limitations and challenges of Terraform should be considered to ensure scalable and maintainable infrastructure management.
Key questions to ask in this regard include:
However, caution is warranted, as attempting to future-proof the code too early and "over-engineering" can lead to convoluted and hard-to-understand code. This principle applies to both traditional code development and Infrastructure-as-Code. It is important to keep the Terraform structure simple and comprehensible, while creating a flexible foundation for future changes.
As described in part 1 of the Terraform series, cloud API calls are made for the resources during the "terraform plan" and "terraform apply" phases. Instead of defining the entire Infrastructure-as-Code in a single large "monolithic" state file, it is better to keep complexity low and implement manageable state files. State files should be kept small and compact to facilitate dependency management and speed up deployment. Another advantage of smaller state files is that they make testing and reviewing easier.
Since Terraform configurations evolve over time, it is important to implement appropriate structuring measures early on to allow for flexible changes and to maintain the long-term maintainability and scalability of resources. Below, I present an example of a Terraform structure with a modular approach, where the main branch serves as the "source of truth" for all environments. However, it is important to note that this is only an example, and many other approaches and solutions are possible.
In the example shown, the configuration files use a shared directory in which reusable components are defined as "modules". This directory could also be implemented as a standalone repository, allowing it to be used and reused by different applications and teams.
Additionally, the example includes an infrastructure directory, which serves for bootstrapping (e.g. for disaster recovery) and for overarching cloud environments that are not environment specific (e.g. logging, secret manager).
The organization of the Terraform code in the example is divided by environment. At first glance, the code may seem somewhat redundant. However, there are many advantages to this setup, as it ensures loose coupling. Resources can be managed independently and configured in a more granular, environment-specific manner. For instance, if a service needs to be deleted while the database instance remains, this can be easily accomplished through separate state files and folder structures. Similarly, if the live environment requires more processing units than the development environment, this can be easily handled. In addition to separation by development environment, it may also be beneficial to group these components by infrastructure layers (such as networking, database, etc.).
Here’s a quick look at the Terraform files used in the example and their contents:
Another tip is to utilize "terraform graph" and visualization tools for current Terraform configurations to quickly identify areas for improvement in the setup.
In summary, Infrastructure-as-Code should be written and maintained with the same rigor as application code, adhering to best practices and standards. A clear directory structure leads to improved quality, maintainability, and collaboration within the development team.
To maintain a long-term manageable setup, standardization, consistency, and reusability should also be prioritized for input and output values. There are many useful tips and best practices available from the Terraform community, and cloud providers often follow certain conventions that point us in the right direction:
Input variables in Terraform are a powerful tool for making configurations flexible and reusable. However, they can also lead to increased complexity in code, so they should only be used when truly necessary. It’s helpful to ask yourself whether the value of the potential variable really needs to be changeable before implementation. Additionally, consider whether there’s a specific use case, or whether using local values might be a more sensible approach.
Using output values can have many benefits. Child modules can use outputs to pass a subset of resources to the parent module. The root module can utilize outputs to display values, for example, via the CLI. To clarify, the root or parent module is the module that calls other modules (child modules) to include their resources in the infrastructure configuration.
Additionally, outputs can be used by other infrastructure configurations after deployment (in the case of a remote state). To facilitate the use of a module for the user, it is helpful to design the outputs in such a way that clearly indicates which value types and attributes they provide.
In conclusion, it is crucial to consider the specific requirements and constraints of each project. It may also be appropriate to deviate from recommendations and develop custom solutions.
"The trick is to weigh the pros and cons of different approaches and finding the best solution for each situation. As is often the case in IT, there is not one right way, but many different ways."
The next part of our Terraform series will look at with testing Infrastructure-as-Code. Stay tuned! ;-)
Want to be part of the team?
We have received your feedback.