Solving the DevOps Infrastructure Dilemma: Enabling developer velocity with control 💡

Register for the webinar here →

Terraform

Terragrunt Tutorial – Getting Started & Examples

Terragrunt Tutorial - Getting Started & Examples

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.

  1. What is Terragrunt
  2. Terragrunt features
  3. How does Terragrunt work
  4. How to install Terragrunt
  5. Terragunt basic commands
  6. Setting up Terragrunt configurations
  7. Terragrunt use cases
  8. Terragrunt benefits
  9. Terragrunt best practices
  10. Terragrunt drawbacks and alternatives
  11. Using Terragrunt with Spacelift

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 examples

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 the terraform 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 before terraform is called.
  • after_hook (block): Nested blocks used to specify command hooks that should be run after terraform 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.

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-approvevar-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"
}

Download The Practitioner’s Guide to Scaling Infrastructure as Code

cheatsheet_image

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 applyrun-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.

  1. 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.
  2. 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.
  3. Use consistent naming conventions for resources to ensure clarity and prevent naming conflicts. Standardizing naming conventions can improve readability and make collaboration easier.
  4. Leverage variable files (e.g., .tfvars files) to store environment-specific information.
  5. Leverage secrets management solutions like Hashicorp Vault to keep sensitive information out of version control.
  6. 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.
  7. Specify version constraints for Terraform and Terragrunt to ensure compatibility and avoid unexpected behavior when updating to newer versions.
  8. Adopt a GitOps workflow where infrastructure changes are made through code changes in version-controlled repositories. This helps with versioning, collaboration, and rollbacks.
  9. Incorporate Terragrunt and Terraform into your CI/CD pipeline to automate infrastructure deployments and validate changes before they are applied.
  10. Write scripts or use automation tools to execute Terragrunt commands, reducing human error and streamlining the workflow.
  11. 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.
  12. 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.

Here, you can learn how to keep your configuration DRY with Terragrunt on Spacelift.

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.

Manage Terraform Better with Spacelift

Build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization and many more.

Start free trial