<?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>Forem: Jeancy Joachim Mukaka</title>
    <description>The latest articles on Forem by Jeancy Joachim Mukaka (@jeancy).</description>
    <link>https://forem.com/jeancy</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%2F3822295%2F820b8506-9bc3-4eab-960d-cd36d34b1f2e.jpeg</url>
      <title>Forem: Jeancy Joachim Mukaka</title>
      <link>https://forem.com/jeancy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jeancy"/>
    <language>en</language>
    <item>
      <title>Stop Putting Everything in One Terraform State: Use Terragrunt Dependency Blocks</title>
      <dc:creator>Jeancy Joachim Mukaka</dc:creator>
      <pubDate>Wed, 29 Apr 2026 15:40:49 +0000</pubDate>
      <link>https://forem.com/jeancy/stop-putting-everything-in-one-terraform-state-use-terragrunt-dependency-blocks-1lhl</link>
      <guid>https://forem.com/jeancy/stop-putting-everything-in-one-terraform-state-use-terragrunt-dependency-blocks-1lhl</guid>
      <description>&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before getting started, make sure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic knowledge of Terraform (HCL syntax, resources, variables, remote state)&lt;/li&gt;
&lt;li&gt;Terraform &amp;gt;= 1.11 installed - &lt;a href="https://developer.hashicorp.com/terraform/install" rel="noopener noreferrer"&gt;Download&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Terragrunt installed - &lt;a href="https://terragrunt.gruntwork.io/docs/getting-started/quick-start/#install-terragrunt" rel="noopener noreferrer"&gt;Installation guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;An AWS CLI configured with sufficient permissions to create S3 buckets and EC2 instances&lt;/li&gt;
&lt;li&gt;Visual Studio Code with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=HashiCorp.terraform" rel="noopener noreferrer"&gt;HashiCorp Terraform extension&lt;/a&gt; for syntax hightlighting and autocompletion&lt;/li&gt;
&lt;li&gt;Read Part 1 of this series: &lt;a&gt;Stop Copy-Pasting Terraform State Configs: Use Terragrunt Instead&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In Part 1 of this series, we saw how Terragrunt eliminates the repetition of remote state backend configurations across environments. if you haven't read it yet, I recommend starting there - &lt;a&gt;Stop Copy-Pasting Terraform State Configs: Use Terragrunt Instead&lt;/a&gt;.&lt;br&gt;
Today, we go one step further.&lt;br&gt;
Most of Terraform projects start the same way: everything in one state file. Your VPC, your security groups, your EC2 instances, your RDS database, all managed together. It feels simple and convenient at first. But as your infrastructure grows, this approach becomes a hidden risk.&lt;br&gt;
Imagine this: a developer runs terraform apply to redeploy an EC2 instance that is rebuilt multiple times a day. Because everything is in the same state file, that single command now has access to your VPC configuration, your production database, and your security groups, resources that should never be touched during a routine EC2 redeployment.&lt;br&gt;
One wrong move, one bad variable, one interrupted apply, and you could accidentally destroy or corrupt critical infrastructure that takes hours to rebuild.&lt;br&gt;
In this article, we'll explore how Terragrunt's dependency blocks allow you to split your Terraform state between infrastructure components, so that frequently changed resources never put your critical infrastructure at risk.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem: One State File to Rule Them All
&lt;/h2&gt;

&lt;p&gt;When everything lives in a single Terraform state file, your infrastructure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;single state file
├── VPC                ← modified once a month
├── Subnets            ← modified once a month
├── Security Groups    ← modified occasionally
├── RDS Database       ← critical, rarely modified
└── EC2 Instances      ← modified 10x per day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every terraform apply, no matter how small, touches this single state file. This creates three serious problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Problem 1: &lt;em&gt;Blast radius&lt;/em&gt;: If something goes wrong during a routine EC2 redeployment, the entire state file is at risk. A corrupted state means Terraform loses track of all your resources, VPC, database, everything.&lt;/li&gt;
&lt;li&gt;Problem 2: &lt;em&gt;No separation of concerns&lt;/em&gt;: A junior developer redeploying an EC2 instance has the same Terraform access as a senior engineer modifying the VPC. There is no natural boundary between critical and non-critical infrastructure.&lt;/li&gt;
&lt;li&gt;Problem 3: &lt;em&gt;Slow operations As your infrastructure grows&lt;/em&gt;: Terraform has to refresh the state of every single resource on every terraform plan or terraform apply, even if you're only changing one EC2 instance. This makes operations increasingly slow.
The solution is to split your state between infrastructure components, and Terragrunt dependency blocks make this both simple and elegant.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Separate States with Dependency Blocks
&lt;/h2&gt;

&lt;p&gt;Instead of one monolithic state file, Terragrunt allows you to give each infrastructure component its own isolated state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vpc/                    ← state 1 — modified rarely
security-groups/        ← state 2 — modified occasionally
rds/                    ← state 3 — critical, rarely modified
ec2/                    ← state 4 — modified daily
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component lives in its own folder with its own terragrunt.hcl file and its own state file in S3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3://my-terraform-state/
├── dev/vpc/terraform.tfstate
├── dev/security-groups/terraform.tfstate
├── dev/rds/terraform.tfstate
└── dev/ec2/terraform.tfstate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when a developer runs terraform apply on the EC2 component, only the EC2 state is touched. The VPC, the database, and the security groups are completely isolated and protected.&lt;br&gt;
&lt;strong&gt;But here's the challenge&lt;/strong&gt;: if components are separated, how does the EC2 module know the subnet ID from the VPC module? How does the security group know the VPC ID?&lt;br&gt;
This is where Terragrunt's dependency block comes in.&lt;br&gt;
The dependency block allows a component to &lt;strong&gt;read the outputs of another component&lt;/strong&gt; without sharing the same state 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;# ec2/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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Declare dependency on VPC component&lt;/span&gt;
&lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&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;"../vpc"&lt;/span&gt;

  &lt;span class="nx"&gt;mock_outputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"subnet-00000000"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Declare dependency on security groups component&lt;/span&gt;
&lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="s2"&gt;"security_groups"&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;"../security-groups"&lt;/span&gt;

  &lt;span class="nx"&gt;mock_outputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sg_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sg-00000000"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Use outputs from dependencies as inputs&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;subnet_id&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;vpc&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;subnet_id&lt;/span&gt;
  &lt;span class="nx"&gt;security_group&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;security_groups&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;sg_id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.micro"&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;"dev"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice here: &lt;br&gt;
First, config_path points to the folder of the dependency, not a specific file. Terragrunt knows where to find the outputs.&lt;br&gt;
Second, mock_outpouts provides fake values for when you run terragrunt plan without the dependencies being deployed yet. this allows you to validate your configuration before deploying anything.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Complete Project Structure
&lt;/h2&gt;

&lt;p&gt;Here is the complete project structure for a dev environment with separated state files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── terragrunt.hcl                    # Root — remote state defined once
└── dev/
    ├── vpc/
    │   ├── terragrunt.hcl
    │   └── main.tf                   # VPC + Subnets
    ├── security-groups/
    │   ├── terragrunt.hcl
    │   └── main.tf                   # Security Groups
    ├── rds/
    │   ├── terragrunt.hcl
    │   └── main.tf                   # RDS Database
    └── ec2/
        ├── terragrunt.hcl
        └── main.tf                   # EC2 Instances
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component exposes its key values through Terraform outputs, which are then consumed by dependent components via the dependency block.&lt;br&gt;
Here is how the dependency chain flows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vpc/
  └── outputs: vpc_id, subnet_id
        │
        ├─────────────────────────┐
        ▼                         ▼
security-groups/                 ec2/
  inputs: vpc_id            inputs: subnet_id
  outputs: sg_id                  │
        │                         │
        └─────────────────────────┘
                    ▼
                  ec2/
             inputs: sg_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terragrunt reads this dependency graph automatically and deploys components in the correct order: VPC first, then security groups, then EC2. You never have to think about the deployment order manually.&lt;br&gt;
The VPC component is the simplest, it has no dependencies:&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/vpc/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="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="s2"&gt;"dev"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And its main.tf exposes the values that other components need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dev/vpc/main.tf&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"vpc_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"subnet_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outputs are what the dependency block reads when EC2 asks for dependency.vpc.outputs.subnet_id.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The full code for this project structure is available in the &lt;a href="https://github.com/JM01lab/aws-terragrunt-examples/tree/main/part2-component-isolation/dev" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deploying with Dependency Blocks
&lt;/h2&gt;

&lt;p&gt;Once your structure is in place, deploying is as simple as one command from the dev/ folder:&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;Terragrunt automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads the dependency graph across all components&lt;/li&gt;
&lt;li&gt;Deploys in the correct order — VPC → Security Groups → EC2&lt;/li&gt;
&lt;li&gt;Passes outputs from one component as inputs to the next&lt;/li&gt;
&lt;li&gt;Creates a separate state file in S3 for each component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also target individual components:&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="c"&gt;# Redeploy only EC2 — VPC and Security Groups are untouched&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;dev/ec2/
terragrunt apply

&lt;span class="c"&gt;# Check outputs of a specific component&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;dev/vpc/
terragrunt output

&lt;span class="c"&gt;# Destroy in reverse order automatically&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;dev/
terragrunt run-all destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notice the power here&lt;/strong&gt;: when you run terragrunt apply in dev/ec2/ only, Terraform touches only the EC2 state file. Your VPC and database state files are completely safe, even if something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mock_outputs - Why They Matter
&lt;/h3&gt;

&lt;p&gt;When you run terragrunt plan on EC2 component before the VPC is deployed, Terragrunt needs values for subnet.id and sg.id to validate the configuration. Since the real values don't exist yet, mock_outputs provides temporary placeholders:&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;dependency&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&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;"../vpc"&lt;/span&gt;

  &lt;span class="nx"&gt;mock_outputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"subnet-00000000"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;mock_outputs_allowed_terraform_commands&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"validate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"plan"&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;The mock_outputs_allowed_terraform_commands parameter ensures that mock values are only used during validate and plan, never during apply. This prevents accidental deployments with fake values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Note on Security&lt;/strong&gt;&lt;br&gt;
Before wrapping up, a quick but important note on security, raised by &lt;a href="https://dev.to/sqlxpert"&gt;Paul Marcelin&lt;/a&gt; in the comments of Part 1.&lt;br&gt;
When your state files are separated by component, you have a natural opportunity to apply different IAM permissions per component. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers can have read/write access to the EC2 state file&lt;/li&gt;
&lt;li&gt;Only senior engineers or CI/CD pipelines can access the VPC and RDS state files&lt;/li&gt;
&lt;li&gt;Production state files can be encrypted with dedicated KMS keys per environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a significant security improvement over a single state file where everyone has access to everything. Separating state files is the first step, securing them with IAM policies and KMS encryption is the natural next step.&lt;br&gt;
For a deep dive on Terraform state file security, I recommend this LinkedIn post by Yaroslav Naumenko: &lt;a href="https://www.linkedin.com/posts/ynaumenko_terraform-terragrunt-iac-share-7439240151249231872-OMca?utm_source=social_share_send&amp;amp;utm_medium=member_desktop_web&amp;amp;rcm=ACoAADjV-nEBBut00mj1Y619QEAPOvT8nA_vQb8" rel="noopener noreferrer"&gt;"Your Terraform state file is a secret. Most teams don't treat it that way."&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Let's recap what we covered in this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The problem:  a monolithic state file creates a dangerous blast radius where routine operations can accidentally affect critical infrastructure&lt;/li&gt;
&lt;li&gt;The solution: Terragrunt dependency blocks allow each component to have its own isolated state file&lt;/li&gt;
&lt;li&gt;The dependency block reads outputs from other components without sharing their state&lt;/li&gt;
&lt;li&gt;mock_outputs allow you to validate configurations before dependencies are deployed&lt;/li&gt;
&lt;li&gt;terragrunt run-all apply automatically respects the dependency order&lt;/li&gt;
&lt;li&gt;Separating state files is also the foundation for better security, different IAM permissions per component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, Part 1 and Part 2 give you a complete Terragrunt workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Part 1 → One root terragrunt.hcl    = No repeated backend configs
Part 2 → Dependency blocks          = No more monolithic state files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you found this helpful, share it and follow me for the next article in the series.&lt;br&gt;
The code for this article is available on my &lt;a href="https://github.com/JM01lab/aws-terragrunt-examples/tree/main/part2-component-isolation/dev" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>devops</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>Stop Copy-Pasting Terraform State Configs: Use Terragrunt instead</title>
      <dc:creator>Jeancy Joachim Mukaka</dc:creator>
      <pubDate>Mon, 13 Apr 2026 14:31:31 +0000</pubDate>
      <link>https://forem.com/jeancy/stop-copy-pasting-terraform-state-configs-use-terragrunt-instead-ana</link>
      <guid>https://forem.com/jeancy/stop-copy-pasting-terraform-state-configs-use-terragrunt-instead-ana</guid>
      <description>&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;br&gt;
Before getting started, make sure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic knowledge of Terraform (HCL Syntax, resources, variables, remote state), the full prerequisite code is available in the &lt;a href="https://github.com/JM01lab/aws-terraform-infrastructure" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Terraform installed on your machine (v0.12 or higher)&lt;/li&gt;
&lt;li&gt;Terragrunt installed, check the &lt;a href="https://terragrunt.gruntwork.io/docs/getting-started/install/" rel="noopener noreferrer"&gt;official installation guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;An AWS account with suffficient permissions to create S3  buckets and DynamoDB tables&lt;/li&gt;
&lt;li&gt;AWS CLI configured with your credentials (aws configure)&lt;/li&gt;
&lt;li&gt;Visual Studio Codes as your code editor, with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=HashiCorp.terraform" rel="noopener noreferrer"&gt;HashiCorp Terraform extension&lt;/a&gt; for syntax highlighting and autocompletion. &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you have been working with Terraform for a while, you have probably faced this situation: you have a working configuration for your dev environment, and now you need to deploy the same infrastructure to staging and prod. So you copy the folder, update a few values, including the remote state backend configuration, and repeat. It works, but something feels wrong.&lt;br&gt;
That "something" is a violation of the DRY principle, don't repeat yourself. Every time you duplicate your backend configuration, you create a new opportunity for error and a new file to maintain. &lt;br&gt;
In this article, we will explore how Terragrunt solves this problem by allowing you to define your remote state configuration once and reuse it across all your environments.&lt;br&gt;
If you are new to terraform, I recommend exploring &lt;a href="https://github.com/JM01lab/aws-terraform-infrastructure" rel="noopener noreferrer"&gt;prerequisite code on GitHub&lt;/a&gt; before diving in.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;The Problem: Repeat Remote State&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When managing multiple environments with Terraform, most developers end up with a structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;environments/
├── dev/
│   └── main.tf
├── staging/
│   └── main.tf
└── prod/
    └── main.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And inside each main.tf, the same backend block appears with only one line changing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dev/main.tf&lt;/span&gt;
&lt;span class="k"&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;"my-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;use_lockfile&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;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;The same block is then copy-pasted into staging/main.tf and prod/main.tf, with only the key value changing (staging/terraform.tfstate, prod/terraform.tfsate). That's three files, three times the same configuration. And if you ever need to change the bucket name, the region, or encryption, you have to update every single file manually. This is exactly the kind of repetition that leads to human error and maintenance nightmares.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is Terragrunt?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Terragrunt is a thin wrapper around Terraform, developed by Gruntwork, It doesn't replace Terraform, it enhances it by providing additional tools to keep your configurations DRY, manageable, and consistent across environments.&lt;br&gt;
Think of it this way: Terraform is the engine, and Terragrunt is the intelligent framework built around it. You still write the same HCL code you know, but Terragrunt handless the repetitive parts for you.&lt;br&gt;
With Terragrunt you can: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define your remote state configuration once and reuse it across all environments&lt;/li&gt;
&lt;li&gt;Automatically create your S3 bucket and DynamoDB table if they don't exist&lt;/li&gt;
&lt;li&gt;Deploy multiple environments with a single command&lt;/li&gt;
&lt;li&gt;Keep your codebase clean, readable, and easy to maintain
The key concept we'll focus on in this article is the remote_state block — the feature that eliminates repeated backend configurations across environments.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;The Solution: Centralized Remote State with Terragrunt&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of repeating the backend configuration in every environment, Terragrunt lets you define it once in a root terragrunt hcl file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── terragrunt.hcl        ← defined once here
├── dev/
│   └── terragrunt.hcl    ← only what changes
├── staging/
│   └── terragrunt.hcl    ← only what changes
└── prod/
    └── terragrunt.hcl    ← only what changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root terragrunt.hcl contains the full remote state configuration:&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;# terragrunt.hcl (root)&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;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;"your-terraform-state-bucket"&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;use_lockfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# Native S3 locking — replaces DynamoDB (Terraform v1.11+)&lt;/span&gt;
&lt;span class="p"&gt;}&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; As of Terraform v1.11, DynamoDB-based state &lt;br&gt;
locking is deprecated. This example uses native S3 locking &lt;br&gt;
via &lt;code&gt;use_lockfile = true&lt;/code&gt;. Thanks to Paul Marcelin for &lt;br&gt;
pointing this out!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each environment file simply inherits from the root. Here is the dev example:&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/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="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="s2"&gt;"dev"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.micro"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The staging and prod files follow the exact same structure, only the environment and instance_type values change. That's it. Three environments, three small files, each containing only what is unique to that environment. The backend configuration lives in one place and is never repeated.&lt;br&gt;
 &lt;em&gt;The full project structure with all environments is available in the GitHub repository.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Key Terragrunt Functions Explained&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Two functions make all of this possible. Understanding them is the key to mastering Terragrunt.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;find_in_parent_folders()&lt;/strong&gt;
This funtcion automatically searches parent directories for the root terragrunt.hcl file. It allows each environment file to inherit the root configuration without hardcoding the path.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&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="c1"&gt;# finds ../../terragrunt.hcl automatically&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No matter how deeply nested your environment folder is, Terragrunt will always find the root configuration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;path_relative_to_include()&lt;/strong&gt;
This is the function that makes the state key dynamic. It returns the relative path of the current environment folder from the root.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path_relative_to_include()}/terraform.tfstate"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Concretely, this means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Environment folder | Generated state key |
| :--- | :--- |
| `dev/` | `dev/terraform.tfstate` |
| `staging/` | `staging/terraform.tfstate` |
| `prod/` | `prod/terraform.tfstate` |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each environment automatically gets its own isolated state file in S3, with zero manual configuration.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;The generate Block&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;This block is often overlooked but extremely powerful. It tells Terragrunt to automatically generate a backend.tf file in each environment folder before running Terraform.&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="err"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you never have to manually write a backend.tf file again. Terragrunt generates it for you, every time, with the correct values.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Deploying All Environments&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;once your configuration is in place, deploying all environments is as simple as running a single command from the root folder:&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="c"&gt;# Deploy all environments at once&lt;/span&gt;
terragrunt run-all apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terragrunt will automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect all terragrunt.hcl files in subdirectories&lt;/li&gt;
&lt;li&gt;Run terraform init for each environment&lt;/li&gt;
&lt;li&gt;Deploy each environment in the correct order&lt;/li&gt;
&lt;li&gt;Create the S3 bucket and DynamoDB table if they don't exist yet
you can also target a specific environment:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy only dev&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;dev/
terragrunt apply

&lt;span class="c"&gt;# Check outputs across all environments&lt;/span&gt;
terragrunt run-all output

&lt;span class="c"&gt;# Destroy all environments&lt;/span&gt;
terragrunt run-all destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare this to the old approach where you had to navigate into each folder manually, run terraform init, then terraform apply, and repeat for every environment. With Terragrunt, that entire workflow collapses into one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Managing Terraform remote state across multiple environments doesn't have to be painful. With Terragrunt's remotestate block, &lt;code&gt;find_in_parent_folders()&lt;/code&gt;, and &lt;code&gt;path_relative_to_include()&lt;/code&gt;, you can define your backend configuration once and let Terragrunt handle the rest.&lt;br&gt;
Let's recap what we covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The problem: repeated backend configuration across environments violate DRY principle&lt;/li&gt;
&lt;li&gt;The solution: a single root terragrunt.hcl that centralizes the remote state configuration&lt;/li&gt;
&lt;li&gt;The magic functions: &lt;code&gt;find_in_parent_folders()&lt;/code&gt; and &lt;code&gt;path_relative_to_include()&lt;/code&gt;that make everything dynamic&lt;/li&gt;
&lt;li&gt;The power of Terragrunt run-all apply: deploy all environments in one command.
This is just the beginning of what Terragrunt can do. In the next article, Part 2, we will go deeper and explore how to split your Terraform dependency blocks. You will learn why putting your VPC, your security groups, and your EC2 instances in the same state file is a risk and how to fix it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this article helpful, feel free to share it and follow me for Part 2. The code for this article is available on &lt;a href="https://github.com/JM01lab/aws-terragrunt-examples" rel="noopener noreferrer"&gt;my GitHub&lt;/a&gt;.&lt;/p&gt;

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