<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Axel Mendoza</title>
    <description>The latest articles on DEV Community by Axel Mendoza (@consciousml).</description>
    <link>https://dev.to/consciousml</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1267199%2Fa579bccd-3b15-4785-9851-f76b36933f74.jpg</url>
      <title>DEV Community: Axel Mendoza</title>
      <link>https://dev.to/consciousml</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/consciousml"/>
    <language>en</language>
    <item>
      <title>Why I Use Terragrunt Over Terraform/OpenTofu in 2025</title>
      <dc:creator>Axel Mendoza</dc:creator>
      <pubDate>Fri, 08 Aug 2025 09:10:35 +0000</pubDate>
      <link>https://dev.to/consciousml/why-i-use-terragrunt-over-terraformopentofu-in-2025-4920</link>
      <guid>https://dev.to/consciousml/why-i-use-terragrunt-over-terraformopentofu-in-2025-4920</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxeiexri3618tmo8a4af.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxeiexri3618tmo8a4af.webp" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform is painful to deal with on large infrastructures.&lt;/li&gt;
&lt;li&gt;Code duplication, manual backend setup, and orchestration gets worse when your codebase grows.&lt;/li&gt;
&lt;li&gt;Terragrunt is a wrapper over Terraform that solves these issues, but has a negative reputation.&lt;/li&gt;
&lt;li&gt;I think this reputation is based on outdated information and misconceptions.&lt;/li&gt;
&lt;li&gt;The new Terragrunt Stacks feature is game-changing.&lt;/li&gt;
&lt;li&gt;It enables pattern-level infrastructure re-use. Something I've never seen before.&lt;/li&gt;
&lt;li&gt;In 2025, most pain points of Terragrunt adoption are solved.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've managed Terraform across multiple environments, you know the pain: massive code duplication between &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;prod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Manual backend state configuration. No built-in orchestration. Custom CI/CD scripts that break at the worst moments.&lt;/p&gt;

&lt;p&gt;Luckily, I tried Terragrunt some years ago. I'm so glad I did!&lt;/p&gt;

&lt;p&gt;It's a wrapper over Terraform to avoid code duplication and orchestrate your Terraform modules.&lt;/p&gt;

&lt;p&gt;I use it in all my projects now, and I'll never go back.&lt;/p&gt;

&lt;p&gt;Yet, when I discuss the “Terragrunt vs Terraform” topic with my peers on LinkedIn and Reddit, I'm always surprised by Terragrunt's bad reputation.&lt;/p&gt;

&lt;p&gt;It really doesn't reflect my own experience at all. So I dug deeper to understand why people have such a negative feeling about it.&lt;/p&gt;

&lt;p&gt;And guess what I found? Most of it stems from misconceptions and outdated knowledge from earlier versions.&lt;/p&gt;

&lt;p&gt;In 2025, most of Terragrunt's pain points have been addressed, and some game changer features have been released.&lt;/p&gt;

&lt;p&gt;In this article, I’ll cover the following to explain why I chose Terragrunt over Terraform:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
The pain points of native Terraform.
&lt;/li&gt;
&lt;li&gt;
How Terragrunt solves these pain points.
&lt;/li&gt;
&lt;li&gt;
The new Terragrunt Stacks feature.
&lt;/li&gt;
&lt;li&gt;
Why most objections to Terragrunt adoption don't hold up anymore.&lt;/li&gt;
&lt;li&gt;
A feature comparison table on Terragrunt vs Terraform.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Want to jump straight to implementation? Use my terragrunt &lt;a href="https://github.com/ConsciousML/terragrunt-template-stack" rel="noopener noreferrer"&gt;stack template&lt;/a&gt; and &lt;a href="https://github.com/ConsciousML/terragrunt-template-live" rel="noopener noreferrer"&gt;live template&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to &lt;a href="https://www.axelmendoza.com/posts/terraform-vs-terragrunt/" rel="noopener noreferrer"&gt;&lt;strong&gt;switch to my blog&lt;/strong&gt;&lt;/a&gt; where I designed the reading experience specifically for technical deep-dives like this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform Pain Points In Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Issues
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpkeg9bp6halomtazu7a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpkeg9bp6halomtazu7a.webp" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Code Duplication
&lt;/h4&gt;

&lt;p&gt;For managing multiple environments with Terraform, there are two possibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using folder duplication.
&lt;/li&gt;
&lt;li&gt;Using Terraform Workspaces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s start with the first option.&lt;/p&gt;

&lt;p&gt;In this scenario, you would build your Terraform modules under the &lt;code&gt;modules&lt;/code&gt; directory and create one directory per environment (&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;prod&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Resulting in the following tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;project/
├── modules/
│   └── vpc/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/
│   ├── dev/
│   │   ├── provider.tf      &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│   │   ├── backend.tf       &lt;span class="c"&gt;# Duplicated (different S3 key)&lt;/span&gt;
│   │   ├── variables.tf     &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│   │   ├── outputs.tf       &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│   │   └── main.tf          
│   ├── staging/
│   │   ├── provider.tf      &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│   │   ├── backend.tf       &lt;span class="c"&gt;# Duplicated (different S3 key)&lt;/span&gt;
│   │   ├── variables.tf     &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│   │   ├── outputs.tf       &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│   │   └── main.tf          
│   └── prod/
│       ├── provider.tf      &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│       ├── backend.tf       &lt;span class="c"&gt;# Duplicated (different S3 key)&lt;/span&gt;
│       ├── variables.tf     &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│       ├── outputs.tf       &lt;span class="c"&gt;# Duplicated&lt;/span&gt;
│       └── main.tf          &lt;span class="c"&gt;# module "vpc" { source = "../../modules/vpc" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;main.tf&lt;/code&gt; instantiates the vpc module. You can directly see the problem here: we need to redefine &lt;code&gt;provider.tf&lt;/code&gt;, &lt;code&gt;backend.tf&lt;/code&gt;, &lt;code&gt;variables.tf&lt;/code&gt;, and &lt;code&gt;outputs.tf&lt;/code&gt; in each environment folder.&lt;/p&gt;

&lt;p&gt;That's a lot of duplication! Any change to shared configuration needs to be manually copied to every environment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Manual Backend State Setup
&lt;/h4&gt;

&lt;p&gt;Following the example from the previous section, you would also need to write the &lt;a href="https://developer.hashicorp.com/terraform/language/backend" rel="noopener noreferrer"&gt;backend configuration&lt;/a&gt; for each environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dev/backend.tf&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myproject-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-locks-dev"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# staging/backend.tf  &lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myproject-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"staging/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-locks-staging"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each environment needs its own unique S3 key to avoid state conflicts.&lt;/p&gt;

&lt;p&gt;Here’s the worst: you can't use variables here! Terraform simply &lt;a href="https://github.com/hashicorp/terraform/issues/13022" rel="noopener noreferrer"&gt;doesn't allow them in backend blocks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You will also need to create the S3 bucket manually before anyone can run &lt;code&gt;terraform init&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Usually, this means running a separate Terraform configuration just for the backend infrastructure, or clicking through the AWS console.&lt;/p&gt;

&lt;p&gt;The whole process becomes a headache that only gets worse as you add more environments and team members.&lt;/p&gt;

&lt;p&gt;You got it right! Each time you need to create an additional environment, you’ll need to copy and paste an environment folder and manually change all the keys.&lt;/p&gt;

&lt;h4&gt;
  
  
  No Built-in Orchestration
&lt;/h4&gt;

&lt;p&gt;Terraform CLI can only work on one directory at a time. It has no idea that your modules depend on each other.&lt;/p&gt;

&lt;p&gt;Here’s a typical application stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dev/
├── vpc/           &lt;span class="c"&gt;# Must deploy FIRST&lt;/span&gt;
├── database/      &lt;span class="c"&gt;# Must deploy SECOND (needs VPC)  &lt;/span&gt;
├── app-servers/   &lt;span class="c"&gt;# Must deploy THIRD (needs database)&lt;/span&gt;
└── load-balancer/ &lt;span class="c"&gt;# Must deploy FOURTH (needs app servers)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To deploy this stack, you have to manually run commands in dependency order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;dev/vpc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terraform apply
&lt;span class="nb"&gt;cd&lt;/span&gt; ../database &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terraform apply  
&lt;span class="nb"&gt;cd&lt;/span&gt; ../app-servers &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terraform apply
&lt;span class="nb"&gt;cd&lt;/span&gt; ../load-balancer &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following such an error-prone process in a production environment is not realistic.&lt;/p&gt;

&lt;p&gt;HashiCorp introduced &lt;a href="https://www.hashicorp.com/en/blog/terraform-stacks-explained" rel="noopener noreferrer"&gt;Terraform Stacks&lt;/a&gt; to address dependency management and orchestration problems. But it's locked behind their paid &lt;a href="https://www.hashicorp.com/en/products/terraform" rel="noopener noreferrer"&gt;Terraform Cloud platform&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Scripts + CI/CD Is Not Ideal
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faevso2eykh0cnxgnk31z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faevso2eykh0cnxgnk31z.webp" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many teams think CI/CD pipelines will elegantly solve these Terraform limitations. I don’t believe it, and I’ll explain you why.&lt;/p&gt;

&lt;p&gt;You still need custom scripts to handle orchestration logic. CI/CD just moves the complexity into your YAML pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Infrastructure&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd environments/dev/vpc &amp;amp;&amp;amp; terraform apply -auto-approve&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd ../database &amp;amp;&amp;amp; terraform apply -auto-approve&lt;/span&gt;  
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd ../app-servers &amp;amp;&amp;amp; terraform apply -auto-approve&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cd ../load-balancer &amp;amp;&amp;amp; terraform apply -auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this solution, we port the orchestration logic in YAML instead of using purpose-built tools.&lt;/p&gt;

&lt;p&gt;Unfortunately, we still have all the problems we discussed earlier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy-paste directory structures
&lt;/li&gt;
&lt;li&gt;Manual backend configuration
&lt;/li&gt;
&lt;li&gt;No native dependencies between modules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s another tedious scenario: when adding a new module or changing the dependencies, you need to rewrite your custom orchestration scripts manually.&lt;/p&gt;

&lt;p&gt;I think CI/CD is excellent for deployment automation, but it's the wrong approach for solving the architectural flaws of Terraform.&lt;/p&gt;

&lt;p&gt;We’re simply adding another layer of complexity on top of the already existing problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform Workspaces Are Flawed
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe35s5w4mczxvwtzmngb2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe35s5w4mczxvwtzmngb2.webp" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/workspaces" rel="noopener noreferrer"&gt;Terraform Workspaces&lt;/a&gt; let you create separate state files for the same codebase. Each workspace maintains its own state while sharing the same configuration files.&lt;/p&gt;

&lt;p&gt;Many engineers use this feature as a way to maintain a single configuration and change the backend’s state file path according to the workspace.&lt;/p&gt;

&lt;p&gt;HashiCorp themselves don't recommend this practice in their official documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Workspaces alone are not a suitable tool for system decomposition because each subsystem should have its own separate configuration and backend." (&lt;a href="https://developer.hashicorp.com/terraform/cli/workspaces#:~:text=Workspaces%20alone%20are%20not%20a%20suitable%20tool%20for%20system%20decomposition%20because%20each%20subsystem%20should%20have%20its%20own%20separate%20configuration%20and%20backend." rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Workspaces only change state file names. They don't solve the architectural problems we've been discussing.&lt;/p&gt;

&lt;p&gt;Additionally, you’re not able to make your environment configurations diverge. For example, you need to use the same instances across &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt;. In realistic scenarios, you’ll want to create smaller machines during development.&lt;/p&gt;

&lt;p&gt;It’s also easy to forget that you’re not in your &lt;code&gt;dev&lt;/code&gt; workspace anymore and inadvertently affect your &lt;code&gt;prod&lt;/code&gt; environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform Cloud? Not Always the Solution
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.axelmendoza.com%2Fposts%2Fterraform-vs-terragrunt%2Fimages%2Fterraform_cloud_pricing_large.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.axelmendoza.com%2Fposts%2Fterraform-vs-terragrunt%2Fimages%2Fterraform_cloud_pricing_large.webp" alt="Terraform Cloud Pricing: resource ramp-up very quickly with multi-environment setups | source: [HashiCorp](https://www.hashicorp.com/en/pricing)" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Terraform Cloud Pricing: resource ramp-up very quickly with multi-environment setups (&lt;a href="https://www.hashicorp.com/en/pricing" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.hashicorp.com/en/products/terraform" rel="noopener noreferrer"&gt;Terraform Cloud&lt;/a&gt; (TFC) is HashiCorp's paid, proprietary platform. For teams focused on open-source tooling, this immediately rules it out.&lt;/p&gt;

&lt;p&gt;HashiCorp has introduced &lt;a href="https://www.hashicorp.com/en/blog/terraform-stacks-explained" rel="noopener noreferrer"&gt;Terraform Stacks&lt;/a&gt; to address some orchestration concerns.&lt;/p&gt;

&lt;p&gt;Stacks can help manage dependencies between components and reduce some of the manual coordination we've been talking about.&lt;/p&gt;

&lt;p&gt;However, Stacks is still in beta, has limited deployment options (500 resources max), and requires expensive tiers for full access.&lt;/p&gt;

&lt;p&gt;Terraform Cloud adds collaboration features and a web UI. &lt;/p&gt;

&lt;p&gt;From my experience, it makes sense to invest in TFC when collaboration becomes a real need, especially for large DevOps teams.&lt;/p&gt;

&lt;p&gt;For teams with less than 10 engineers, I think sticking to Terragrunt is a sound move: you get the same orchestration benefits without the vendor dependency or per-resource costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terragrunt for Production in 2025
&lt;/h2&gt;

&lt;p&gt;I don't get excited about many tools, but Terragrunt genuinely transformed how I manage Infrastructure as Code.&lt;/p&gt;

&lt;p&gt;Terragrunt is a wrapper over Terraform that tackles its limitations.&lt;/p&gt;

&lt;p&gt;After dealing with all the problems I just described, it solved every major pain point in a way that felt elegant and maintainable.&lt;/p&gt;

&lt;p&gt;To be clear, the discussion is not really "Terragrunt (TG) vs Terraform (TF)" but rather "native TF vs (TF + TG)". As TG uses TF under the hood.&lt;/p&gt;

&lt;p&gt;Let’s jump right into how it addresses each issue we've covered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terragrunt Solves Terraform’s Pain Points
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Avoids Code Duplication
&lt;/h4&gt;

&lt;p&gt;Terragrunt follows the &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself" rel="noopener noreferrer"&gt;Don't Repeat Yourself&lt;/a&gt; (DRY) principle.&lt;/p&gt;

&lt;p&gt;Instead of duplicating files across environments, you define shared configurations once and inherit them in child configurations.&lt;/p&gt;

&lt;p&gt;Here's what the same structure looks like with Terragrunt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;project/
├── root.hcl                 &lt;span class="c"&gt;# Shared configuration&lt;/span&gt;
├── modules/
│   └── vpc/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── live/
    ├── dev/
    │   └── terragrunt.hcl   &lt;span class="c"&gt;# Only environment-specific values&lt;/span&gt;
    └── prod/
        └── terragrunt.hcl    &lt;span class="c"&gt;# Only environment-specific values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;root.hcl&lt;/code&gt; defines the provider configuration once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="s2"&gt;"provider"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"provider.tf"&lt;/span&gt;
  &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite_terragrunt"&lt;/span&gt;
  &lt;span class="nx"&gt;contents&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
terraform {
  required_version = "&amp;gt;= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&amp;gt; 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# We'll cover remote_state configuration later&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each environment configuration instantiates the Terraform &lt;code&gt;vpc&lt;/code&gt; module while changing environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/prod/terragrunt.hcl&lt;/span&gt;
&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"root.hcl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../modules/vpc"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myproject-${local.env}"&lt;/span&gt;
  &lt;span class="nx"&gt;azs&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"us-west-2a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2b"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Availability zones list&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dev&lt;/code&gt; environment would be identical except &lt;code&gt;env = "dev"&lt;/code&gt; and only one availability zone, for example &lt;code&gt;azs = ["us-west-2a"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;include&lt;/code&gt; blocks let child configurations inherit everything from the parent. Terragrunt automatically generates the &lt;code&gt;provider.tf&lt;/code&gt; file in each environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prevents the duplication of the Terraform version, the AWS provider version, and the provider block.&lt;/p&gt;

&lt;h4&gt;
  
  
  Supports Automated Backend State Isolation
&lt;/h4&gt;

&lt;p&gt;Remember the manual backend configuration nightmare from earlier? Terragrunt eliminates all of that.&lt;/p&gt;

&lt;p&gt;You define the backend configuration once in your &lt;code&gt;root.hcl&lt;/code&gt; file, and Terragrunt automatically handles unique state isolation for each environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# root.hcl&lt;/span&gt;

&lt;span class="c1"&gt;# Configuration from earlier plus:&lt;/span&gt;
&lt;span class="nx"&gt;remote_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;

  &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend.tf"&lt;/span&gt;
    &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite_terragrunt"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myproject-terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path_relative_to_include()}/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-lock-table"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It literally does the coffee for you: Terragrunt automatically creates the S3 bucket and DynamoDB table if they don't exist.&lt;/p&gt;

&lt;p&gt;No more bootstrap scripts or clicking manually on the AWS console!&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;path_relative_to_include()&lt;/code&gt; function automatically generates unique S3 keys based on the directory structure.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;prod&lt;/code&gt; environment produces &lt;code&gt;prod/terraform.tfstate&lt;/code&gt;, while &lt;code&gt;dev&lt;/code&gt; generates &lt;code&gt;dev/terraform.tfstate&lt;/code&gt;. No manual key configuration, no copy-paste backend files!&lt;/p&gt;

&lt;p&gt;Unlike Terraform's backend blocks, Terragrunt can use dynamic values and local variables in backend configuration.&lt;/p&gt;

&lt;p&gt;You write the backend setup once, and it works correctly across all environments without any manual intervention.&lt;/p&gt;

&lt;h4&gt;
  
  
  Has Built-in Orchestration
&lt;/h4&gt;

&lt;p&gt;Remember the custom CI/CD scripts and manual orchestration from earlier? Terragrunt eliminates that entirely with native dependency management.&lt;/p&gt;

&lt;p&gt;Here's how you can structure related modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;project/
├── root.hcl
├── live/
│   ├── prod/
│   │   ├── env.hcl              &lt;span class="c"&gt;# Environment-specific values&lt;/span&gt;
│   │   ├── database/
│   │   │   └── terragrunt.hcl
│   │   └── app/
│   │       └── terragrunt.hcl
│   └── dev/
│       ├── env.hcl              &lt;span class="c"&gt;# Environment-specific values&lt;/span&gt;
│       ├── database/
│       │   └── terragrunt.hcl
│       └── app/
│           └── terragrunt.hcl
└── modules/
   ├── app/
   │   ├── main.tf
   │   └── …
   └── database/
       └── main.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environment-specific values are declared in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/prod/env.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the &lt;code&gt;database&lt;/code&gt; module with no dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/prod/database/terragrunt.hcl&lt;/span&gt;
&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"root.hcl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Read the locals from env.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read_terragrunt_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"env.hcl"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../../modules/database"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
  &lt;span class="nx"&gt;db_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db-${local.env}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app module declares its dependency and uses the &lt;code&gt;db_endpoint&lt;/code&gt; output from the database module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prod/app/terragrunt.hcl&lt;/span&gt;
&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"root.hcl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Read the locals from env.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read_terragrunt_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"env.hcl"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="s2"&gt;"database"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;config_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../database"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../modules/app"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
  &lt;span class="nx"&gt;db_host&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_endpoint&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terragrunt automatically understands that the database must deploy before the app.&lt;/p&gt;

&lt;p&gt;One command deploys everything in the correct order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terragrunt run-all apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much more elegant, isn’t it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Terragrunt Stacks Is a Game-Changer
&lt;/h3&gt;

&lt;p&gt;Gruntwork just released &lt;a href="https://terragrunt.gruntwork.io/docs/features/stacks/" rel="noopener noreferrer"&gt;Stacks&lt;/a&gt;, a new feature that makes Terragrunt even DRYer!&lt;/p&gt;

&lt;p&gt;I'm going to show you exactly how this changes everything.&lt;/p&gt;

&lt;h4&gt;
  
  
  Definitions
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.axelmendoza.com%2Fposts%2Fterraform-vs-terragrunt%2Fimages%2Fterragrunt_stack_large.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.axelmendoza.com%2Fposts%2Fterraform-vs-terragrunt%2Fimages%2Fterragrunt_stack_large.webp" alt="Example of a Terragrunt Stack | source: [Gruntwork](https://www.gruntwork.io/blog/the-road-to-terragrunt-1-0-stacks)" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me start with the basics first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit&lt;/strong&gt;: A Terragrunt wrapper around a Terraform module. Defines a single, deployable piece of infrastructure.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack&lt;/strong&gt;: Defines a collection of related units that can be reused.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Making The Example Even DRYer
&lt;/h4&gt;

&lt;p&gt;In the previous example, we still had to copy Terragrunt configurations in each environment directory. &lt;code&gt;prod/app/terragrunt.hcl&lt;/code&gt; and &lt;code&gt;dev/app/terragrunt.hcl&lt;/code&gt; were still duplicated.&lt;/p&gt;

&lt;p&gt;With Terragrunt Stacks, we can factorize that too!&lt;/p&gt;

&lt;p&gt;First, we will move &lt;code&gt;prod/app/terragrunt.hcl&lt;/code&gt; and &lt;code&gt;prod/database/terragrunt.hcl&lt;/code&gt; to the &lt;code&gt;units&lt;/code&gt; directory without changing their content.&lt;/p&gt;

&lt;p&gt;Next, we define the pattern (app + database) once in a stack file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stacks/web-app/terragrunt.stack.hcl&lt;/span&gt;
&lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="s2"&gt;"database"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::git@github.com/yourorg/infrastructure-modules.git//units/database?ref=v1.0.0"&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"database"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::git@github.com/yourorg/infrastructure-modules.git//units/app?ref=v1.0.0"&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then each environment just calls the stack with environment-specific values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/dev/terragrunt.stack.hcl&lt;/span&gt;
&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::git@github.com/yourorg/infrastructure-modules.git//stacks/web-app?ref=v1.0.1"&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"services"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# live/prod/terragrunt.stack.hcl&lt;/span&gt;
&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::git@github.com/yourorg/infrastructure-modules.git//stacks/web-app?ref=v1.0.0"&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"services"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prod&lt;/code&gt; and &lt;code&gt;dev&lt;/code&gt; are exactly identical. That's right! Because our units search for the &lt;code&gt;env.hcl&lt;/code&gt; file in parent directories, we don't even have to specify the environment.&lt;/p&gt;

&lt;p&gt;You can even point your &lt;code&gt;dev&lt;/code&gt; stack to a more recent version &lt;code&gt;v1.0.1&lt;/code&gt; and keep &lt;code&gt;prod&lt;/code&gt; on a stable one.&lt;/p&gt;

&lt;p&gt;This is already a significant improvement. We've eliminated the last bits of duplication.&lt;/p&gt;

&lt;p&gt;But honestly, I thought this was just a nice incremental upgrade.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;h4&gt;
  
  
  Nested Stacks
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.axelmendoza.com%2Fposts%2Fterraform-vs-terragrunt%2Fimages%2Fterragrunt_stacks_large.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.axelmendoza.com%2Fposts%2Fterraform-vs-terragrunt%2Fimages%2Fterragrunt_stacks_large.webp" alt="Nested Terragrunt Stacks example | source: [Gruntwork](https://www.gruntwork.io/blog/the-road-to-terragrunt-1-0-stacks)" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what I didn't expect: Terragrunt Stacks can be nested.&lt;/p&gt;

&lt;p&gt;For example, you could re-use your &lt;code&gt;web-app&lt;/code&gt; stack and add a &lt;code&gt;monitoring&lt;/code&gt; stack to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# live/prod/web-app-monitoring/terragrunt.stack.hcl&lt;/span&gt;
&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="s2"&gt;"web-app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::git@github.com/yourorg/infrastructure-modules.git//stacks/web-app?ref=v1.0.0"&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"services-web-app"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="s2"&gt;"monitoring"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::git@github.com/yourorg/infrastructure-modules.git//stacks/monitoring?ref=v1.0.0"&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"services-monitoring"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gruntwork is planning to incorporate the ability to &lt;a href="https://github.com/gruntwork-io/terragrunt/issues/4067" rel="noopener noreferrer"&gt;use stack outputs as dependencies&lt;/a&gt; in the future. This will allow for even greater modularity.&lt;/p&gt;

&lt;p&gt;Then it clicked: this isn't just about eliminating copy-paste anymore.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pattern Level Re-Use
&lt;/h4&gt;

&lt;p&gt;This is the fundamental shift I completely missed at first.&lt;/p&gt;

&lt;p&gt;With this new feature, platform teams aren't just sharing Terraform modules anymore. They're packaging and distributing complete infrastructure patterns.&lt;/p&gt;

&lt;p&gt;Want to deploy a new microservice? Don't think about databases, load balancers, monitoring, and networking separately. Just reference the microservice pattern and input your application-specific values.&lt;/p&gt;

&lt;p&gt;Need a data pipeline? Reference the data pipeline pattern. It comes with ingestion, processing, storage, and observability already wired together.&lt;/p&gt;

&lt;p&gt;Imagine the possibilities: creating a reusable stack that you could easily incorporate into your client’s infrastructures.&lt;/p&gt;

&lt;p&gt;Want to learn more? Read &lt;a href="https://www.gruntwork.io/blog/the-road-to-terragrunt-1-0-stacks" rel="noopener noreferrer"&gt;Gruntwork’s Terragrunt Stacks article&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terragrunt’s Pain Points
&lt;/h3&gt;

&lt;p&gt;I get it. Terragrunt has a reputation for being complex and frustrating to work with.&lt;/p&gt;

&lt;p&gt;Some of these objections are totally understandable, especially if your experience dates back to pre-2023.&lt;/p&gt;

&lt;p&gt;But here's my honest perspective on the main concerns people raise, and why I think the trade-offs are worth it in 2025.&lt;/p&gt;

&lt;h4&gt;
  
  
  Objection 1: Learning Curve
&lt;/h4&gt;

&lt;p&gt;Terragrunt is known in the community to have a steep learning curve.&lt;/p&gt;

&lt;p&gt;Yes, I must confess that the onboarding is harder initially.&lt;/p&gt;

&lt;p&gt;The inheritance model and generative nature feel painful compared to copy-paste when you're starting out.&lt;/p&gt;

&lt;p&gt;However, I’ve been building and maintaining complex multi-environment infrastructures for &lt;a href="https://en.wikipedia.org/wiki/Recommender_system" rel="noopener noreferrer"&gt;RecSys&lt;/a&gt;. I’ve found it’s been totally fine!&lt;/p&gt;

&lt;p&gt;I’ve even taught junior engineers to work and collaborate in such an environment.&lt;/p&gt;

&lt;p&gt;The beauty is, once it clicks, the payoff is huge.&lt;/p&gt;

&lt;h4&gt;
  
  
  Objection 2: Poor Performance
&lt;/h4&gt;

&lt;p&gt;I’ve found a recurring complaint that Terragrunt is slow and has performance issues.&lt;/p&gt;

&lt;p&gt;Terragrunt v0.80 delivered &lt;a href="https://www.gruntwork.io/blog/gruntwork-newsletter-may-2025#:~:text=42%25%20speed%20improvement%20and%20a%2043%25%20memory%20reduction." rel="noopener noreferrer"&gt;42% performance improvements, 43% memory reduction&lt;/a&gt;, and solved the O(n²) scaling issues that plagued earlier versions.&lt;/p&gt;

&lt;p&gt;I’ve never had performance issues deploying and maintaining multiple environments with thousands of resources.&lt;/p&gt;

&lt;h4&gt;
  
  
  Objection 3: Difficult Debugging
&lt;/h4&gt;

&lt;p&gt;Coming from Terraform, people may find the debugging process more difficult because Terragrunt has longer traces. It applies the infrastructure following a Directed Acyclic Graph (DAG).&lt;/p&gt;

&lt;p&gt;I think it is certainly easier to debug than the custom CI/CD workflows needed to work around Terraform’s caveats.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why I Think These Trade-offs Are Worth It
&lt;/h4&gt;

&lt;p&gt;Look, I'm not going to pretend Terragrunt has zero learning curve or that every team should adopt it.&lt;/p&gt;

&lt;p&gt;But when I see a &lt;a href="https://scalr.com/learning-center/top-5-most-common-terragrunt-issues-may-2025/" rel="noopener noreferrer"&gt;2025 article from Scalr&lt;/a&gt; ranking #1 on Google for &lt;code&gt;Terragrunt drawbacks&lt;/code&gt;, I’m baffled to find that all the highlighted problems have been resolved since 2021-2022.&lt;/p&gt;

&lt;p&gt;Unfortunately, this leads people to make decisions based on outdated information.&lt;/p&gt;

&lt;p&gt;At the time of the writing, Terragrunt has 215 open issues versus 2,350 closed issues. That's pretty solid for an open-source project.&lt;/p&gt;

&lt;p&gt;Bottom line: for teams managing multiple environments with orchestrated deployments, I think Terragrunt is absolutely worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform vs Terragrunt: Comparative Feature Matrix
&lt;/h2&gt;

&lt;p&gt;Here’s my attempt at building a Terragrunt versus Terraform table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Terragrunt v0.80+&lt;/th&gt;
&lt;th&gt;Terraform/OpenTofu&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workflow Simplicity&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High:&lt;/strong&gt; One command deploys an entire stack of modules defined in one file.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Low:&lt;/strong&gt; Requires manual scripts or CI workflows to coordinate multiple modules.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code Duplication&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Low:&lt;/strong&gt; Hierarchical configs via include blocks. Stacks eliminate copy-paste across environments. Modules reused without duplication.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High:&lt;/strong&gt; Module reuse exists, but backend/provider configs must be duplicated.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency Handling&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Automatic:&lt;/strong&gt; Built-in dependency graph with dependency blocks.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Manual:&lt;/strong&gt; No inter-module automatic dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State Isolation&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Strong&lt;/strong&gt;: Enforces one state per module. Auto-calculates unique remote state keys. Can auto-create backends. Small blast radius.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Manual:&lt;/strong&gt; State keys must be split manually. Large modules risk huge blast radius.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Moderate:&lt;/strong&gt; v0.80 delivered 42% faster execution. Still adds overhead, but now acceptable.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Baseline:&lt;/strong&gt; No wrapper overhead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Moderate:&lt;/strong&gt; Works with Terratest. Can capture plan outputs for validation. No built-in test framework.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High:&lt;/strong&gt; Has &lt;a href="https://developer.hashicorp.com/terraform/language/tests/mocking" rel="noopener noreferrer"&gt;provider mocking&lt;/a&gt;. Otherwise same testing approaches as Terragrunt.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning Curve&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High:&lt;/strong&gt; Must learn Terragrunt syntax. Initial training investment needed, but day-to-day usage becomes simpler.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Low:&lt;/strong&gt; Most DevOps engineers are already familiar with Terraform.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Community&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Moderate:&lt;/strong&gt; Active, engaged community with responsive maintainers.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High:&lt;/strong&gt; Massive community and learning resources.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://blog.upbound.io/infrastructure-as-code-with-tacos" rel="noopener noreferrer"&gt;TACOS&lt;/a&gt; Support&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Low:&lt;/strong&gt; Integration with &lt;a href="https://github.com/runatlantis/atlantis" rel="noopener noreferrer"&gt;Atlantis&lt;/a&gt; is not trivial. &lt;a href="https://github.com/diggerhq/digger" rel="noopener noreferrer"&gt;Digger&lt;/a&gt; seems like a promising OSS alternative. For enterprise features, &lt;a href="https://www.gruntwork.io/platform/pipelines" rel="noopener noreferrer"&gt;Gruntwork Pipeline&lt;/a&gt; has been designed for Terragrunt.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High:&lt;/strong&gt; Supported by most (if not all) TACOS platforms.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Terragrunt dominates features-wise (workflow, DRY, dependencies, state isolation) while Terraform/OpenTofu wins on familiarity, simplicity, performance, and TACOS support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;I'll be honest, Terragrunt has genuinely made my infrastructure experience easier and more enjoyable.&lt;/p&gt;

&lt;p&gt;When I read people complain about Terragrunt being "too complex" or "over-engineered," I’ve realized they're usually thinking about the 2019 version or relying on misconceptions.&lt;/p&gt;

&lt;p&gt;What happened in 2025 changed the entire conversation.&lt;/p&gt;

&lt;p&gt;Terragrunt evolved from a DRY orchestration tool to a pattern-level infrastructure platform. Meanwhile, Terraform remained focused on component-level management.&lt;/p&gt;

&lt;p&gt;Even before Stacks, I used to rely on Terragrunt instead of Terraform for reducing duplication and orchestration. Now? They're solving different problems entirely.&lt;/p&gt;

&lt;p&gt;Terragrunt isn't right for everyone. If you're managing a single environment with few resources, native Terraform is probably fine. If you're a small team that deploys infrastructure once a month, the learning curve might not be worth it.&lt;/p&gt;

&lt;p&gt;But if you're managing multiple environments, writing custom module orchestration, or constantly copying configuration between folders? Terragrunt solves this problem.&lt;/p&gt;

&lt;p&gt;That's exactly why I wrote this article. In the hope you find the same benefits in Terragrunt that I've found myself.&lt;/p&gt;

&lt;p&gt;Give it a try, I'd love to hear about your experience!&lt;/p&gt;

&lt;h2&gt;
  
  
  Stay in touch
&lt;/h2&gt;

&lt;p&gt;I hope you enjoyed this article as much as I enjoyed writing it!&lt;/p&gt;

&lt;p&gt;Feel free to DM me any feedback on &lt;a href="https://www.linkedin.com/in/axelmdz/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or &lt;a href="//axelmendoza@hotmail.fr"&gt;email&lt;/a&gt; me directly. It will be highly appreciated.&lt;/p&gt;

&lt;p&gt;That's what keeps me going!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.axelmendoza.com/subscribe" rel="noopener noreferrer"&gt;&lt;strong&gt;Subscribe&lt;/strong&gt;&lt;/a&gt; to get the latest articles from my blog delivered straight to your inbox!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>infrastructureascode</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
