Terraform is a well-known IaC tool from Hashicorp. Terragrunt is a thin wrapper of Terraform that provides extra tools for keeping your configurations DRY, working with multiple Terraform modules, and managing remote states. It is an open source project from Gruntwork, co-founded by Yevgeniy Brikman, author of Terraform up and running.
This article discusses common patterns in Infrastructure as Code (IaC) based on Terraform and Terragrunt examples and summarizes and highlights the design principle underlying good architecture.
We will present these patterns in an abstract way. Although the examples presented in this article use Terraform and Terragrunt, the main concepts apply to other popular IaC tools and platforms like Pulumi, AWS CDK, CloudFormation, Terraform Cloud, Atlantis, Spacelift, and Terraform Cloud alternatives.
To put it simply, “managing collections of infrastructure resources” is an abstract concept. The technologies used to implement this concept include:
Git branch (allows you to create two branches, one called workspace_1, the other workspace_2, then relate remote state to each of the branches, which is actually a neat trick.)
Pattern: Project Layout
In short, to utilize IaC is to describe your infrastructure with code. Coding infrastructure in HCL (Hashicorp Configuration Language) with Terraform is just like coding with any other programming language, like Python or Java. Programming technologies such as linter, IDE, unittest, documentation, and CICD, are all applicable to IaC. Additionally, a project layout is crucial to defining a structure to organize your code, dependencies, configuration files, etc.
One common pattern of the project layout for IaC is much like the following:
The basic principle is to:
- Separate the process of IaC into two phases: development and deployment.
- Separate the deployment environments into prod, qa, and stage, for example.
- Separate the cloud architecture into components according to projects, types of infrastructure, etc. Each component can be stored in a single “gigantic” repository or multiple “micro” repositories (see Pattern: Mono-repo vs Multi-repo).
Separate Deployment and Development Phase
The process of IaC usually consists of two phases: development and deployment (if you are interested in this topic, the GitOps and weaveworks articles are good sources of information).
In the development phase, one writes code to describe and configure the desired infrastructure. In the deployment phase, the written infrastructure code is applied.
The advantages of separation:
- Separation of Concern: developers who write IaC do not need to know the details of the deployment, and can work or test on a new version of the code without influencing the actual deployed infrastructure.
- Flexibility: a well-designed IaC codebase is modularized and parameterized. The same code snippet can be reused and deployed under different use cases in different environments with customized parameters.
Separate Cloud Architecture into Components
Imagine you have an AWS cloud architecture for web services, including different AWS services like ECS, RDS, S3, IAM, SSM, Secret Manager, Cloudwatch, Cloudfront and so on. One option is to pack all of the components into a single Terraform remote state (main.tf). This is fine for quick and dirty prototyping, but not recommended for production.
Always cut your cloud architecture into meaningful components such as:
- Stateful components like database, filesystem, or S3.
- Stateless components like lambda, ECS, or API Gateway.
You can also cut it based on projects or functional areas (e.g. frontend, backend, data warehouse).
The advantages of separation:
- Avoids scanning the whole architecture if a change needs to be made only locally to one resource. One can enter the component folder which includes that resource, make changes there, and apply them. Scanning a fat cloud account can be time consuming on a large amount of cloud resources, even causing unnecessary API throttling.
- Avoids unnecessary touches on databases or other crucial infrastructure to prevent accidental destructive operations.
- Increases readability of the IaC.
Translate the above described project layout into folder structure:
The separation of the development and deployment process is implemented with two git repositories – env/ and modules/. Notice the .git in the folder. Here, Terragrunt.hcl is simply a configuration file for modules specifically in the language of Terragrunt. The separation of cloud architecture components is implemented with subfolders app/ , mysql/, vpc/. The separation of deployment environments is implemented with subfolders prod/ , qa/ , stage/.
Pattern: Don’t Repeat Yourself
One of the principles of software engineering is DRY (don’t repeat yourself). The same applies to IaC. There are at least two kinds of tricks you can use to DRY IaC:
- Configuration inheritance
A Terraform module is a way of creating a template of a cloud pattern, parameterizing, and reusing it. Like any other artifacts, modules can be stored and versioned in the registry. The official Terraform module registry is https://registry.Terraform.io/. You can also build your own private registry.
2) Configuration Inheritance
Configuration inheritance is a feature supported by Terragrunt. The concept is similar to class inheritance in object oriented programming, except that configuration inheritance serves the purpose of configuration reusability.
In the following example (also see the project layout above), we have a configuration file at the global level (global_vars.yaml) and configuration files at the environment level (environment_specific_vars.yaml).
In global_vars.yaml, variables like account_id, region_name and repository_url can be defined, and in environment_specific_vars, these global variables will be inherited and present, additionally defining new environment level variables like vpc_id (usually we use separate vpc for different environments), environment_name, etc.
In summary, configuration inheritance increases the reusability of configuration files and reduces the difficulty in maintenance.
Pattern: Mono-Repo vs Multi-Repo
Last but not least, let’s review the concept of mono-repo and multi-repo. It has been pointed out by Hashicorp as a major architectural decision for IaC projects, a view I share.
The problem is, there are two ways to organize code. In simple terms, mono-repo stores all code into one git repository (for example with multiple subfolders), whereas multi-repo separates them into multiple repositories.
Analogously, the relationship between mono-repo and multi-repo can be compared to monolith software architecture and microservice architecture. Mono-repo is intended to perceive the architecture as a whole and as evolving together. It is usually more suitable for small projects or big projects in prototyping. Multi-repo, on the other hand, facilitates collaboration but with extra overhead. Naturally, the more repos you have, the higher the maintenance cost, but also the more autonomous and self-service each repository is.
In this article, we reviewed several patterns of IaC architecture in a technology-agnostic way. The examples given are based on Terraform and Terragrunt, since they are the most popular open source tools on the market at the moment. The idea, however, is to look at different technologies like pulumi, AWS CDK, cloudformation, etc., where the same patterns keep appearing in slight variations.
We rooted the principles of IaC architecture design in software engineering concepts like inheritance, DRY, project layout, parameterization, and so on. Afterall, Infrastructure as Code is coding infrastructure, and recognizing common patterns is the recommended way to learn and apply IaC.