Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Terragrunt: Keep Terraform DRY

By Kokil Thapa | Last reviewed: September 2026

You copied the same S3 backend block into twelve Terraform folders. Then you changed the bucket name once and missed three stacks. Terragrunt: Keep Terraform DRY solves that by moving repeated config into parent terragrunt.hcl files that child modules inherit automatically. If you already use infrastructure as code with Terraform, Terragrunt is the thin wrapper that stops env-specific duplication from eating your week. This guide covers folder layout, remote state, dependencies, and the mistakes I see on real deployments.

What is Terragrunt and why does it help keep Terraform DRY?

Terragrunt is a CLI wrapper around Terraform published by Gruntwork. It does not replace Terraform. It orchestrates it.

Terraform modules already reduce duplication inside a single repo. Terragrunt reduces duplication across repos and environments. You define a remote backend once. You pin provider versions once. You set common tags once. Every child stack picks those up through include blocks.

On production systems I maintain, the same EC2 + RDS pattern repeats across staging and production. Without Terragrunt, each folder carries its own backend snippet, its own provider block, and its own copy-pasted variables. That is how drift starts.

Terragrunt: Keep Terraform DRYRoot terragrunt.hclBackend + providers + tagsenv/stagingShared env inputsenv/productionShared env inputsenv/devShared env inputsvpcmodule callrdsmodule callvpcmodule callec2module calls3moduleEach leaf inherits parent config — edit once, apply everywhere
Terragrunt hierarchy: root config flows down through environment folders to individual Terraform modules

The DRY principle in Terragrunt rests on three mechanisms documented in the official Terragrunt documentation:

  • Include blocks — child terragrunt.hcl files pull in parent configs with include "root".
  • Remote state generation — Terragrunt writes the backend block into a temp folder before each run.
  • Dependency blocks — one stack reads outputs from another without manual terraform output copy-paste.

Terragrunt wraps Terraform commands. Run terragrunt plan instead of terraform plan. Under the hood it generates files, sets working directories, and runs Terraform in the right context. That is the whole trick.

How do you set up a Terragrunt project structure?

A live Terragrunt repo mirrors how your org thinks about environments and services. I prefer a layout that separates reusable modules from live infrastructure.

infra-live/
├── root.hcl
├── _envcommon/
│   ├── vpc.hcl
│   ├── rds.hcl
│   └── ec2.hcl
├── staging/
│   ├── env.hcl
│   ├── vpc/terragrunt.hcl
│   ├── rds/terragrunt.hcl
│   └── ec2/terragrunt.hcl
└── production/
    ├── env.hcl
    ├── vpc/terragrunt.hcl
    ├── rds/terragrunt.hcl
    └── ec2/terragrunt.hcl

infra-modules/
├── vpc/
├── rds/
└── ec2/

Keep modules in a separate repo or top-level folder. Point Terragrunt at them with a terraform { source = "..." } block. This mirrors how Terraform modules for reusable infrastructure work, but Terragrunt handles the wiring between envs.

Root configuration file

Your root root.hcl (or terragrunt.hcl at the repo root) holds everything repeated across stacks:

remote_state {
  backend = "s3"
  config = {
    bucket         = "my-org-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-southeast-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "ap-southeast-1"
  default_tags {
    tags = {
      ManagedBy = "Terragrunt"
    }
  }
}
EOF
}

Notice the path_relative_to_include() function. Terragrunt sets a unique state key per folder automatically. That alone removes dozens of duplicated backend blocks. For deeper state strategy, see how to manage Terraform state safely and remote backend configuration.

Environment-level inputs

Each environment gets an env.hcl with locals only:

locals {
  environment = "staging"
  vpc_cidr    = "10.10.0.0/16"
  instance_type = "t3.small"
}

Child stacks read these through read_terragrunt_config(find_in_parent_folders("env.hcl")). You never hard-code staging CIDRs inside a VPC module folder.

Include Inheritance Flowroot.hclremote_stateenv.hclenv locals_envcommonmodule defaultsstaging/vpc/terragrunt.hclinclude root + envcommon + env localsterraform { source = git::.../vpc?ref=v1.2.0 }inputs = { cidr = local.vpc_cidr }terragrunt plan → generated backend.tf + provider.tf
How Terragrunt include blocks merge root, environment, and common module configs before running Terraform

Leaf stack example

A production VPC stack stays short because inheritance does the heavy work:

include "root" {
  path = find_in_parent_folders("root.hcl")
}

include "envcommon" {
  path   = "${dirname(find_in_parent_folders("root.hcl"))}/_envcommon/vpc.hcl"
  expose = true
}

locals {
  env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

inputs = {
  cidr_block  = local.env_vars.locals.vpc_cidr
  environment = local.env_vars.locals.environment
}

Run terragrunt run-all plan from the staging/ folder to plan every stack in dependency order. That command alone saves hours on multi-stack envs. Validate generated JSON with a JSON formatter when debugging complex inputs maps.

How does Terragrunt manage remote state and dependencies?

Remote state is where Terragrunt earns its keep. Plain Terraform needs a backend "s3" {} block in every module. Terragrunt generates it from the root config and keeps keys unique per folder.

Automatic state key isolation

The expression ${path_relative_to_include()}/terraform.tfstate produces keys like staging/vpc/terraform.tfstate and production/rds/terraform.tfstate. No two stacks collide. You do not maintain a spreadsheet of state paths.

Locking through DynamoDB works the same as native Terraform. Terragrunt just writes the block for you. If you run OpenTofu instead of HashiCorp Terraform, the backend config is identical — see OpenTofu as the open Terraform fork for compatibility notes.

Dependency blocks

RDS needs a VPC ID. In raw Terraform you export outputs manually or use terraform_remote_state data sources with hard-coded keys. Terragrunt simplifies this:

dependency "vpc" {
  config_path = "../vpc"

  mock_outputs = {
    vpc_id         = "vpc-mock"
    private_subnets = ["subnet-mock"]
  }
  mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

inputs = {
  vpc_id     = dependency.vpc.outputs.vpc_id
  subnet_ids = dependency.vpc.outputs.private_subnets
}

Terragrunt reads the remote state of ../vpc before planning RDS. It also respects order in run-all apply. VPC applies first. RDS waits. That is declarative orchestration without a separate tool.

Terragrunt Dependency GraphVPC Stackoutputs: vpc_id, subnetsRDS Stackdependency.vpc.outputsEC2 Stackdependency.vpc.outputsS3 Remote Statestaging/rds/tfstateS3 Remote Statestaging/ec2/tfstaterun-all apply respects dependency order automatically
Terragrunt dependency blocks wire stack outputs through remote state without manual copy-paste

For multi-region or multi-cloud setups, the same pattern scales. Each account/region gets an env folder. Shared root config stays identical. Read managing multi-cloud state with Terraform for backend choices beyond a single S3 bucket.

How do Terragrunt and Terraform modules work together?

Terragrunt does not replace modules. It calls them. Think of Terragrunt as the deployment layer and Terraform modules as the building blocks.

The terraform { source = "..." } block tells Terragrunt where to fetch module code. Common patterns:

  1. Local pathsource = "../../../infra-modules/vpc" for monorepos.
  2. Git refsource = "git::https://github.com/org/modules.git//vpc?ref=v2.1.0" for version pinning.
  3. Registrysource = "tfr:///terraform-aws-modules/vpc/aws?version=5.0.0" for public modules.

Pin module versions explicitly. Terragrunt caches downloaded modules in .terragrunt-cache. Add that folder to .gitignore. When you bump a ref, run terragrunt init -upgrade in affected stacks.

Provider version constraints still live in the module's versions.tf or in a Terragrunt generate block. Align this with Terraform provider version pinning practices so CI and local runs match.

DRY inputs with _envcommon

The _envcommon/ folder holds partial configs shared by every environment for a given module type. A vpc.hcl there might set:

terraform {
  source = "git::https://github.com/myorg/terraform-modules.git//vpc?ref=v1.4.0"
}

inputs = {
  enable_dns_hostnames = true
  enable_dns_support   = true
  az_count             = 2
}

Staging and production leaf files only override what differs — CIDR, NAT gateway count, tags. That is DRY at the input level, not just the backend level.

ApproachDRY levelBest forTrade-off
Plain Terraform foldersLow — copy backend per folder1–3 simple stacksFast start, painful at scale
Terraform modules onlyMedium — shared resource logicSingle env, many resourcesBackend still duplicated per deploy target
Terraform workspacesMedium — shared code, split stateSmall teams, similar envsWorkspace sprawl, weak isolation story
Terragrunt + modulesHigh — backend, providers, inputs, depsMulti-env, multi-account, 5+ stacksExtra tool, learning curve

For workspace-specific trade-offs, compare with Terraform workspaces and environments. Workspaces solve a narrower problem. Terragrunt solves org-scale repetition.

What are common Terragrunt mistakes to avoid?

I've debugged Terragrunt setups that worked on a laptop and failed in CI. Most failures fall into a short list.

Duplicating config instead of including it

Teams new to Terragrunt often copy the root remote_state block into every child file "just to be safe." That defeats the purpose. Use include and find_in_parent_folders(). One source of truth.

Skipping mock_outputs on dependencies

Without mock_outputs, terragrunt plan on a fresh clone fails because dependency state does not exist yet. Always allow mocks for validate and plan commands. Remove mocks only when you know the dependency is applied.

Deep nesting without run-all flags

Running terragrunt apply manually in fifteen folders is error-prone. Use terragrunt run-all apply with --terragrunt-non-interactive in CI. Pair it with Terraform CI/CD in GitHub Actions or your GitLab pipeline.

Committing .terragrunt-cache

The cache directory contains downloaded modules and generated files. It bloats the repo and causes merge conflicts. Ignore it.

Mixing Terragrunt and hand-written backend.tf

If a module folder already has a backend.tf, Terragrunt's generate block may conflict. Pick one approach per stack. Generated backends with if_exists = "overwrite_terragrunt" are the clean default.

Before vs After: Terragrunt DRYBefore (WET)12 copies of backend.tf12 copies of provider.tfManual output copy-pasteDrift on bucket rename3 missed folders~600 lines repeatedHigh maintenance costAfter (DRY)1 root.hcl backend block1 generated provider.tfdependency blocks_envcommon shared inputsrun-all orchestration~40 lines per leaf stackEdit once, apply everywhere
Terragrunt: Keep Terraform DRY — duplicated config versus centralised root and envcommon inheritance

When should you choose Terragrunt over plain Terraform?

Not every project needs Terragrunt. A single VPS provisioned once a year is fine with plain Terraform for VPS provisioning.

Add Terragrunt when you hit any of these thresholds:

  • Three or more environments with identical backend requirements.
  • Five or more Terraform stacks that share dependencies.
  • Multiple AWS accounts, regions, or cloud providers under one repo.
  • CI pipelines that must plan/apply stacks in order without custom scripts.
  • A team where Terraform variables, locals, and outputs are duplicated across folders weekly.

For sister sites I deploy with Deployer 7 and GitLab CI on shared EC2, Terragrunt would fit if we moved infra to code at scale. Today those sites share a pipeline pattern, not a Terraform monorepo. The same DRY thinking applies — centralise what repeats. See the Adventure Third Pole Trek deployment for a live Laravel + Livewire stack that benefits from consistent ops patterns.

Install Terragrunt alongside Terraform on your workstation or CI runner:

brew install terragrunt
terragrunt --version

cd infra-live/staging/vpc
terragrunt init
terragrunt plan
terragrunt apply

Terragrunt detects the Terraform binary on your PATH. It passes through most CLI flags. Use terragrunt hcl fmt to format config files the way terraform fmt formats .tf files.

If your team manages servers day-to-day rather than cloud APIs, compare Terraform vs Ansible for provisioning vs configuration. Terraform (with Terragrunt) builds infra. Ansible configures it afterward. Many production setups use both.

For certification prep that covers Terraform fundamentals Terragrunt assumes you know, see the HashiCorp Terraform Associate certification guide. Terragrunt itself has no official cert, but the Terraform base matters.

Official Terraform language reference for modules and backends lives in the HashiCorp Terraform documentation. Cross-check generated HCL against those specs when debugging.

Key Takeaways

  • Define remote state, providers, and common tags once in a root terragrunt.hcl — child stacks inherit via include blocks.
  • Use path_relative_to_include() for automatic per-stack state keys so stacks never overwrite each other.
  • Wire cross-stack values with dependency blocks and mock_outputs so plans work on fresh clones.
  • Keep reusable module logic in a separate repo; Terragrunt handles env-specific inputs through _envcommon and env.hcl.
  • Run terragrunt run-all plan/apply in CI instead of applying stacks one folder at a time by hand.
  • Adopt Terragrunt when you have multiple envs and repeated backend or dependency wiring — not for a single one-off stack.

People Also Ask

Does Terragrunt replace Terraform?

No. Terragrunt is a wrapper that generates Terraform files, manages remote state config, and orchestrates multiple modules. You still write Terraform modules and run Terraform providers under the hood. Terragrunt calls terraform plan and terraform apply for you.

Can Terragrunt work with OpenTofu?

Yes. Set the environment variable TG_TF_PATH=tofu or configure terraform_binary in your terragrunt config. OpenTofu uses the same HCL syntax and backend blocks Terragrunt generates. Test in a non-production env first.

How is Terragrunt different from Terraform workspaces?

Workspaces split state inside one backend key prefix within a single folder. Terragrunt uses separate folders per env with inherited config and explicit dependency graphs. Terragrunt scales better when environments differ in inputs, accounts, or stack count.

Where should I store Terragrunt config secrets?

Never commit secrets in terragrunt.hcl. Use environment variables, AWS SSM Parameter Store, or HashiCorp Vault. Terragrunt supports get_env() and sops integration patterns. Keep secrets out of version control and inject them at CI runtime.

Ship DRY infrastructure without the copy-paste tax

Terragrunt: Keep Terraform DRY by treating repeated backend, provider, and input config as a hierarchy problem, not a copy-paste problem. Start with a root config, add env folders, wire dependencies, and let run-all handle orchestration. The first hour spent on layout saves days of drift fixes later.

If you want help structuring live infra repos, CI pipelines, or server hardening around your Terraform workflow, Linux system administration and ongoing support and maintenance cover the full deploy path. For greenfield apps that will eventually need this ops layer, review enterprise application development or kokil.com.np. Ready to talk through your stack? Contact us with your current folder layout and env count.

Frequently Asked Questions

Terragrunt is a CLI wrapper around Terraform published by Gruntwork. It does not replace Terraform; it orchestrates it. You define shared remote backend settings, provider blocks, and common tags once in parent terragrunt.hcl files. Child stacks inherit those through include blocks, remote state generation, and dependency blocks. That stops the same S3 backend snippet from being copied into twelve folders and drifting when one copy gets missed.

No. Terragrunt generates Terraform files, sets working directories, and runs terraform plan and terraform apply under the hood. You still write Terraform modules and use Terraform providers.

Split reusable modules from live infrastructure. A typical layout has infra-modules/ for vpc, rds, and ec2 modules, and infra-live/ with root.hcl, _envcommon/ for shared partial configs, plus staging/ and production/ folders each containing env.hcl and leaf stacks like vpc/terragrunt.hcl. Root holds repeated remote_state and generate provider blocks. Environment folders hold locals only. Leaf files include root and envcommon, then pass env-specific inputs.

It builds a unique S3 state key per stack automatically, such as staging/vpc/terraform.tfstate or production/rds/terraform.tfstate. You define the bucket, region, encryption, and DynamoDB lock table once in root.hcl. Terragrunt writes the backend block into a temp folder before each run. Stacks never overwrite each other's state, and you stop maintaining a spreadsheet of state paths by hand.

Child terragrunt.hcl files pull parent configs with include "root" { path = find_in_parent_folders("root.hcl") }. You can also include shared module configs from _envcommon/ with expose = true. Environment values come from read_terragrunt_config(find_in_parent_folders("env.hcl")). Root supplies backend and provider generation; env.hcl supplies CIDRs and instance types; the leaf stack only sets what differs for that service.

Plain Terraform needs a backend block in every module folder. Terragrunt generates it from root config using generate { path = "backend.tf" if_exists = "overwrite_terragrunt" }. Locking through DynamoDB works the same as native Terraform; Terragrunt just writes the block for you. If you run OpenTofu instead of HashiCorp Terraform, the backend config is identical. Automatic key isolation via path_relative_to_include() is the main win over copy-pasted backends.

A dependency block points at another stack's folder, such as ../vpc. Terragrunt reads that stack's remote state before planning the current one. Inputs map dependency.vpc.outputs.vpc_id into your module. During terragrunt run-all apply, VPC applies first and RDS waits. That replaces manual terraform output copy-paste or hard-coded terraform_remote_state keys. It gives declarative orchestration across stacks without a separate tool.

mock_outputs supply placeholder values like vpc-mock when dependency state does not exist yet. Without them, terragrunt plan on a fresh clone fails because the upstream stack was never applied. Set mock_outputs_allowed_terraform_commands to validate and plan so those commands succeed before dependencies exist. Remove mocks only when you know the dependency is already applied. Skipping mock_outputs is one of the most common setup mistakes I see.

Terragrunt is the deployment layer; Terraform modules are the building blocks. The terraform { source = "..." } block tells Terragrunt where to fetch module code from a local path, a git ref, or the Terraform Registry. Pin versions explicitly. Terragrunt caches downloads in .terragrunt-cache, which belongs in .gitignore. Provider version constraints live in the module's versions.tf or in a Terragrunt generate block. Bump a ref, then run terragrunt init -upgrade in affected stacks.

_envcommon/ holds partial configs shared by every environment for a given module type. A vpc.hcl there might set the module source git ref and common inputs like enable_dns_hostnames, enable_dns_support, and az_count. Staging and production leaf files only override what differs, such as CIDR, NAT gateway count, or tags. That is DRY at the input level, not just the backend level, and keeps leaf stacks short.

A single VPS provisioned once a year is fine with plain Terraform. Add Terragrunt when you hit thresholds like three or more environments with identical backend requirements, five or more stacks sharing dependencies, multiple AWS accounts or regions under one repo, or CI pipelines that must plan and apply stacks in order. If your team copies backend blocks, provider settings, and variables across folders weekly, the extra tool pays for itself quickly.

Workspaces split state inside one backend key prefix within a single folder. They suit small teams with similar environments but lead to workspace sprawl and weak isolation. Terragrunt uses separate folders per environment with inherited root config and explicit dependency graphs via dependency blocks. When environments differ in inputs, accounts, or stack count, Terragrunt scales better. Workspaces solve a narrower problem; Terragrunt solves org-scale repetition across many stacks.

Yes. Set TG_TF_PATH=tofu or configure terraform_binary in your terragrunt config. OpenTofu uses the same HCL syntax and backend blocks Terragrunt generates.

Duplicating the root remote_state block into every child file defeats the purpose; use include and find_in_parent_folders instead. Skipping mock_outputs breaks plans on fresh clones. Applying stacks manually in fifteen folders is error-prone; use terragrunt run-all apply with --terragrunt-non-interactive in CI. Never commit .terragrunt-cache. If a module folder already has a hand-written backend.tf, Terragrunt's generate block may conflict; pick one approach per stack with if_exists = "overwrite_terragrunt" as the clean default.

Never commit secrets in terragrunt.hcl files. Use environment variables, AWS SSM Parameter Store, or HashiCorp Vault instead. Terragrunt supports get_env() and sops integration patterns for pulling values at runtime without baking credentials into version control. Keep root and env.hcl focused on non-sensitive infrastructure settings like bucket names, regions, and CIDR blocks. Treat secret handling the same way you would in plain Terraform: inject at apply time, not in git history.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: