- 📐 Recommended project architecture
- 🗨️ Context pattern
- 🐗 Terragrunt context proposal
- 🗃️ Files and folder naming
- ⚖️ Pros and cons
A design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design
.
├── environment
│ ├── module.hcl
│ ├── dev
│ │ ├── inputs.hcl
│ │ └── terragrunt.hcl
│ ├── preproduction
│ │ ├── inputs.hcl
│ │ └── terragrunt.hcl
│ └── production
│ ├── inputs.hcl
│ ├── organisation.tf
│ └── terragrunt.hcl
├── application
│ ├── app_1
│ │ ├── module.hcl
| │ ├── dev
| │ │ ├── inputs.hcl
| │ │ └── terragrunt.hcl
| │ ├── preproduction
| │ │ ├── inputs.hcl
| │ │ └── terragrunt.hcl
| │ └── production
| │ ├── inputs.hcl
| │ └── terragrunt.hcl
│ ├── app_2
│ └── app_3
├── common.hcl
└── _settings.hcl
You implemented the WYSIWYG pattern and have instantiated your module multiple times in your layers and are tired of copying them or/and made some mistakes while doing so.
The context pattern for WYSIWYG aims to keep your code DRY (Don’t repeat yourself)! 🌞
Thus all concepts from the WYSIWYG pattern apply to this pattern.
Don’t repeat yourself is the motto of terragrunt. Terragrunt allows you:
- To write your configuration throughout your infrastructure
- To keep your backend config dry
- Enforce separation of duty between your layers
Each terragrunt layer defines one and only one module, but input for this module can be defined throughout the tree structure. Example:
.
├── environment
│ ├── module.hcl # Defines the module used by every subsequent layer
│ └─── dev
│ ├── inputs.hcl # Defines **unique** input for this layer (Ex: the database should be a bit smaller in dev)
│ └── terragrunt.hcl # Defines terragrunt configuration
├── common.hcl # Defines default value to every layer (Ex: tenant id, size of the database)
└── _settings.hcl # Defines required_version of terraform, terraform provider used but not the version and unique identifier for the backend **for every layer**
The context of a layer is all the inputs required for it to be created. Your tree structure, with inputs throughout it, is thus your context.
If your context is DRY, then your tree structure is OK. If you have to repeat an input you might want to refactor your tree structure. Check the refactoring section for details.
Each terragrunt layer defines one and only one module, you can not create a new resource independently without adding it to the module. This will force you to ask the question
Should this new resource really be added to this layer ? Isn’t this resource a new project need ?
Consequently, you can either:
- Integrate it with the current layer module because this configuration will be generalized
- Create a new tree structure because a new project need has been identified
The biggest advantage of terragrunt is that since every layer is a single module thus a single state. When identifying a new projectneed,d you can rearrange you tree structure to match the new view of your project need.
While terragrunt allows you to have a DRY configuration. It also allows you to easily split layer to match project needs This will enforce you to rethink your splitting every time you add a resource / module to a layer.
In Example 1 the use case is that we want to add resources for a new frontend. At this point you have 2 choices :
- If you have to add the frontend to every application and future application then simply add it to the module
- adding a conditional
has_frontend
boolean variable is a temporary solution and should be used with caution, as It may hide a change in your project’s needs
- adding a conditional
- If you have to add the frontend to a specific app and not any other, or if the other will be different, then you have identified a new business need : you should split the layers.
.
├── application
│ ├── module.hcl # Calls the backend module
│ └─── app_1
| ├── dev
│ │ ├── inputs.hcl
│ │ └── terragrunt.hcl
│ └── production
│ ├── inputs.hcl
│ └── terragrunt.hcl
└── _settings.hcl
My application needs a frontend, but only for this one app
.
├── application
│ ├── backend
│ │ ├── module.hcl
│ │ └─── app_1
│ │ ├── dev
│ │ │ ├── inputs.hcl
│ │ │ └── terragrunt.hcl
│ │ └── production
│ │ ├── inputs.hcl
│ │ └── terragrunt.hcl
│ └── frontend
│ ├── module.hcl
│ └─── app_1
│ ├── dev
│ │ ├── inputs.hcl
│ │ └── terragrunt.hcl
│ └── production
│ ├── inputs.hcl
│ └── terragrunt.hcl
└── _settings.hcl
.
├── application
│ ├── module.hcl # Calls the backend module
│ └─── app_1
| ├── dev
│ │ ├── inputs.hcl
│ │ └── terragrunt.hcl
│ └── production
│ ├── inputs.hcl
│ └── terragrunt.hcl
└── _settings.hcl
I’m going to need a Redis for my backend I’ll juste update my module backend and test it by overwriting the call to the module within the layer terragrunt.hcl
- Root folder are named depending on project need (Ex: application, environment)
- Layer folder are named depending on subproject need (Ex: production, dev, app_1)
- Module folder are named after what they define
- The file
input.hcl
defines all the inputs spcefique to the layer - The file
module.hcl
defines the module used by subsequent layers - The file
terragrunt.hcl
defines in every layer the terragrunt configuration of include - The file
common.hcl
defines default value to every layer - Files prefixed with a
_
are created with the generated function
There are a lot of ways to organize your files and include function. Here is a proposition :
- The root
terragrunt.hcl
- Inputs of global parameter (Ex: tenant ID)
- Generate function of
_settings.tf
file with backend configuration and Terraform provider and version
- Every leaf layer
input.hcl
that defines all inputs requiredterragrunt.hcl
describing includes withinput.hcl
, the rootterragrunt.hcl
and if needed the parentmodule.hcl
Pros:
- Very easy to create a new layer (project need)
- Segmentation of feature with module
- Ease of exploration : What you see is what you get (WYSIWYG)
- DRY 🌞
- Enforce best practices when creating a new resource
- Ease of refactoring
Cons:
- Adds a tool to your stack