Comprehensive Guide to Terragrunt: Best Practices for Managing Multiple Environments
Managing infrastructure as code (IaC) is a cornerstone of modern DevOps practices, and Terraform has become a go-to tool for this purpose. However, as infrastructure grows in complexity, so does the need for efficient management of multiple environments, such as development, staging, and production. This is where Terragrunt comes in. Terragrunt is a thin wrapper around Terraform that provides extra tools for keeping configurations DRY (Don't Repeat Yourself) and managing multiple environments more effectively.
In this article, we will dive deep into best practices for managing multiple environments with Terragrunt. Whether you are new to Terragrunt or looking to refine your usage, this guide will provide valuable insights and practical tips.
Terragrunt is an open-source tool that extends Terraform by offering features to manage complex infrastructure setups. It addresses some of the pain points associated with Terraform, such as the handling of multiple environments, by introducing a hierarchical configuration system and DRY principles.
Before proceeding, you might also want to read this introduction to Terragrunt.
Managing multiple environments in Terraform can become cumbersome due to the need to duplicate code and configurations. Terragrunt simplifies this by providing a structured way to define and manage these environments, ensuring consistency and reducing maintenance overhead.
A well-organized directory structure is crucial for managing multiple environments efficiently. Terragrunt recommends a hierarchical structure where each environment is a separate folder. Here is an example:
infrastructure-live
├── dev
│ ├── us-east-1
│ │ └── app
│ │ └── terragrunt.hcl
├── staging
│ ├── us-east-1
│ │ └── app
│ │ └── terragrunt.hcl
└── prod
├── us-east-1
└── app
└── terragrunt.hcl
In this structure:
infrastructure-live
is the root directory containing environment-specific configurations.dev
, staging
, and prod
are directories for each environment.us-east-1
is a region-specific subdirectory.terragrunt.hcl
file with the specific configuration for that context.Leverage Terragrunt’s ability to include configurations to avoid duplication. You can create a common configuration file and reference it in environment-specific configurations.
Common configuration (common.hcl):
inputs = {
aws_region = "us-east-1"
environment = "common"
}
Environment-specific configuration (terragrunt.hcl):
include {
path = find_in_parent_folders("common.hcl")
}
inputs = {
environment = "dev"
}
In this setup:
common.hcl
defines common variables such as aws_region
.terragrunt.hcl
file includes the common configuration and can override or add additional inputs specific to that environment.Managing state files is critical in Terraform. Terragrunt can help by automatically configuring remote state storage. Define a remote state configuration in a common file and include it in your environment-specific files.
Remote state configuration (remote_state.hcl):
remote_state {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
}
}
Include remote state in environment-specific configuration (terragrunt.hcl):
include {
path = find_in_parent_folders("remote_state.hcl")
}
In this example:
remote_state.hcl
specifies the S3 bucket, key, and region for the remote state.terragrunt.hcl
includes this remote state configuration, ensuring all environments use a consistent state backend.Terragrunt can automate common Terraform workflows, such as applying and destroying resources. Use hooks to run scripts before or after Terraform commands.
Example hook configuration:
terraform {
before_hook "before_apply" {
commands = ["apply"]
execute = ["bash", "-c", "echo Running pre-apply script"]
}
after_hook "after_apply" {
commands = ["apply"]
execute = ["bash", "-c", "echo Running post-apply script"]
}
}
In this example:
before_hook
runs a script before the apply
command.after_hook
runs a script after the apply
command.If your infrastructure has dependencies between modules (e.g., a network module needed by an application module), you can define these dependencies in Terragrunt to ensure they are applied in the correct order.
Example dependency configuration:
dependencies {
paths = ["../network"]
}
In this configuration:
dependencies
block specifies that the current module depends on the network
module located in a sibling directory.Use environment variables or encrypted files to manage sensitive data such as API keys or database passwords. Avoid hardcoding sensitive information in your Terragrunt configurations.
Example using environment variables:
inputs = {
db_password = get_env("DB_PASSWORD", "default_password")
}
In this example:
db_password
input is retrieved from an environment variable. If the variable is not set, a default value is used.Ensure you are using the latest versions of Terragrunt and Terraform to benefit from new features, improvements, and security patches. Regular updates can prevent compatibility issues and enhance the stability of your infrastructure.
Suppose you have an application that needs to be deployed across multiple AWS regions. You can use Terragrunt to manage the infrastructure for each region while keeping your Terraform configurations DRY.
Directory structure:
infrastructure-live
├── dev
│ ├── us-east-1
│ │ └── app
│ │ └── terragrunt.hcl
│ ├── us-west-2
│ │ └── app
│ │ └── terragrunt.hcl
├── staging
│ ├── us-east-1
│ │ └── app
│ │ └── terragrunt.hcl
│ ├── us-west-2
│ │ └── app
│ │ └── terragrunt.hcl
└── prod
├── us-east-1
│ └── app
│ └── terragrunt.hcl
├── us-west-2
└── app
└── terragrunt.hcl
Common configuration (common.hcl):
inputs = {
app_name = "my-app"
aws_region = get_env("AWS_REGION")
}
Environment-specific configuration (dev/us-east-1/app/terragrunt.hcl):
include {
path = find_in_parent_folders("common.hcl")
}
inputs = {
environment = "dev"
}
In this scenario:
app_name
and aws_region
.terragrunt.hcl
file includes the common configuration and sets the environment
input.In a microservices architecture, services often depend on shared resources such as a VPC or database. Terragrunt can help manage these dependencies.
Directory structure:
infrastructure-live
├── dev
│ ├── vpc
│ │ └── terragrunt.hcl
│ ├── database
│ │ └── terragrunt.hcl
│ └── app
│ └── terragrunt.hcl
VPC configuration (dev/vpc/terragrunt.hcl):
terraform {
source = "git::ssh://git@github.com/my-org/terraform-modules.git//vpc"
}
Database configuration (dev/database/terragrunt.hcl):
dependencies {
paths = ["../vpc"]
}
terraform {
source = "git::ssh://git@github.com/my-org/terraform-modules.git//database"
}
App configuration (dev/app/terragrunt.hcl):
dependencies {
paths = ["../vpc", "../database"]
}
terraform {
source = "git::ssh://git@github.com/my-org/terraform-modules.git//app"
}
In this scenario:
vpc
module is defined first and used as a dependency by both the database
and app
modules.database
module depends on the vpc
module.app
module depends on both the vpc
and database
modules.You want to automate the provisioning of your infrastructure to ensure consistency and reduce manual errors. Use Terragrunt hooks to run pre-apply and post-apply scripts.
Example hook configuration (dev/app/terragrunt.hcl):
terraform {
before_hook "before_apply" {
commands = ["apply"]
execute = ["bash", "-c", "echo Running pre-apply script"]
}
after_hook "after_apply" {
commands = ["apply"]
execute = ["bash", "-c", "echo Running post-apply script"]
}
}
In this example:
before_apply
hook runs a script before the apply
command, which can be used for tasks such as validating configurations or notifying stakeholders.after_apply
hook runs a script after the apply
command, which can be used for tasks such as sending notifications or running integration tests.Terragrunt offers powerful features that can significantly streamline the management of multiple environments in Terraform. By following best practices such as maintaining a hierarchical directory structure, adhering to the DRY principle, automating workflows, and managing dependencies, you can achieve a more efficient and maintainable infrastructure setup.
Incorporating Terragrunt into your DevOps toolkit will not only enhance your ability to manage complex infrastructure but also ensure that your Terraform code remains clean, DRY, and easy to manage across different environments.
For more detailed information, you can refer to the official Terragrunt documentation.
By following these best practices and applying them to practical scenarios, you can leverage the full potential of Terragrunt, making your infrastructure management more robust and scalable.
Happy Clouding !!!
Did you like this post?
If you did, please buy me coffee 😊