<?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: VincentChabran</title>
    <description>The latest articles on DEV Community by VincentChabran (@vincentchabran).</description>
    <link>https://dev.to/vincentchabran</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4018187%2F4eaa2608-d665-4c4b-ae95-f1a1889ca318.jpg</url>
      <title>DEV Community: VincentChabran</title>
      <link>https://dev.to/vincentchabran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vincentchabran"/>
    <language>en</language>
    <item>
      <title>One Block to Force a Default AWS Region Across Every Terragrunt Module</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Mon, 06 Jul 2026 17:32:57 +0000</pubDate>
      <link>https://dev.to/vincentchabran/one-block-to-force-a-default-aws-region-across-every-terragrunt-module-51ol</link>
      <guid>https://dev.to/vincentchabran/one-block-to-force-a-default-aws-region-across-every-terragrunt-module-51ol</guid>
      <description>&lt;p&gt;A quiet failure mode in multi-module Terragrunt setups: someone copies an existing module to bootstrap a new one, forgets to check the region in the provider config, and a resource lands in &lt;code&gt;us-east-1&lt;/code&gt; instead of &lt;code&gt;eu-west-1&lt;/code&gt; — three weeks later, someone's debugging why a Lambda can't reach a VPC that "should" be right there.&lt;/p&gt;

&lt;p&gt;This isn't a Terraform bug. It's a consequence of region being just another string that every module has to get right, independently, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default pattern (and why it's fragile)
&lt;/h2&gt;

&lt;p&gt;Most Terragrunt setups declare region per-project:&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;# projects/some-service/project.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;aws_region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the provider generation block in &lt;code&gt;root.hcl&lt;/code&gt; reads 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;# root.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;project&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;"project.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;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"&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;
provider "aws" {
  region = "${local.project.locals.aws_region}"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing wrong with this in isolation. The problem shows up at scale: with 20+ modules, &lt;code&gt;aws_region&lt;/code&gt; is set 20+ times, by hand, by whoever created each module — usually by copy-pasting an existing &lt;code&gt;project.hcl&lt;/code&gt; and hoping they remembered to check every field. New modules silently inherit whatever the copy-paste source happened to have, correct or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: a default that wins unless explicitly overridden
&lt;/h2&gt;

&lt;p&gt;Flip the default. &lt;code&gt;root.hcl&lt;/code&gt; computes the region with a fallback, so a project only needs to declare &lt;code&gt;aws_region&lt;/code&gt; when it's an actual, deliberate exception:&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="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&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;"project.hcl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="c1"&gt;# Falls back to the org-wide default if the project doesn't override it.&lt;/span&gt;
  &lt;span class="nx"&gt;aws_region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;try&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;project&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;aws_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&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"&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;
provider "aws" {
  region = "${local.aws_region}"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a brand-new &lt;code&gt;project.hcl&lt;/code&gt; can be &lt;em&gt;empty&lt;/em&gt; on the region question and still deploy to the right place by default:&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;# projects/brand-new-service/project.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;legacy_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"brand-new-service"&lt;/span&gt;
  &lt;span class="c1"&gt;# no aws_region — inherits eu-west-1 from root.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;An exception is now a &lt;em&gt;visible, deliberate&lt;/em&gt; line, not a silent inheritance from whatever was copy-pasted:&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;# projects/legacy-email-sender/project.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;legacy_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"legacy-email-sender"&lt;/span&gt;
  &lt;span class="nx"&gt;aws_region&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-north-1"&lt;/span&gt; &lt;span class="c1"&gt;# locked to this region — legacy third-party integration, see runbook §3&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is where the burden of proof sits. Before: every module has to actively get the region right. After: every module gets it right by doing nothing, and getting it "wrong" (i.e., different from the default) requires writing a line down and, ideally, a comment saying why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this generalizes
&lt;/h2&gt;

&lt;p&gt;Region is the clearest example because a wrong region is easy to notice, but the same shape applies to any setting that should be consistent across a growing set of modules and currently isn't enforced by anything except habit: default tags, default VPC, default log retention. Anywhere you find yourself saying "just copy an existing module's config and change what's different," that's a signal the &lt;em&gt;common&lt;/em&gt; part belongs in the root config with a &lt;code&gt;try()&lt;/code&gt; fallback, and only the actual differences belong in each project's own file.&lt;/p&gt;

&lt;p&gt;It costs one extra local in &lt;code&gt;root.hcl&lt;/code&gt;. It saves the debugging session three weeks from now.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Renaming Terragrunt Projects Without Migrating State</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Mon, 06 Jul 2026 17:31:33 +0000</pubDate>
      <link>https://dev.to/vincentchabran/renaming-terragrunt-projects-without-migrating-state-52jg</link>
      <guid>https://dev.to/vincentchabran/renaming-terragrunt-projects-without-migrating-state-52jg</guid>
      <description>&lt;p&gt;If you've run Terragrunt across more than a handful of modules for a while, you've hit this: you want to reorganize your repo — rename a folder, group projects differently, fix a naming mistake from six months ago — and Terragrunt's default behavior punishes you for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;By default, Terragrunt derives your remote state key from the file path of the module. Something like:&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="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"&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;"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;"eu-west-1"&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;&lt;code&gt;path_relative_to_include()&lt;/code&gt; is convenient until the day you move &lt;code&gt;projects/old-name/&lt;/code&gt; to &lt;code&gt;projects/new-name/&lt;/code&gt;. The key changes, Terraform can no longer find the state for those resources, and you're one &lt;code&gt;terragrunt apply&lt;/code&gt; away from either a pile of "already exists" errors or, worse, Terraform trying to recreate live infrastructure because it thinks it's gone.&lt;/p&gt;

&lt;p&gt;The usual fixes are painful: &lt;code&gt;terraform state mv&lt;/code&gt; per resource, or a full &lt;code&gt;terraform import&lt;/code&gt; pass. Both work, both are slow, both are one typo away from downtime on something that's just supposed to be a folder rename.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: decouple the state key from the file path
&lt;/h2&gt;

&lt;p&gt;Give every project a &lt;code&gt;project.hcl&lt;/code&gt; with a &lt;code&gt;legacy_prefix&lt;/code&gt; — the state key it should keep, independent of wherever the folder happens to live today:&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;# projects/new-name/project.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;legacy_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"old-name"&lt;/span&gt;   &lt;span class="c1"&gt;# the prefix this project's state has always used&lt;/span&gt;
  &lt;span class="nx"&gt;aws_region&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then have &lt;code&gt;root.hcl&lt;/code&gt; build the state key from that prefix instead of the live path:&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="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&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;"project.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;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"&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;"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;"${local.project.locals.legacy_prefix}/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;"eu-west-1"&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;Now the folder path is free to change. Rename it, move it, restructure the whole repo — the state key is pinned to &lt;code&gt;legacy_prefix&lt;/code&gt;, not to &lt;code&gt;path_relative_to_include()&lt;/code&gt;. A new project just sets &lt;code&gt;legacy_prefix&lt;/code&gt; to whatever it wants (usually matching its own path, since it has no history to preserve); an existing project keeps its old prefix forever, regardless of where the folder lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof, not just the theory
&lt;/h2&gt;

&lt;p&gt;Don't take "it should work" on faith. Before you consider a reorganization safe, run:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;on every module you touched, and the only acceptable output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: 0 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything else means the key computation didn't land on the same value as before, and you stop right there. This one-line proof requirement is what makes the whole pattern trustworthy enough to use on live infrastructure: you're not hoping the refactor is safe, you're verifying it before it touches anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is worth it
&lt;/h2&gt;

&lt;p&gt;This pattern earns its keep the moment you have more than a few Terragrunt modules and expect to reorganize them more than once — a new team convention, a naming scheme that didn't survive contact with reality, projects that outgrew their original folder structure. If you have three modules that will never move, plain &lt;code&gt;path_relative_to_include()&lt;/code&gt; is simpler and you don't need this.&lt;/p&gt;

&lt;p&gt;The core idea generalizes beyond Terragrunt: whenever a tool derives a stable identifier (a state key, a resource ID, a cache key) from something that isn't actually stable (a file path, a display name), that's worth decoupling explicitly — before the first painful migration forces you to do it under pressure.&lt;/p&gt;

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