What Is Terragrunt?

Terragrunt acts as a wrapper for Terraform to help support DRY (Don’t Repeat Yourself) configuration, and to automate certain tasks. It was built by Gruntwork, and helps to cover many emerging operational pain points when scaling up your Terraform use.
The tool doesn’t replace Terraform – it enhances it. Think of Terragrunt as a helpful assistant that handles repetitive tasks, manages remote state, and provides improved module management capabilities.
Why consider using Terragrunt? If you’ve ever:
- Found yourself copying and pasting similar Terraform code across environments
- Struggled with remote state management
- Needed better ways to handle variables across multiple environments
- Wanted to execute Terraform commands on multiple modules at once
Then Terragrunt might be exactly what you’re looking for. Many teams implementing devops development and consulting services find Terragrunt invaluable for managing complex infrastructure at scale. If you’re also comparing who should implement and maintain your IaC setup, check this DataArt vs AppRecode expert comparison for a quick side-by-side view.
How Terragrunt Works: Core Concepts
To understand how Terragrunt works, familiarize yourself with these core concepts:
terragrunt.hcl Files
The heart of Terragrunt is the terragrunt.hcl configuration file. This file contains Terragrunt-specific configuration and can inherit from parent directories, enabling better code organization with hierarchical configurations.
Remote State Management
Terragrunt simplifies remote state configuration by providing a single place to define it. Instead of copying the same backend configuration across every module, you can define it once in a parent terragrunt.hcl file.
Input Variables
Terragrunt lets you define and manage input variables centrally. You can set variables at different levels (root, environment, component) and have them automatically passed to Terraform.
Dependencies
Terragrunt elegantly handles dependencies between modules. You can explicitly define dependencies, allowing Terragrunt to determine the correct order for applying or destroying resources.
Keep Your Code DRY
One of Terragrunt’s main purposes is eliminating repetition. By enabling configuration inheritance, you avoid duplicating common settings across multiple Terraform modules.
Terragrunt Modules & Architecture Best Practices
When working with Terragrunt, following thoughtful architecture patterns helps maximize its benefits:
Hierarchical Directory Structure
A well-organized directory structure might look like:
├── terragrunt.hcl # Root configuration
├── prod
│ ├── terragrunt.hcl # Production environment configuration
│ ├── vpc
│ │ └── terragrunt.hcl # VPC configuration for production
│ └── databases
│ └── terragrunt.hcl # Database configuration for production
└── dev
├── terragrunt.hcl # Development environment configuration
├── vpc
│ └── terragrunt.hcl # VPC configuration for development
└── databases
└── terragrunt.hcl # Database configuration for development
Configuration Inheritance
Terragrunt’s inheritance model means child configurations automatically receive settings from parent configurations. This works beautifully for environment-specific settings.
Centralized Module Source
Use a centralized repository for Terraform modules and reference them in Terragrunt configurations. This approach significantly reduces duplication and enforces standardization.
Environment-Specific Variables
Store environment-specific variables in the appropriate terragrunt.hcl files. This keeps your infrastructure definitions consistent while allowing for environment-specific values.
When structuring your configuration, always remember that organizations implementing managed cloud services benefit from thoughtful architecture that reduces maintenance complexity.
Essential Terragrunt Commands You Need
These commands form the foundation of working with Terragrunt:
terragrunt init: initializes the working directory by downloading the necessary providers and modules.
terragrunt plan: shows what changes Terragrunt will make to your infrastructure.
terragrunt apply: applies the changes required to reach the desired state.
terragrunt destroy: destroys all resources managed by the current configuration.
terragrunt run-all: runs a command across multiple Terragrunt modules.
terragrunt output: extracts the outputs from the Terraform state.
These commands closely mirror their Terraform counterparts, making the transition to Terragrunt straightforward for teams familiar with Terraform.
Step-by-Step Terragrunt Tutorial
Let's walk through setting up a basic Terragrunt project:
Step 1: Install Terragrunt
For macOS users with Homebrew:
brew install terragrunt
For Linux users:
curl -Lo terragrunt https://github.com/gruntwork-io/terragrunt/releases/download/v0.45.0/terragrunt_linux_amd64
chmod +x terragrunt
sudo mv terragrunt /usr/local/bin/
Step 2: Create Your Project Structure
Create a directory structure for a simple project:
mkdir -p terragrunt-demo/{terragrunt.hcl,dev/{vpc,rds}/terragrunt.hcl,prod/{vpc,rds}/terragrunt.hcl}
Step 3: Define Root Configuration
In the root terragrunt.hcl, set up remote state configuration:
# terragrunt-demo/terragrunt.hcl
remote_state {
backend = “s3”
config = {
bucket = “my-terraform-state”
key = “${path_relative_to_include()}/terraform.tfstate”
region = “us-west-2”
encrypt = true
dynamodb_table = “terraform-locks”
}
}
# Generate provider configuration for all child configurations
generate “provider” {
path = “provider.tf”
if_exists = “overwrite_terragrunt”
contents = <<EOF
provider “aws” {
region = “us-west-2”
}
EOF
}
Step 4: Define Environment-Specific Configurations
For the development environment:
# terragrunt-demo/dev/terragrunt.hcl
include {
path = find_in_parent_folders()
}
inputs = {
environment = “dev”
instance_type = “t3.small”
}
For production:
# terragrunt-demo/prod/terragrunt.hcl
include {
path = find_in_parent_folders()
}
inputs = {
environment = “prod”
instance_type = “m5.large”
}
Step 5: Define Module Configurations
For the VPC in development:
# terragrunt-demo/dev/vpc/terragrunt.hcl
include {
path = find_in_parent_folders()
}
terraform {
source = “tfr://registry.terraform.io/terraform-aws-modules/vpc/aws?version=3.14.0”
}
inputs = {
name = “dev-vpc”
cidr = “10.0.0.0/16”
azs = [“us-west-2a”, “us-west-2b”]
private_subnets = [“10.0.1.0/24”, “10.0.2.0/24”]
public_subnets = [“10.0.101.0/24”, “10.0.102.0/24”]
enable_nat_gateway = true
single_nat_gateway = true
}
It gives a great deal of flexibility in your code while maintaining consistent environments.
Terragrunt vs Terraform: Key Differences
It’s important to understand when looking at Terragrunt and Terraform what they do and how they are related:
Terraform: The core IaC tool that defines and provisions infrastructure.
Terragrunt: A thin wrapper around Terraform that provides:
- Better code reuse through inheritance
- Simplified remote state management
- Dependency management between components
- Multi-module workflows
Terraform works perfectly for simple projects, but as complexity grows, Terragrunt adds valuable capabilities that help manage that complexity. Organizations implementing cloud migration solutions often find Terragrunt essential for maintaining sanity during large migrations.
Real Terragrunt Example
Let’s examine a real-world example where Terragrunt shines. Imagine managing a multi-environment AWS infrastructure with shared components:
Root terragrunt.hcl
remote_state {
backend = “s3”
config = {
bucket = “acme-terraform-states”
key = “${path_relative_to_include()}/terraform.tfstate”
region = “us-east-1”
encrypt = true
dynamodb_table = “terraform-locks”
}
}
Pass common variables to all child Terragrunt configurations
inputs = {
company_name = “ACME”
aws_region = “us-east-1”
}
Common AWS provider for all modules
generate “provider” {
path = “provider.tf”
if_exists = “overwrite_terragrunt”
contents = <<EOF
provider “aws” {
region = “${local.aws_region}”
default_tags {
tags = {
Environment = “${local.environment}”
Terraform = “true”
Project = “${local.project}”
}
}
}
EOF
}
Environment-specific configuration:
environments/production/terragrunt.hcl
include {
path = find_in_parent_folders()
}
locals {
environment = “production”
project = “main-app”
}
inputs = {
environment = local.environment
instance_types = [“m5.large”]
high_availability = true
backup_retention = 30
}
Component-specific configuration:
environments/production/database/terragrunt.hcl
include {
path = find_in_parent_folders()
}
dependency “vpc” {
config_path = “../vpc”
Configure mock outputs for the vpc module to allow
database plan to work even when VPC isn’t created yet
mock_outputs = {
private_subnets = [“subnet-123”, “subnet-456”]
vpc_id = “vpc-123”
}
}
terraform {
source = “github.com/terraform-aws-modules/terraform-aws-rds?ref=v3.4.0”
}
inputs = {
identifier = “production-db”
Get values from the VPC module output
subnet_ids = dependency.vpc.outputs.private_subnets
vpc_security_group_ids = [dependency.vpc.outputs.database_security_group_id]
engine = “postgres”
engine_version = “13.4”
instance_class = “db.m5.large”
allocated_storage = 100
name = “appdb”
username = “dbadmin”
password = “get_from_parameter_store”
backup_retention_period = 30
Additional RDS parameters…
}
This example demonstrates several Terragrunt strengths:
- Consistent remote state configuration
- Environment-specific variables
- Dependencies between components
- Code reuse through centralized modules
Organizations concerned with managed cloud security services appreciate how Terragrunt helps maintain security standards across environments through configuration inheritance.
Conclusion
Terragrunt solves many of Terraform’s limitations when working with more complex multi-environment infrastructure projects. Terragrunt’s ability to keep configurations DRY, automate common workflows, and manage dependencies makes the infrastructure-as-code experience substantially better.
There is a learning curve to Terragrunt, but the investment in learning pays dividends quickly for teams which manage infrastructure at scale. The modularity and inheritance features of Terragrunt give teams the ability to standardize their infrastructure while still accommodating the changes necessary for different environments.
As cloud infrastructure continues to become more and more complicated, tools like Terragrunt become increasingly more important for keeping things manageable and consistent. If you are suffering from Terraform sprawl and copy/pasting configurations, Terragrunt brings tremendous value and should be seriously considered.
Frequently asked questions
Do I Need to Learn Terraform Before Using Terragrunt?
Learn the foundations of either OpenTofu or Terraform first. You need not become an expert in every provider, but Terragrunt will be frustrating—and potentially unsafe—if a plan, state file, or module is still unfamiliar territory. It organizes how the underlying engine is used; the engine remains responsible for evaluating .tf code, contacting providers, building a plan, changing resources, and recording state.
A practical starting point is HCL syntax, variables, outputs, resources, data sources, modules, provider and version constraints, initialization, planning, applying, and remote state. Spend time reading plans, including replacements and deletions. Know what locking protects and what it does not. Terragrunt can remove repeated configuration, but it cannot tell whether destroying a database is the business outcome you intended.
Adoption can be gradual. Put an empty terragrunt.hcl beside a working root module and Terragrunt can proxy familiar OpenTofu or Terraform commands, including auto-init when required. Once that behaves predictably, try inputs for deployment-specific values and a terraform block that selects a pinned shared module. Introduce named includes for genuinely common settings. Dependencies and run --all should come later, after the units plan and apply correctly on their own.
There is no requirement to choose HashiCorp Terraform specifically: current Terragrunt documentation supports both Terraform and OpenTofu. Whichever engine the team chooses, pin and test compatible Terragrunt, engine, provider, and module versions in local development and CI. A wrapper does not insulate a repository from breaking upgrades. Understanding the layer below it is what makes troubleshooting and approval credible.
Is Terragrunt Suitable for Large Infrastructure Projects?
Terragrunt can be a strong fit when a repository contains many independently deployable OpenTofu or Terraform modules, several environments, repeated settings, and real ordering constraints. It lets teams treat a directory containing terragrunt.hcl as a unit, reuse common configuration through includes, retrieve outputs from another unit, and operate a collection of units through the run queue. Those capabilities can reduce copy-and-paste work without combining an entire platform into one state file.
Size alone is not a reason to add it. A large system with clear native modules, a manageable number of root configurations, and an existing orchestration platform may gain little from another layer. Terragrunt introduces its own HCL, cache, command surface, upgrade path, and debugging questions. The team must be willing to own those operational costs.
For a scalable design, keep units small enough to plan and recover independently, but avoid splitting every resource into a separate state. Pin reusable module versions rather than pointing production directly at a moving branch. Use a clearly named shared file such as root.hcl; current Terragrunt guidance recommends this over using terragrunt.hcl for both the shared root configuration and deployable units. Model genuine dependencies explicitly and reject cycles. Separate accounts, subscriptions, regions, or environments where the desired access boundaries and blast radius require it.
Test the structure with representative plans before a broad migration. Measure plan duration, queue behavior, credentials, state recovery, and how engineers diagnose failures. Terragrunt helps organize scale, but repository conventions, reviews, access control, observability, backups, and practiced recovery procedures still determine whether the infrastructure is maintainable.
Can Terragrunt Be Used with CI/CD Pipelines?
Yes. Terragrunt can run in a CI/CD system just like other command-line infrastructure tools, provided the runner has a compatible Terragrunt binary, OpenTofu or Terraform, cloud credentials, and access to module and state locations. Pin tool versions and verify downloaded binaries so the same commit produces a comparable plan locally and in automation.
A safe pipeline normally separates review from deployment. Format and validate configuration first, then create a plan for the affected unit or units. Preserve the plan and relevant logs according to the engine’s workflow, require the appropriate review or environment approval, and apply from a protected branch with short-lived, least-privilege credentials. Serialize operations that share state and rely on a backend with locking or an equivalent concurrency safeguard. Do not pass secrets in ordinary variables, command lines, or public logs.
For several units, current Terragrunt syntax is terragrunt run --all plan or terragrunt run --all apply; the older run-all command is deprecated by the CLI redesign. The run queue discovers units and uses declared dependencies to determine order while allowing safe parallel work. That does not mean every pull request should target the whole repository. Scope runs by working directory or supported filters, and verify that changes to included files also select the units that read them.
Keep production applies non-interactive only where the pipeline already supplies an explicit approval control. Save run reports, surface partial failures, and make retries deliberate: a failed multi-unit run may have applied some dependencies before stopping. Finish with drift detection and periodic recovery tests. CI/CD automates execution; it does not replace review of the infrastructure plan.
What Is a terragrunt.hcl File and Why Is It Important?
Think of terragrunt.hcl as the assembly instructions for one deployable unit. The file may select a versioned OpenTofu or Terraform module, provide its input values, identify another unit whose outputs are needed, or add hooks and command arguments. Terragrunt reads these instructions before it invokes the chosen IaC engine. The file therefore complements the module’s .tf code; it does not replace resources, variables, outputs, or provider requirements.
That boundary keeps a repository understandable. A reusable module might describe the standard shape of a network. The terragrunt.hcl beside a production unit chooses the module revision and supplies values for one region and environment. Another unit can use the same module with different values. Module upgrades can then move through environments by review instead of changing every deployment at once.
Repeated settings can live in a shared HCL file and reach units through named includes. For example, a child can use include "root" { path = find_in_parent_folders("root.hcl") }. Terragrunt now recommends a name such as root.hcl for this shared parent. Calling both the parent and each deployable unit terragrunt.hcl is an older convention that can make run --all discovery confusing. It remains supported during migration, but warnings and a documented migration path exist.
Commit Terragrunt HCL to version control, format and validate it, pin remote module references, and review its plans. Credentials belong in a secure identity or secret mechanism, never in the file. Reviewers should also be able to trace includes: inherited values and merge behavior should be deliberate, rather than hidden behind a directory hierarchy nobody can explain.
Can I Use Terragrunt with AWS and Azure at the Same Time?
Terragrunt can orchestrate units that deploy to AWS, Azure, and other providers in the same repository. The actual cloud operations still come from OpenTofu or Terraform providers, so each unit needs the correct provider configuration, credentials, backend, and version constraints. Terragrunt does not turn one provider into another or create a universal multi-cloud state.
Organize the repository around operational boundaries that people can recognize. Cloud, account or subscription, region, environment, and component are common dimensions, but the directory tree should reflect how the team plans, approves, deploys, and recovers infrastructure. Keep separate state for units that need independent access control or blast radius. A single shared parent file may hold truly common tags or naming rules, while cloud-specific includes can generate or supply AWS and Azure settings without filling every child file with conditionals.
Authentication deserves special care. CI should use short-lived identities such as workload federation or platform-native role mechanisms, with separate privileges for planning and applying where practical. Do not store AWS keys, Azure client secrets, or state credentials in terragrunt.hcl. Restrict state access independently for each backend and test locking, encryption, backup, and recovery. Terragrunt documents enhanced bootstrapping for some backends, but backend behavior and supported options are not identical across clouds.
Cross-cloud dependencies are possible, yet they can make failures and recovery harder. If an Azure unit consumes an AWS output, document ownership, data sensitivity, deployment order, and what happens when one control plane is unavailable. Prefer stable interfaces and loose coupling over a long synchronous dependency chain. Multi-cloud organization is supported; operational resilience still has to be designed.
What Is the Difference Between dependency and dependencies Blocks in Terragrunt?
Use a dependency block when one unit needs output values from another. Give the block a label, set its config_path, and reference exported values through an expression such as dependency.vpc.outputs.vpc_id. Terragrunt can call the underlying engine’s output command for that target and use the relationship when building the run queue. The upstream module must actually declare the required outputs.
The plural dependencies block is narrower. It declares paths that must be ordered before the current unit during multi-unit operations, but it does not expose their outputs to the configuration. It is useful when ordering is real even though no value has to cross the boundary. In many applications, a singular dependency already supplies both the output access and the ordering information, so adding the same relationship again as dependencies only creates noise.
Mock outputs can make validation or planning possible before an upstream unit has been applied, but they need strict limits. Placeholder subnet IDs are not evidence that a real apply will succeed. Restrict mocks to appropriate commands, use structurally realistic values, and never let a production apply silently depend on invented identifiers. Prefer testing against an isolated environment when integration behavior matters.
Dependencies should form a directed acyclic graph. A cycle means the units cannot be ordered and often signals that boundaries or interfaces need redesign. Keep chains short where possible, because each output lookup adds work and a failure can block downstream units. For destruction, ordering is reversed so dependents are removed before the units they rely on. Always review the run queue before approving a broad apply or destroy, especially after dependency paths change.
How Should Terragrunt Remote State Be Configured Safely?
Give every independently deployed unit a unique state key and store production state in a remote backend with encryption, access control, versioning or recoverable history, and locking or the backend’s current concurrency mechanism. State can contain sensitive values even when an output is marked sensitive, so access to it should be narrower than ordinary repository access. Backups matter only if the team has tested restoration.
Terragrunt’s remote_state block can generate the OpenTofu or Terraform backend configuration, allowing shared values such as bucket and region to live in one included file while a function such as path_relative_to_include() produces a different key per unit. Terragrunt also has backend bootstrapping capabilities for supported backends. Review the current documentation for the selected backend rather than copying an old S3 example: accepted settings and locking recommendations change over time.
Do not let two unrelated units resolve to the same key. Before restructuring directories or renaming an include, inspect the computed state path and plan a controlled migration; changing the key without moving the state can make the engine appear to see an empty environment. Never “fix” that surprise by applying immediately. Compare the old and new backend locations, use the documented migration workflow, and take a recoverable snapshot first.
Credentials should come from short-lived identity mechanisms or secure runner configuration, not HCL committed to source control. Separate state by account, subscription, environment, or trust boundary where permissions require it. Finally, remember that Terragrunt reduces repeated backend configuration but does not eliminate state operations. Lock contention, failed migrations, accidental deletion, and compromised credentials still need monitoring, runbooks, and rehearsed recovery.
What Terragrunt Commands and Repository Checks Should a Team Use Before Apply?
Start with pinned versions and a clean checkout. Format Terragrunt HCL with the current terragrunt hcl fmt command and validate the configuration with terragrunt hcl validate. Validate the underlying OpenTofu or Terraform module as well, because Terragrunt validation does not prove that provider arguments, resources, or module inputs are correct. Run security, policy, and secret scans appropriate to the repository.
For a single unit, initialize when required and run terragrunt plan. Terragrunt auto-init can perform initialization automatically, but an explicit initialization step may make CI logs and provider downloads easier to audit. Read the complete plan, including replacements, deletions, provider changes, data-source reads, and values that will be known only after apply. A zero exit code is not a business approval.
For a stack of units, use the modern form terragrunt run --all plan and inspect the queue and dependency order. Narrow the selection with current supported filters or a working directory when a whole-tree run is unnecessary. Changes to shared included HCL can affect units whose own files did not change, so the pipeline’s impact detection must account for readers of those files. Do not substitute a simple list of changed terragrunt.hcl files for dependency-aware selection.
Before apply, confirm the intended account, subscription, region, workspace or backend key, module revision, and credential identity. Require review proportionate to risk, protect production branches and environments, and make rollback or forward-fix steps explicit. After apply, record the result, run targeted smoke checks, and monitor the service. A successful command only confirms that the engine completed; it does not confirm that the system is healthy.





