In this tutorial, we will explain what Terragrunt is, what it is used for, and show how to use it with example commands and configurations. We will discuss example use cases, best practices, and alternatives, along with an installation guide on how to get it set up and get started.
What is Terragrunt?
Terragrunt is a popular open-source tool or 'thin wrapper' developed by Gruntwork, that helps manage Terraform configurations by providing additional features and simplifying workflow. It is often used to address common challenges in Terraform, such as keeping configurations DRY (Don't Repeat Yourself), managing remote state, handling multiple environments, and executing custom code before or after running Terraform.
Terragrunt is a project that is actively developed, with new features being added all the time.
See Terragrunt vs. Terraform comparison.
Terragrunt features
The top useful features of Terragrunt:
1. Remote state management
Terragrunt simplifies remote state management for Terraform projects. It can automatically configure and store state files remotely in services like Amazon S3, Google Cloud Storage, or any other backend supported by Terraform.
2. DRY (Don't Repeat Yourself) configurations
Terragrunt promotes DRY principles by allowing you to define and reuse common configurations across multiple Terraform modules. This helps reduce duplication and makes configurations more maintainable.
3. Dependency management
Terragrunt supports dependency management between different Terraform modules and states, ensuring that dependent resources are deployed in the correct order.
4. Configuration inheritance
Terragrunt allows you to create modular configurations that can inherit parameters and settings from parent configurations, making it easier to manage and organize your infrastructure code.
5. Environment-specific configurations
Terragrunt supports the creation of environment-specific configurations (e.g., dev, staging, prod) using HCL (HashiCorp Configuration Language) interpolation, making it easier to maintain consistent environments.
6. Remote backend configurations
Terragrunt allows you to specify backend configurations (e.g., S3 bucket, DynamoDB table) for each environment, enabling a more dynamic and flexible approach to state storage.
7. Locking mechanism
Terragrunt provides a locking mechanism to prevent concurrent executions that could potentially cause conflicts when modifying shared infrastructure.
8. Secrets management
Terragrunt can integrate with external secrets management tools like AWS Secrets Manager or HashiCorp Vault to handle sensitive data securely.
9. Integration with CI/CD pipelines
Terragrunt can be integrated into continuous integration and continuous deployment (CI/CD) pipelines to automate infrastructure deployments.
10. Configurable hooks
Terragrunt supports pre- and post-terraform hooks, allowing you to run custom scripts or commands before or after running Terraform commands.
How does Terragrunt work?
Terragrunt relies on a configuration file called terragrunt.hcl
. This file is placed in the root directory of your Terraform project or in the directories of specific modules. It contains settings and parameters that customize Terragrunt's behavior for your project or module.
How to install Terragrunt
To install Terragrunt, follow the steps below.
Step 1: Install Terraform
As Terragrunt is a wrapper around Terraform, you'll need to have Terraform installed first. You can download the appropriate version of Terraform for your operating system here.
Step 2: Extract the binary and place it in a directory included in your system's PATH
After downloading Terraform, extract the binary and place it in a directory included in your system's PATH
.
The PATH tells a system where it should look for executables, making them accessible via command-line interfaces or scripts.
To add a new folder to PATH in Windows, navigate to Advanced System Settings > Environment Variables, select PATH, click "Edit" and then "New."
Step 3: Download Terragrunt
Next, head over to the Terragrunt GitHub page to download it.
Step 4: Place the Terragrunt binary in a directory included in your system's PATH
Once you have downloaded the Terragrunt binary, place it in a directory included in your system's PATH
. You may also rename the binary to simply terragrunt
(without the platform-specific suffix) for convenience.
Step 5: Verify the installation
Lastly, verify the installation by running terragrunt --version
on your console command line. It should show the currently installed version.
Terragrunt basic commands
Terragrunt command should be run from the project directory that contains your terragrunt.hcl
configuration file. Terragrunt has many of the same commands available you will be familiar with the Terraform workflow, (you just need to replace terraform
with terragrunt
).
These include:
-
terragrunt init
-
terragrunt validate
-
terragrunt plan
-
terragrunt apply
-
terragrunt destroy
-
terragrunt graph
-
terragrunt state
-
terragrunt version
-
terragrunt output
Also, check out this Terraform cheat sheet.
How to set up Terragrunt configurations
First, create your terragrunt.hcl
file in the directory you want to use Terragrunt in. The terragrunt.hcl
file consists of configuration blocks that define various settings for Terragrunt.
Note that the Terragrunt configuration file uses the same HCL syntax as Terraform itself in terragrunt.hcl
. Terragrunt also supports JSON-serialized HCL in a terragrunt.hcl.json
file: where terragrunt.hcl
is mentioned, you can always use terragrunt.hcl.json
instead.
The terraform
block is used to configure how Terragrunt will interact with Terraform. You can configure things like before and after hooks for indicating custom commands to run before and after each terraform call or what CLI args to pass in for each command.
The source attribute specifies where to find Terraform configuration files and uses the same syntax as the Terraform module source attribute.
For example, you can pull modules directly from a Github repo:
terraform {
source = "git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1"
}
Or modules from the local file system (Terragrunt will make a copy of the source folder in the Terragrunt working directory, typically .terragrunt-cache
):
terraform {
source = "../modules/networking/vpc"
}
Or modules from the Terraform registry using the tfr
protocol (tfr:/// is shorthand for accessing modules in the public registry):
terraform {
source = "tfr:///terraform-aws-modules/vpc/aws?version=3.5.0"
}
If you wish to access a private module registry (e.g., Terraform Cloud/Enterprise), you can provide the authentication to Terragrunt as an environment variable with the key TG_TF_REGISTRY_TOKEN
. This token can be any registry API token.
The source can then be specified in the format:
tfr://REGISTRY_HOST/MODULE_SOURCE?version=VERSION.
Other options for the terraform
block:
-
include_in_copy
(attribute): A list of glob patterns (e.g.,["*.txt"]
) that should always be copied into the Terraform working directory. -
extra_arguments
(block): Nested blocks used to specify extra CLI arguments to pass to theterraform
CLI.
For example, the below block configures a lock timeout of 20 minutes for any Terraform commands that use locking.
extra_arguments "retry_lock" {
commands = get_terraform_commands_that_need_locking()
arguments = ["-lock-timeout=20m"]
}
-
before_hook
(block): Nested blocks used to specify command hooks that should be run beforeterraform
is called. -
after_hook
(block): Nested blocks used to specify command hooks that should be run afterterraform
is called. -
error_hook
(block): Nested blocks used to specify command hooks that run when an error is thrown.
Other blocks you can configure in your terraform.hcl
file include:
Check the docs link for each for more information. For our example, we will only need to specify the source so Terraform knows where to find the modules required.
💡 You might also like:
- Managing Multiple Terraform Environments Efficiently
- Terraform with Azure DevOps CI/CD Pipelines
- How to Manage Terraform with GitHub Actions
Terragrunt use cases
In this section, we will take a look at the common use cases for using Terragrunt with some examples, and detailed explanations for each.
Example 1: Keeping remote state configuration DRY
Using Terragrunt, you can keep your remote state configuration DRY (Don't Repeat Yourself) by defining it in a separate Terragrunt configuration file and inheriting it across different environments or projects.
In the following example, we will define the remote state configuration once in the terragrunt/
directory and inherit it in the my-vm-module/
directory. This approach allows you to maintain consistent state management across multiple environments or projects while avoiding duplication of configuration settings.
Here, we have some files in the following folder structure:
my-vm-module/
├── terragrunt.hcl
├── main.tf
└── variables.tf
terragrunt/
├── terragrunt.hcl
In the terragrunt/
directory, create a terragrunt.hcl
file to define the remote state configuration:
terraform {
# Backend configurations for storing state remotely
backend "azurerm" {
resource_group_name = "my-terraform-rg"
storage_account_name = "mytfstatestorage"
container_name = "tfstatecontainer"
key = "my-vm-module.tfstate"
}
}
In the my-vm-module/
directory, create the terragrunt.hcl
file to inherit the remote state configuration from the terragrunt/
directory:
terraform {
# Include the remote state configuration from the terragrunt/ directory
source = "../terragrunt"
}
locals {
# Azure region where the VM will be deployed
region = "UK South"
}
The main.tf
file in the my-vm-module/
directory will define the Azure VM:
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "my-terraform-rg"
location = local.region
}
resource "azurerm_virtual_network" "example" {
name = "my-virtual-network"
location = local.region
resource_group_name = azurerm_resource_group.example.name
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "example" {
name = "my-subnet"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_network_interface" "example" {
name = "my-nic"
location = local.region
resource_group_name = azurerm_resource_group.example.name
ip_configuration {
name = "my-nic-config"
subnet_id = azurerm_subnet.example.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_virtual_machine" "example" {
name = "my-vm"
location = local.region
resource_group_name = azurerm_resource_group.example.name
network_interface_ids = [azurerm_network_interface.example.id]
vm_size = "Standard_DS1_v2"
delete_os_disk_on_termination = true
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
storage_os_disk {
name = "osdisk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
os_profile {
computer_name = "myvm"
admin_username = "myadminuser"
admin_password = "P@ssw0rd1234"
}
os_profile_linux_config {
disable_password_authentication = false
}
tags = {
environment = "dev"
}
}
Initialize and apply the infrastructure in the my-vm-module/
directory using the Terrgrunt commands:
# Navigate to the my-vm-module directory and deploy the infrastructure
cd my-vm-module
terragrunt init
terragrunt apply
Example 2: Keeping Terraform CLI arguments DRY
Terragrunt provides a way to keep Terraform CLI arguments DRY by defining them in a single location and inheriting them across multiple environments or configurations.
In this example, we will define common Terraform CLI arguments (e.g., auto-approve
, var-file
) in the root terragrunt.hcl
file and inherit them in each environment or module.
Consider the following file and folder structure:
my-vm-module/
├── terragrunt.hcl
├── main.tf
└── variables.tf
terragrunt.hcl
We define our CLI commands in the extra_arguments
block of our terraform.hcl
file:
# terragrunt.hcl
terraform {
# Specify the Terraform version constraint (optional)
required_version = ">= 0.14.0"
# Backend configurations for storing state remotely
backend "azurerm" {
resource_group_name = "my-terraform-rg"
storage_account_name = "mytfstatestorage"
container_name = "tfstatecontainer"
key = "my-vm-module.tfstate"
}
# Common Terraform CLI arguments
extra_arguments "common" {
commands = [
"auto-approve",
"var-file=common.tfvars",
]
}
}
Inside the my-vm-module/
directory, create the terragrunt.hcl
file to inherit the common Terraform CLI arguments using the include
block to specify the inheritance of Terragrunt configuration files.
terraform {
# Include the common Terraform CLI arguments from the root terragrunt.hcl file
include {
path = find_in_parent_folders()
}
# Other module-specific configurations
}
locals {
# Azure region where the VM will be deployed
region = "UK South"
}
Example 3: Keeping Terraform configuration DRY
In this example, we will show how to share local values centrally, reducing duplication.
Consider we have the following file and folder structure:
my-vm-module/
├── terragrunt.hcl
├── main.tf
└── variables.tf
common/
├── terragrunt.hcl
In the common/
directory, create the terragrunt.hcl
file to define common configurations in the locals
block:
terraform {
# Specify the Terraform version constraint (optional)
required_version = ">= 0.14.0"
# Backend configurations for storing state remotely
backend "azurerm" {
resource_group_name = "my-terraform-rg"
storage_account_name = "mytfstatestorage"
container_name = "tfstatecontainer"
key = "my-vm-module.tfstate"
}
}
locals {
# Azure region where the VM will be deployed
region = "UK South"
}
Again we create the terragrunt.hcl
file to inherit the common configurations inside the my-vm-module/
directory:
terraform {
# Include the common configurations from the common/ directory
include {
path = "../common"
}
}
# Other module-specific configurations
The main.tf file that defines the Azure VM configuration can then reference the values in the locals
block:
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "my-terraform-rg"
location = local.region
}
# Other resources and configurations for the VM
Example 4: Running multiple modules at once
To run multiple modules at once using Terragrunt, you can use therun-all apply
, run-all plan
or run-all destroy
commands.
Consider your file and folder structure looks like this:
terraform-root/
├── module1/
│ ├── terragrunt.hcl
│ ├── main.tf
│ └── variables.tf
├── module2/
│ ├── terragrunt.hcl
│ ├── main.tf
│ └── variables.tf
└── terragrunt.hcl
Inside the terraform-root/
directory, create the terragrunt.hcl
file. This file will include the configurations for running multiple modules:
# terraform-root/terragrunt.hcl
terraform {
# Specify the Terraform version constraint (optional)
required_version = ">= 0.14.0"
}
# Include modules using the "terraform" block
include {
path = "./module1"
}
include {
path = "./module2"
}
When you run the appropriate run-all
command they will run the respective Terraform commands for each module in the specified directory (terraform-root/
) and its subdirectories, effectively applying, planning, or destroying resources across all modules at once.
Terragrunt benefits
Where Terraform allows you the freedom to structure your code in multiple ways, Terragrunt places restraints on how you can organize your Terraform code and forces you to use directory structure hierarchies and shared variable definition files to organize your code. These restraints force your code to be more consistent and make it harder to make mistakes. The trade-off is that the amount of flexibility you have is reduced.
The key to using Terragrunt effectively is to carefully plan your directory structure in order to keep your code base DRY. Organizing your infrastructure code into reusable modules that represent logical components of your infrastructure is one way to achieve this.
Terragrunt best practices
Aside from keeping code DRY and modularizing your code, best practices for Terragrunt use really depend on making full use of its available features.
- Create separate directories for different environments (e.g., dev, staging, production) and use Terragrunt to manage each environment's specific configurations. This helps maintain isolation between environments and allows you to apply changes independently.
- Utilize remote state storage for your Terraform configurations to ensure secure and centralized storage. Terragrunt supports various backends like Amazon S3, Azure Blob Storage, or HashiCorp Terraform Cloud.
- Use consistent naming conventions for resources to ensure clarity and prevent naming conflicts. Standardizing naming conventions can improve readability and make collaboration easier.
- Leverage variable files (e.g.,
.tfvars
files) to store environment-specific information. - Leverage secrets management solutions like Hashicorp Vault to keep sensitive information out of version control.
- Use Terragrunt's
dependency
blocks to manage module dependencies explicitly. This ensures that modules are applied in the correct order to avoid errors. This can add complexity, so use it with caution. - Specify version constraints for Terraform and Terragrunt to ensure compatibility and avoid unexpected behavior when updating to newer versions.
- Adopt a GitOps workflow where infrastructure changes are made through code changes in version-controlled repositories. This helps with versioning, collaboration, and rollbacks.
- Incorporate Terragrunt and Terraform into your CI/CD pipeline to automate infrastructure deployments and validate changes before they are applied.
- Write scripts or use automation tools to execute Terragrunt commands, reducing human error and streamlining the workflow.
- Keep detailed documentation for your Terraform modules, Terragrunt configurations, and infrastructure architecture. This helps onboard new team members and ensures a clear understanding of your infrastructure.
- Enforce code reviews for Terragrunt changes to catch potential issues.
Terragrunt drawbacks and alternatives
While Terragrunt offers many benefits detailed in this article, it also adds an additional layer of complexity to your infrastructure management and may require more initial setup. It is also another tool to manage and doesn't work with Terraform Cloud if you use that. You will need to educate and train your team on the use of Terragrunt, which will create additional costs.
You may consider using 'pure' Terraform to be sufficient for your projects, as it will support many of the features natively, which will not be as feature-rich as Terragrunt, but may suffice, (such as multiple workspaces / remote state etc.).
Terraspace is a fully-fledged framework for Terraform which offers further benefits over Terragrunt, including the removal of duplicated terraform.hcl
files further making your code base DRY. It provides structure to your deployment, in Terragrunt this needs to be carefully planned to fully reap its benefits. Terraspace can also automatically create backend buckets for remote state management.
Using Terragrunt with Spacelift
Check out also how Spacelift makes it easy to work with Terraform and Terragrunt. If you need any help managing your Terraform infrastructure, building more complex workflows based on Terraform, and managing AWS credentials per run, instead of using a static pair on your local machine, Spacelift is a fantastic tool for this. It supports Git workflows, policy as code, programmatic configuration, context sharing, drift detection, and many more great features right out of the box.
Key points
Terragrunt is a powerful tool that helps you manage Terraform configurations more efficiently. To make the most out of Terragrunt and maintain a clean, scalable, and organized infrastructure codebase, be sure to follow the best practices and plan your folder structure and use of Terragrunt carefully.
Written by Jack Roper