<?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: Bartłomiej Danek</title>
    <description>The latest articles on DEV Community by Bartłomiej Danek (@bard).</description>
    <link>https://dev.to/bard</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%2F176145%2Ffcbab39e-a7ce-49dd-8505-b8b8e2146e4e.png</url>
      <title>DEV Community: Bartłomiej Danek</title>
      <link>https://dev.to/bard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bard"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Mon, 04 May 2026 18:41:00 +0000</pubDate>
      <link>https://dev.to/bard/-20i5</link>
      <guid>https://dev.to/bard/-20i5</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bard/hcl-linter-001-alpha-is-out-1k59" class="crayons-story__hidden-navigation-link"&gt;hcl-linter 0.0.1-alpha is out&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bard" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F176145%2Ffcbab39e-a7ce-49dd-8505-b8b8e2146e4e.png" alt="bard profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bard" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Bartłomiej Danek
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Bartłomiej Danek
                
              
              &lt;div id="story-author-preview-content-3610597" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bard" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F176145%2Ffcbab39e-a7ce-49dd-8505-b8b8e2146e4e.png" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Bartłomiej Danek&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bard/hcl-linter-001-alpha-is-out-1k59" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 4&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bard/hcl-linter-001-alpha-is-out-1k59" id="article-link-3610597"&gt;
          hcl-linter 0.0.1-alpha is out
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/terraform"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;terraform&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hcl"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hcl&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/terragrunt"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;terragrunt&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/bard/hcl-linter-001-alpha-is-out-1k59#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devops</category>
      <category>opensource</category>
      <category>terraform</category>
      <category>tooling</category>
    </item>
    <item>
      <title>hcl-linter 0.0.1-alpha is out</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Mon, 04 May 2026 18:32:55 +0000</pubDate>
      <link>https://dev.to/bard/hcl-linter-001-alpha-is-out-1k59</link>
      <guid>https://dev.to/bard/hcl-linter-001-alpha-is-out-1k59</guid>
      <description>&lt;h1&gt;
  
  
  hcl-linter 0.0.1-alpha is out
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;terraform fmt&lt;/code&gt; only touches whitespace. In a codebase with dozens of contributors and hundreds of HCL files, block order drifts, naming conventions split, and required fields go missing quietly. I wanted something that could catch and fix that automatically - so I built &lt;a href="https://github.com/bard-works/hcl-linter" rel="noopener noreferrer"&gt;&lt;code&gt;hcl-linter&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first run fixed over 1000 &lt;code&gt;terragrunt.hcl&lt;/code&gt; files.&lt;/p&gt;

&lt;h2&gt;
  
  
  getting started
&lt;/h2&gt;

&lt;p&gt;Two commands to go from nothing to a first pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hcl-linter init             &lt;span class="c"&gt;# scaffold .hcl-linter/ from your existing files&lt;/span&gt;
hcl-linter fix ./ &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="c"&gt;# preview what would change&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; walks the project, groups files by basename, and writes a &lt;code&gt;.hcl-linter/default.hcl&lt;/code&gt; plus one &lt;code&gt;extends = "default"&lt;/code&gt; override per unique filename. No config needed for a first formatting pass either - &lt;code&gt;--format&lt;/code&gt; applies opinionated defaults without any config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hcl-linter fix ./ &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  block order
&lt;/h2&gt;

&lt;p&gt;Wrong block order is the most common drift. &lt;code&gt;block_order&lt;/code&gt; catches and auto-fixes 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;# violation - terraform before include&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;"git::https://github.com/example/vpc.git"&lt;/span&gt;
&lt;span class="p"&gt;}&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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;hcl-linter fix&lt;/code&gt;:&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;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;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;"git::https://github.com/example/vpc.git"&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;"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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Config:&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;rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;block_order&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&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;order&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"locals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"dependency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"inputs"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  naming
&lt;/h2&gt;

&lt;p&gt;Hyphens in block labels break &lt;code&gt;dependency.my-vpc.outputs.id&lt;/code&gt; references in locals. &lt;code&gt;name_validation&lt;/code&gt; flags and auto-fixes them - hyphens replaced with underscores, references in &lt;code&gt;locals&lt;/code&gt; updated too:&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;# violation&lt;/span&gt;
&lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="s2"&gt;"my-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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="s2"&gt;"db-primary"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&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;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"^[a-z][a-z0-9_]*$"&lt;/span&gt;
    &lt;span class="nx"&gt;blocks&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dependency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"include"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  required fields
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;required_fields&lt;/code&gt; catches missing attributes when a block is present and auto-adds them:&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;# violation - expose missing on include blocks&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;include&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;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../vpc"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After fix:&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;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="nx"&gt;expose&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="nx"&gt;include&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;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;expose&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_fields&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;expose&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  count and for_each
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;count_for_each&lt;/code&gt; warns on patterns that silently disable resources - &lt;code&gt;count = 0&lt;/code&gt; or &lt;code&gt;for_each = {}&lt;/code&gt; left behind after feature flags, and &lt;code&gt;count&lt;/code&gt; + &lt;code&gt;for_each&lt;/code&gt; used on the same block (Terraform rejects this at plan time):&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;         &lt;span class="c1"&gt;# WARNING: resource will not be created&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-12345"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"db"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_configs&lt;/span&gt;  &lt;span class="c1"&gt;# ERROR: cannot use both count and for_each&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not fixable - &lt;code&gt;count = 0&lt;/code&gt; is sometimes intentional. The rule flags it so the intent is explicit.&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;rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count_for_each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&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;warn_on_count_zero&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;warn_on_empty_for_each&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;warn_on_conflict&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;h2&gt;
  
  
  get_env without default
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hcl_functions&lt;/code&gt; warns on &lt;code&gt;get_env()&lt;/code&gt; calls with no fallback - those fail silently in CI if the variable is unset. It also validates that &lt;code&gt;find_in_parent_folders()&lt;/code&gt; targets exist on disk:&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;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;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MY_SECRET"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="c1"&gt;# WARNING: no default&lt;/span&gt;
  &lt;span class="nx"&gt;root&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;"nonexistent.hcl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# ERROR: file not found&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PORT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"8080"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# ok&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hcl_functions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&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;find_in_parent_folders_exists&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;get_env_has_default&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;h2&gt;
  
  
  deprecated terraform hooks
&lt;/h2&gt;

&lt;p&gt;Terragrunt renamed &lt;code&gt;before_hook&lt;/code&gt; / &lt;code&gt;after_hook&lt;/code&gt; to &lt;code&gt;before_hooks&lt;/code&gt; / &lt;code&gt;after_hooks&lt;/code&gt; a while back. &lt;code&gt;terraform_block&lt;/code&gt; catches the old names still in use:&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;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git::https://github.com/example/vpc.git"&lt;/span&gt;

  &lt;span class="nx"&gt;before_hook&lt;/span&gt; &lt;span class="s2"&gt;"run_fmt"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;      &lt;span class="c1"&gt;# WARNING: deprecated, use before_hooks&lt;/span&gt;
    &lt;span class="nx"&gt;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;"terragrunt"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;execute&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"terraform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"fmt"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;terraform_block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&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;no_deprecated_fields&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;h2&gt;
  
  
  dependency outputs
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dependency_outputs&lt;/code&gt; statically validates that &lt;code&gt;dependency.x.outputs.y&lt;/code&gt; references exist in the target module - no plan needed:&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;"./mock-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;vpc_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;vpc_id&lt;/span&gt;       &lt;span class="c1"&gt;# ok&lt;/span&gt;
  &lt;span class="nx"&gt;bad_output&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;nonexistent&lt;/span&gt;  &lt;span class="c1"&gt;# WARNING: not declared&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walks the dependency chain, parses output blocks from &lt;code&gt;.tf&lt;/code&gt; files. Supports &lt;code&gt;.mock-outputs.json&lt;/code&gt; for modules where upstream state is not available locally.&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;rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dependency_outputs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&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;h2&gt;
  
  
  CI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;check&lt;/code&gt; fails the build on any rule violation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hcl-linter check ./infra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fix --dry-run&lt;/code&gt; catches everything &lt;code&gt;check&lt;/code&gt; does, plus byte-level drift from fixable rules - if someone committed without running &lt;code&gt;fix&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hcl-linter fix ./infra &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are composable: run &lt;code&gt;check&lt;/code&gt; for semantic issues and &lt;code&gt;fix --dry-run&lt;/code&gt; for formatting drift as separate steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/bard-works/hcl-linter@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or grab a binary from &lt;a href="https://github.com/bard-works/hcl-linter/releases" rel="noopener noreferrer"&gt;releases&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://github.com/bard-works/hcl-linter/releases/latest/download/hcl-linter_linux_amd64.tar.gz | &lt;span class="nb"&gt;tar &lt;/span&gt;xz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source and full docs:  &lt;a href="https://github.com/bard-works/hcl-linter" rel="noopener noreferrer"&gt;bard-works/hcl-linter&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/hcl-linter-release/" rel="noopener noreferrer"&gt;https://bard.sh/posts/hcl-linter-release/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>hcl</category>
      <category>terragrunt</category>
      <category>devops</category>
    </item>
    <item>
      <title>Beyond Slack Analytics: Building Custom Engagement Metrics with Webhooks, Prometheus, and Grafana</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:01:24 +0000</pubDate>
      <link>https://dev.to/bard/beyond-slack-analytics-building-custom-engagement-metrics-with-webhooks-prometheus-and-grafana-2onj</link>
      <guid>https://dev.to/bard/beyond-slack-analytics-building-custom-engagement-metrics-with-webhooks-prometheus-and-grafana-2onj</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Slack's native analytics won't tell you which threads die, which requests get ignored, or how yyour team's collaboration patterns evolve. I built a Go webhook handler that captures every reaction, reply, and thread via Slack's event API, exports it to Prometheus, and visualizes it in Grafana - giving me metrics Slack never will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wake-Up Call
&lt;/h2&gt;

&lt;p&gt;It started with a post-incident review. We'd just recovered from a 3-hour outage, and I was asked the usual questions: How fast did we respond? Which channels were involved? Were there warning signs in our internal Slack discussions before things broke?&lt;/p&gt;

&lt;p&gt;I opened Slack's analytics dashboard. Member counts. Message volume. Channel growth. Vanity metrics that told me nothing about the incident or our response patterns.&lt;/p&gt;

&lt;p&gt;I couldn't answer basic questions: How many infrastructure requests were sitting unresolved in our &lt;code&gt;#platform-requests&lt;/code&gt; channel? Who was drowning in context-switching across too many active threads? Had we missed early warning signs buried in reaction patterns on code review requests?&lt;/p&gt;

&lt;p&gt;Worse, I realized I was guessing about team health. I'd see a busy Slack day and assume we were productive. I'd see green checkmark reactions and assume requests were getting fulfilled. But I had no data - just gut feel and fragmented chat history.&lt;/p&gt;

&lt;p&gt;Then came the compliance gap. A &lt;code&gt;terraform apply&lt;/code&gt; failed in staging, and someone replied with "skipping, will fix later" - then applied the change manually to unblock themselves. It showed up as a casual thread reply with a 😱 reaction from a teammate, buried under 30 messages. No alert, no metric, no visibility - until I went looking for it during the post-mortem and found the &lt;code&gt;octopus&lt;/code&gt; reaction someone had wisely added to flag the IaC violation.&lt;/p&gt;

&lt;p&gt;That's when I realized: &lt;strong&gt;Slack is where my team actually works, but Slack's analytics treat it like a broadcast channel instead of a workflow engine.&lt;/strong&gt; Every reaction, every thread, every reply is a data point. I just wasn't collecting them.&lt;/p&gt;

&lt;p&gt;So I built something to fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Slack Won't Tell You What You Need to Know
&lt;/h2&gt;

&lt;p&gt;Slack gives you member counts, message volume, and channel growth. That's it. If you're running a platform engineering team, a DevOps org, or an internal support channel, those numbers are vanity metrics.&lt;/p&gt;

&lt;p&gt;What you actually need to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which threads get resolved vs. abandoned?&lt;/li&gt;
&lt;li&gt;How fast does the team respond to infrastructure requests?&lt;/li&gt;
&lt;li&gt;Which code review requests get ignored (and why)?&lt;/li&gt;
&lt;li&gt;Are people actually engaging with your announcements?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Slack doesn't expose this. But Slack's event subscriptions do - if you know how to catch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: How I Capture What Slack Hides
&lt;/h2&gt;

&lt;p&gt;The system has three layers: Slack events → Go webhook handler → Prometheus + Grafana.&lt;/p&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%2Fbard.sh%2F..%2Fdiagrams%2F01-architecture.png" 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%2Fbard.sh%2F..%2Fdiagrams%2F01-architecture.png" alt="Architecture Overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Webhook Handler
&lt;/h3&gt;

&lt;p&gt;Built in Go 1.26 with Gin, the handler listens at &lt;code&gt;POST /api/slack/events&lt;/code&gt;. Every event goes through this middleware stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Middleware&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recovery&lt;/td&gt;
&lt;td&gt;Panic recovery&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RequestID&lt;/td&gt;
&lt;td&gt;Unique ID per request&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GinLogger&lt;/td&gt;
&lt;td&gt;Structured JSON logging&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RequestSizeLimit&lt;/td&gt;
&lt;td&gt;Reject &amp;gt;1MB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RateLimiter&lt;/td&gt;
&lt;td&gt;10 req/s per IP, burst 20&lt;/td&gt;
&lt;td&gt;Handles Slack's event batching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SlackVerification&lt;/td&gt;
&lt;td&gt;HMAC-SHA256 signature check&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;burst 20&lt;/code&gt; setting is critical - Slack's Event API sometimes batches multiple events into a short window, so a single IP can legitimately spike above 10 req/s. The burst allows those spikes without dropping legitimate events.&lt;/p&gt;

&lt;p&gt;The three event types we care about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;message&lt;/code&gt; events&lt;/strong&gt; - catch new threads and replies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/handler/slack.go (simplified)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadTs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadTs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Top-level message → new thread&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordNewThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Reply in thread&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordThreadReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadTs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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;strong&gt;2. &lt;code&gt;reaction_added&lt;/code&gt; / &lt;code&gt;reaction_removed&lt;/code&gt;&lt;/strong&gt; - the signal layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Reactions are stored in Redis + recorded as metrics&lt;/span&gt;
&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordThreadReaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordUserEngagement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;url_verification&lt;/code&gt;&lt;/strong&gt; - Slack's challenge-response during app setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Slack Retry Problem
&lt;/h3&gt;

&lt;p&gt;Slack expects a &lt;code&gt;200 OK&lt;/code&gt; within &lt;strong&gt;3 seconds&lt;/strong&gt;. If your handler is busy writing to Redis or the Prometheus exporter is slow, Slack will retry the event - and you'll get duplicate counts in your metrics.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;acknowledge first, process asynchronously&lt;/strong&gt;. Return &lt;code&gt;200 OK&lt;/code&gt; immediately, then handle the event in a goroutine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/handler/slack.go (simplified)&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;HandleEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;SlackEvent&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShouldBindJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Acknowledge immediately - don't block on Redis/Prometheus&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c"&gt;// Process in background&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadTs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadTs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordNewThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordThreadReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadTs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents Slack's retry mechanism from creating duplicate metric increments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Reactions Are the Secret Signal
&lt;/h3&gt;

&lt;p&gt;I don't just count messages. I track &lt;em&gt;reactions&lt;/em&gt; as semantic signals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Emoji&lt;/th&gt;
&lt;th&gt;Reaction Name&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Metric Label&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;white_check_mark&lt;/td&gt;
&lt;td&gt;Request completed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reaction="white_check_mark"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;😱&lt;/td&gt;
&lt;td&gt;scream&lt;/td&gt;
&lt;td&gt;Multiple active threads per person (overload)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reaction="scream"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚫&lt;/td&gt;
&lt;td&gt;no_entry&lt;/td&gt;
&lt;td&gt;Irrelevant code review request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reaction="no_entry"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔁&lt;/td&gt;
&lt;td&gt;repeat&lt;/td&gt;
&lt;td&gt;Repeating / duplicate request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reaction="repeat"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;td&gt;Irrelevant request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reaction="x"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✔️&lt;/td&gt;
&lt;td&gt;done&lt;/td&gt;
&lt;td&gt;Completed (no PR review needed)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reaction="done"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🐙&lt;/td&gt;
&lt;td&gt;octopus&lt;/td&gt;
&lt;td&gt;Infrastructure-as-Code violation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reaction="octopus"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This turns Slack into a structured workflow tool - reactions become queryable metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting the Team on Board (Social Engineering)
&lt;/h3&gt;

&lt;p&gt;This only works if people actually use the reactions. I didn't mandate it - instead, I led by example: I started reacting to threads with &lt;code&gt;octopus&lt;/code&gt; when I saw IaC violations, and with &lt;code&gt;scream&lt;/code&gt; when I was drowning in threads. Within a week, the team adopted it naturally. The key is making it feel like a helpful shorthand, not a tracking mechanism. If the team doesn't use the emoji, your metrics are useless - so make it useful for them first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metric Definitions and Component Mapping
&lt;/h2&gt;

&lt;p&gt;I define 5 OpenTelemetry counter metrics in &lt;code&gt;internal/metrics/slack.go&lt;/code&gt;. The Prometheus exporter appends &lt;code&gt;_total&lt;/code&gt;, giving the double suffix you see in PromQL queries - you might want to fix it in your OTEL Agent.&lt;/p&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%2Fbard.sh%2F..%2Fdiagrams%2F02-metric-mapping.png" 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%2Fbard.sh%2F..%2Fdiagrams%2F02-metric-mapping.png" alt="Metric-to-Component Mapping"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Metric-to-Component Mapping
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code Name&lt;/th&gt;
&lt;th&gt;Labels&lt;/th&gt;
&lt;th&gt;Triggered By&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webhook_requests_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Every incoming webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;threads_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;channel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New top-level message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;thread_replies_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reply in thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;thread_reactions_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;reaction&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reaction added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_engagement_total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reply OR reaction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Prometheus Cardinality Bomb
&lt;/h3&gt;

&lt;p&gt;If you're tempted to add &lt;code&gt;thread_id&lt;/code&gt; as a label - don't. In my original POC, I made this mistake. In Prometheus, every unique combination of label values creates a new time series. With thousands of Slack threads (each with a unique &lt;code&gt;thread_id&lt;/code&gt;), you're creating thousands of time series. This is a &lt;strong&gt;cardinality bomb&lt;/strong&gt; that will explode your Prometheus memory usage and can crash the instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Keep Prometheus for aggregate channel-level counters only. Thread-level detail belongs in Redis (which I'm already using) or a SQL database, not in Prometheus labels. If you want the "Longest Thread" feature, query Redis directly - don't pollute your metrics with high-cardinality labels. Prometheus is for metrics, not event logging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Performance Indicators
&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%2Fbard.sh%2F..%2Fdiagrams%2F03-dashboard-kpi.png" 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%2Fbard.sh%2F..%2Fdiagrams%2F03-dashboard-kpi.png" alt="Dashboard KPIs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dashboard is organized into four insight layers, each answering a different question about how your Slack channels actually operate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume &amp;amp; Throughput&lt;/strong&gt; - &lt;em&gt;Are we drowning or cruising?&lt;/em&gt;&lt;br&gt;
&lt;code&gt;Total Threads&lt;/code&gt; shows raw request load per day, making it obvious which days are release days, incident spikes, or quiet periods. &lt;code&gt;Active Threads&lt;/code&gt; (threads with at least one reply) reveals engagement depth - a high thread count with low reply rate means people are posting but not discussing, a signal of announcements drowning out conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resolution Signals&lt;/strong&gt; - &lt;em&gt;Are requests actually getting done?&lt;/em&gt;&lt;br&gt;
The green panels tell the completion story. &lt;code&gt;white_check_mark&lt;/code&gt; reactions track formal completions - infrastructure requests fulfilled, reviews done. &lt;code&gt;done&lt;/code&gt; reactions capture quick wins that never needed a PR. Meanwhile, &lt;code&gt;x&lt;/code&gt; reactions surface canceled or irrelevant requests, helping you spot scope creep or misrouted asks. Together, they give you a resolution rate without ever asking anyone to fill out a form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quality &amp;amp; Friction&lt;/strong&gt; - &lt;em&gt;Where is the process breaking?&lt;/em&gt;&lt;br&gt;
The red and yellow panels are your early warning system. A spike in &lt;code&gt;scream&lt;/code&gt; reactions means people are drowning in threads - too many active requests per person. &lt;code&gt;no_entry&lt;/code&gt; on code reviews exposes recurring low-quality submissions from specific contributors. &lt;code&gt;repeat&lt;/code&gt; reactions highlight documentation gaps - the same question asked three times means the answer isn't findable. &lt;code&gt;octopus&lt;/code&gt; flags manual infrastructure changes that bypass your IaC pipeline, a compliance risk you'd otherwise miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-Based Insights&lt;/strong&gt; - &lt;em&gt;When is the team really working?&lt;/em&gt;&lt;br&gt;
The blue panels surface patterns hidden by aggregate daily stats. &lt;code&gt;After-Hours Work&lt;/code&gt; quantifies overtime - if 30% of threads start after 17:00, you have an on-call problem, not a "dedicated team" problem. &lt;code&gt;Longest Thread&lt;/code&gt; with a clickable Slack link lets you jump directly to the most contentious discussion - usually a design debate or a stuck incident. &lt;code&gt;Busiest Day&lt;/code&gt; aggregates threads by date to reveal your actual rhythm: Tuesday releases? Friday incident spikes? The data tells the story.&lt;/p&gt;
&lt;h3&gt;
  
  
  Business-Hour Awareness
&lt;/h3&gt;

&lt;p&gt;One panel tracks requests outside 10:00–17:00 (browser time):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum_over_time(
  (
    sum(
      increase(threads_total{channel=~"$channel_filter"}[5m])
      and on()
        ((hour() &amp;lt; 10) or (hour() &amp;gt;= 17))
    )
  )[$__range:5m]
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells us: is the team working after hours? If so, why?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;hour()&lt;/code&gt; function in Prometheus uses &lt;strong&gt;UTC&lt;/strong&gt;. If your team is in a different timezone (e.g., CET/Poland), adjust the offsets: 10:00 CET = 08:00 UTC, 17:00 CET = 15:00 UTC. Adjust the &lt;code&gt;&amp;lt; 10&lt;/code&gt; / &lt;code&gt;&amp;gt;= 17&lt;/code&gt; values based on where your Prometheus server is running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Longest Thread Detection
&lt;/h3&gt;

&lt;p&gt;Since I removed &lt;code&gt;thread_id&lt;/code&gt; from Prometheus labels (see The Prometheus Cardinality Bomb), the "Longest Thread" feature works differently: I pull thread IDs directly from &lt;strong&gt;Redis&lt;/strong&gt;, where high-cardinality data lives safely. The Grafana dashboard queries Redis for the top threads by reply count, then overlays Prometheus aggregate trends for context.&lt;/p&gt;

&lt;p&gt;To implement this, use a Grafana &lt;strong&gt;Infinity datasource&lt;/strong&gt; or &lt;strong&gt;Redis datasource plugin&lt;/strong&gt; to query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;LRANGE replies:&lt;span class="o"&gt;{&lt;/span&gt;channel&lt;span class="o"&gt;}&lt;/span&gt;:&lt;span class="k"&gt;*&lt;/span&gt; 0 &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then match the longest list to its channel and thread_ts. The clickable Slack link is built from these Redis keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yourworkspace.slack.com/archives/{channel}/p{thread_ts_without_dot}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, if you keep thread_id in Prometheus for a small team (few threads), the PromQL would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;topk(
  1,
  sum by (thread_id, channel) (
    max_over_time(
      thread_replies_total_total{
        channel=~"$channel_filter"
      }[$__range]
    )
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a &lt;strong&gt;Data link&lt;/strong&gt; override pointing to &lt;code&gt;https://yourworkspace.slack.com/archives/${channel}/p${__cell}&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Busiest Day Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum(
  increase(threads_total{channel=~"$channel_filter"}[1d])
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grafana transformations convert the timestamp to &lt;code&gt;YYYY-MM-DD&lt;/code&gt; format and sort by value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies: What We Chose and Why
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;go.mod (direct dependencies)
├── github.com/gin-gonic/gin v1.11.0         # HTTP routing
├── go.opentelemetry.io/otel v1.40.0         # Metrics SDK
├── go.opentelemetry.io/otel/exporters/prometheus v0.62.0  # Prometheus export
├── go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0
├── github.com/prometheus/client_golang v1.23.2  # Prometheus client
├── github.com/redis/go-redis/v9 v9.17.3     # Redis client (cluster support)
├── golang.org/x/time v0.14.0                # Rate limiting
└── github.com/alicebob/miniredis/v2 v2.36.1 # In-memory Redis for tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Trade-offs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenTelemetry over raw Prometheus client&lt;/td&gt;
&lt;td&gt;Future-proof, OTLP export option&lt;/td&gt;
&lt;td&gt;More boilerplate setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis for thread storage&lt;/td&gt;
&lt;td&gt;Fast lookups, TTL support&lt;/td&gt;
&lt;td&gt;Extra dependency, network hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gin over stdlib&lt;/td&gt;
&lt;td&gt;Fast dev, built-in middleware&lt;/td&gt;
&lt;td&gt;Framework lock-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counter metrics only&lt;/td&gt;
&lt;td&gt;Simple, append-only&lt;/td&gt;
&lt;td&gt;No histograms for latency (yet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fly.io deployment&lt;/td&gt;
&lt;td&gt;Simple, cheap, EU region (waw)&lt;/td&gt;
&lt;td&gt;Less control than K8s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What I Skipped
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Histograms&lt;/strong&gt;: I don't track webhook processing latency. The handler is fast enough (&amp;lt;10ms) that it hasn't mattered yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gauge metrics&lt;/strong&gt;: No "current active threads" gauge. I rely on &lt;code&gt;increase()&lt;/code&gt; queries instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thread cancellation&lt;/strong&gt;: No separate metric needed - canceled threads are tracked via &lt;code&gt;thread_reactions_total{reaction="x"}&lt;/code&gt; (the ❌ emoji). The Grafana dashboard queries reactions with &lt;code&gt;reaction="x"&lt;/code&gt; to surface irrelevant/canceled requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Redis Data Structures
&lt;/h2&gt;

&lt;p&gt;The handler stores thread context in Redis for enrichment (not just metrics):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key Pattern&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reactions:{channel}:{ts}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;Field: &lt;code&gt;{reaction}:{user}&lt;/code&gt;, Value: JSON with timestamps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messages:{channel}:{ts}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;JSON: user, text, ts, channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replies:{channel}:{thread_ts}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;td&gt;JSON entries for each reply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;processed_events:{event_id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;TTL 5min - idempotency key for Slack retries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Event Deduplication
&lt;/h3&gt;

&lt;p&gt;Slack retries events if it doesn't get a 200 OK fast enough. To prevent double-counting metrics, the handler uses the &lt;code&gt;event_id&lt;/code&gt; as an idempotency key with &lt;code&gt;SETNX&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Before processing, check if we already handled this event&lt;/span&gt;
&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"processed_events:%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slackEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// SetNX returns true if key was SET (new event), false if key already existed&lt;/span&gt;
&lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetNX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Event already processed - skip to prevent double-counting&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that even if Slack sends the same event twice during a network blip, your metrics are only incremented once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reactions Are Better Than Thread Replies for Signals
&lt;/h3&gt;

&lt;p&gt;A reply requires typing. A reaction is one click. I get 3× more signal from reactions than replies because the friction is lower.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Grafana Transformations Are Powerful But Fragile
&lt;/h3&gt;

&lt;p&gt;The "Longest Thread" panel uses 4 transformations: &lt;code&gt;sortBy&lt;/code&gt;, &lt;code&gt;limit&lt;/code&gt;, &lt;code&gt;convertFieldType&lt;/code&gt;, &lt;code&gt;formatTime&lt;/code&gt;, and &lt;code&gt;renameByRegex&lt;/code&gt;. One breaks, the panel dies. Keep transformations minimal where possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rate Limiting Is Essential
&lt;/h3&gt;

&lt;p&gt;Slack's event replay will flood you if your handler was down. Our rate limiter (10 req/s per IP) prevents thundering herd on restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up: What You Get Out of This
&lt;/h2&gt;

&lt;p&gt;Building custom Slack metrics isn't about dashboards for the sake of dashboards. It's about making the invisible visible. Here's what this setup gives you after a few weeks of data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You stop guessing about team health.&lt;/strong&gt; A quick glance at the KPI row tells you: are we keeping up (green), drowning in context-switching (scream spikes), or fielding the same question repeatedly (repeat reactions)? These aren't vanity metrics - they're leading indicators of burnout, documentation gaps, and process breakdowns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You catch compliance and quality issues early.&lt;/strong&gt; That &lt;code&gt;octopus&lt;/code&gt; reaction on a thread? It's someone manually changing infrastructure instead of using Terraform. The &lt;code&gt;no_entry&lt;/code&gt; on a code review? A recurring contributor quality problem you can address directly. Without this signal, those issues stay hidden in chat history until they become outages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You understand your real working patterns.&lt;/strong&gt; The after-hours panel quantifies overtime without timesheets. The longest thread view surfaces your actual bottlenecks - the design debates and stuck incidents that deserve retrospectives. The busiest day chart reveals whether your release rhythm is working or just creating spikes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You get all of this without asking anyone to change behavior.&lt;/strong&gt; No new forms, no status updates, no "please label your threads." People just use Slack the way they always have - the reactions and replies they'd make anyway become the data. That's the real win: observability that emerges from existing workflows, not another process to maintain.&lt;/p&gt;

&lt;p&gt;The stack is lightweight: a Go handler (~200 lines), Redis for context, Prometheus for storage, Grafana for visualization. But the output is a window into your team's actual collaboration patterns - the stuff Slack's analytics won't show you, and the stuff you need if you want to lead a team effectively.&lt;/p&gt;




&lt;p&gt;One final note: the working POC took me a few hours to build - without AI assistance. Not because I'm unusually fast, but because this isn't actually hard when you understand how the pieces fit together. Slack gives you webhooks. Prometheus takes counters. Grafana draws panels. The real skill isn't writing the code - it's recognizing that the data you need is already flowing through your tools, you just haven't built the bridge between them yet. Sometimes the best observability projects are the ones where you stop waiting for a vendor feature and wire up the integration yourself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/slack-metrics/" rel="noopener noreferrer"&gt;https://bard.sh/posts/slack-metrics/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>slack</category>
      <category>devops</category>
      <category>prometheus</category>
      <category>observability</category>
    </item>
    <item>
      <title>Terraform Modules: Composition Over Abstraction</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Mon, 27 Apr 2026 13:19:36 +0000</pubDate>
      <link>https://dev.to/bard/terraform-modules-composition-over-abstraction-47i0</link>
      <guid>https://dev.to/bard/terraform-modules-composition-over-abstraction-47i0</guid>
      <description>&lt;h1&gt;
  
  
  Terraform Modules: Composition Over Abstraction
&lt;/h1&gt;

&lt;p&gt;Terraform makes it easy to build large, highly abstracted modules that try to solve everything in one place. At first glance, this feels efficient: fewer modules, fewer calls, less wiring.&lt;/p&gt;

&lt;p&gt;In practice, that approach often creates more problems than it solves.&lt;/p&gt;

&lt;p&gt;A better pattern-especially as infrastructure grows-is to design &lt;strong&gt;small, focused, “atomic” modules&lt;/strong&gt; and combine them using &lt;strong&gt;composition&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Mean by Composition
&lt;/h2&gt;

&lt;p&gt;In this context, &lt;strong&gt;composition&lt;/strong&gt; means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Building infrastructure by combining smaller, independent modules instead of hiding everything behind a single, all-in-one module.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one module doing everything internally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple modules wired together explicitly
&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;module&lt;/span&gt; &lt;span class="s2"&gt;"role"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"policy"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"attachment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not just a stylistic choice-it directly impacts maintainability, safety, and clarity.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with “Do-It-All” Modules
&lt;/h2&gt;

&lt;p&gt;A common anti-pattern is a module that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates multiple IAM roles&lt;/li&gt;
&lt;li&gt;generates multiple policies&lt;/li&gt;
&lt;li&gt;attaches them&lt;/li&gt;
&lt;li&gt;conditionally enables features via flags&lt;/li&gt;
&lt;li&gt;supports multiple unrelated use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"irsa"&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/irsa"&lt;/span&gt;

  &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;service_a&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;policies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;service_b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;policies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sqs"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks convenient, but introduces several issues.&lt;/p&gt;




&lt;h3&gt;
  
  
  Hidden Coupling
&lt;/h3&gt;

&lt;p&gt;All resources share one lifecycle.&lt;/p&gt;

&lt;p&gt;A small change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can affect unrelated roles&lt;/li&gt;
&lt;li&gt;may trigger unnecessary diffs&lt;/li&gt;
&lt;li&gt;increases risk during apply&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Poor Reusability
&lt;/h3&gt;

&lt;p&gt;“Generic” modules often become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;too opinionated for some use cases&lt;/li&gt;
&lt;li&gt;too complex for others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consumers either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fight the interface&lt;/li&gt;
&lt;li&gt;or reimplement logic elsewhere&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Hard-to-Review Changes
&lt;/h3&gt;

&lt;p&gt;A simple modification may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;touch multiple resources&lt;/li&gt;
&lt;li&gt;impact different logical paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it difficult to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What will this change actually do?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Large Blast Radius
&lt;/h3&gt;

&lt;p&gt;Terraform operates at the module/state level.&lt;/p&gt;

&lt;p&gt;Large modules lead to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bigger plans&lt;/li&gt;
&lt;li&gt;slower applies&lt;/li&gt;
&lt;li&gt;harder rollbacks&lt;/li&gt;
&lt;li&gt;increased risk&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Atomic Modules: A Better Approach
&lt;/h2&gt;

&lt;p&gt;“Atomic” does not mean artificially small.&lt;/p&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A module should represent a single logical responsibility.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;iam-role&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iam-policy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iam-role-policy-attachment&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each module:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;has a clear purpose&lt;/li&gt;
&lt;li&gt;exposes a minimal interface&lt;/li&gt;
&lt;li&gt;can be reused independently&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Example: IRSA - Monolith vs Composition
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Monolithic Approach
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"irsa"&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/irsa"&lt;/span&gt;

  &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;service_a&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;policies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tightly coupled lifecycle&lt;/li&gt;
&lt;li&gt;unclear ownership&lt;/li&gt;
&lt;li&gt;difficult to modify safely&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Composed Approach
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Create role
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"service_a_role"&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/iam-role"&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"service-a"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;irsa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Create policy
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"service_a_s3_policy"&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/iam-policy"&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"service-a-s3"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Attach policy
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"service_a_attach_s3"&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/iam-role-policy-attachment"&lt;/span&gt;

  &lt;span class="nx"&gt;role_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_a_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_a_s3_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Composition Works Better
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Clear Ownership
&lt;/h3&gt;

&lt;p&gt;Each resource:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is defined explicitly&lt;/li&gt;
&lt;li&gt;belongs to a specific consumer&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Safer Changes
&lt;/h3&gt;

&lt;p&gt;Updating a policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;affects only that policy&lt;/li&gt;
&lt;li&gt;does not impact unrelated roles&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Better Reusability
&lt;/h3&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reuse policies across roles&lt;/li&gt;
&lt;li&gt;attach policies flexibly&lt;/li&gt;
&lt;li&gt;compose behavior without modifying modules&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Easier Debugging
&lt;/h3&gt;

&lt;p&gt;Failures are easier to trace because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modules are small&lt;/li&gt;
&lt;li&gt;responsibilities are clear&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Composition vs Abstraction
&lt;/h2&gt;

&lt;p&gt;These are often confused.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Abstraction&lt;/td&gt;
&lt;td&gt;Simplicity of usage&lt;/td&gt;
&lt;td&gt;Hidden complexity, rigidity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composition&lt;/td&gt;
&lt;td&gt;Flexibility, clarity&lt;/td&gt;
&lt;td&gt;More explicit wiring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monolithic modules favor abstraction.&lt;/p&gt;

&lt;p&gt;Atomic modules favor composition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trade-offs of Composition
&lt;/h2&gt;

&lt;p&gt;This approach is not free.&lt;/p&gt;

&lt;h3&gt;
  
  
  More Module Calls
&lt;/h3&gt;

&lt;p&gt;You will write more blocks.&lt;/p&gt;

&lt;p&gt;This increases verbosity, but also improves clarity.&lt;/p&gt;




&lt;h3&gt;
  
  
  More Explicit Wiring
&lt;/h3&gt;

&lt;p&gt;You pass outputs between modules.&lt;/p&gt;

&lt;p&gt;This is intentional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependencies are visible&lt;/li&gt;
&lt;li&gt;behavior is predictable&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Risk of Over-Fragmentation
&lt;/h3&gt;

&lt;p&gt;Splitting everything blindly leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unnecessary complexity&lt;/li&gt;
&lt;li&gt;modules that are never used independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example of going too far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separating tightly coupled resources that must always exist together&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical Rule of Thumb
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If a resource can be safely changed, reused, or destroyed independently, it should likely be its own module.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If not, keep it together.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Larger Modules Still Make Sense
&lt;/h2&gt;

&lt;p&gt;There are valid cases for bigger modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tightly coupled infrastructure (e.g., VPC with subnets and routing)&lt;/li&gt;
&lt;li&gt;opinionated platform layers&lt;/li&gt;
&lt;li&gt;internal “productized” infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;avoid “god modules”&lt;/li&gt;
&lt;li&gt;keep boundaries clear&lt;/li&gt;
&lt;li&gt;prefer internal composition&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Insight
&lt;/h2&gt;

&lt;p&gt;The real shift is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Move complexity from inside modules to between modules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Monolith → implicit complexity&lt;/li&gt;
&lt;li&gt;Composition → explicit complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In infrastructure systems, &lt;strong&gt;explicit complexity is easier to manage over time&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prefer &lt;strong&gt;composition over monolithic abstraction&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Design modules with &lt;strong&gt;single responsibilities&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Keep dependencies &lt;strong&gt;explicit and visible&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Accept some verbosity in exchange for &lt;strong&gt;safety and clarity&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not smaller modules.&lt;/p&gt;

&lt;p&gt;The goal is &lt;strong&gt;predictable, composable, and low-risk infrastructure&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/terraform-modules-composition-over-abstraction/" rel="noopener noreferrer"&gt;https://bard.sh/posts/terraform-modules-composition-over-abstraction/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>terragrunt</category>
      <category>infrastructureascode</category>
      <category>devops</category>
    </item>
    <item>
      <title>Smart DNS: Auto-Detecting Route53 Records in Terraform</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Mon, 27 Apr 2026 12:15:06 +0000</pubDate>
      <link>https://dev.to/bard/smart-dns-auto-detecting-route53-records-in-terraform-4ilf</link>
      <guid>https://dev.to/bard/smart-dns-auto-detecting-route53-records-in-terraform-4ilf</guid>
      <description>&lt;h1&gt;
  
  
  Smart DNS: Auto-Detecting Route53 Records in Terraform
&lt;/h1&gt;

&lt;p&gt;Every Route53 record in plain Terraform means a zone lookup, an explicit type, and for load balancers - a separate data source just to wire the alias.&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;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_zone"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-nlb"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_record"&lt;/span&gt; &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;zone_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_route53_zone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt;

  &lt;span class="nx"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;zone_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zone_id&lt;/span&gt;
    &lt;span class="nx"&gt;evaluate_target_health&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;With a module that infers everything, the same record becomes:&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;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;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-nlb-1234567890.elb.eu-west-1.amazonaws.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  zone auto-detection
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name = "api.example.com"
        │
        └─ strip first label
                │
                ▼
        "example.com"  ──► aws_route53_zone lookup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module splits the FQDN, drops the first label, queries Route53 for the hosted zone. No &lt;code&gt;zone_id&lt;/code&gt; from the caller.&lt;/p&gt;

&lt;p&gt;For apex records or non-standard cases, &lt;code&gt;zone_override&lt;/code&gt; skips this.&lt;/p&gt;

&lt;h2&gt;
  
  
  type inference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;records value
      │
      ├─ type_override set? ───────────────► use that type (NS/MX/TXT/etc.)
      │
      ├─ is map?
      │     ├─ has "name" key? ────────────► alias A  (lookup LB by name)
      │     └─ has "tags" key? ────────────► alias A  (lookup LB by tags)
      │
      ├─ is string or list?
      │     ├─ all values are IPv4? ───────► A
      │     ├─ ends with .amazonaws.com? ──► alias A  (lookup LB by hostname)
      │     └─ else ───────────────────────► CNAME
      │
      └─ (nothing created if no branch matches)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each branch maps to a separate &lt;code&gt;aws_route53_record&lt;/code&gt; resource using &lt;code&gt;for_each&lt;/code&gt; over a conditional set - only one fires per invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  lb alias lookup modes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;By hostname&lt;/td&gt;
&lt;td&gt;&lt;code&gt;records = "my-nlb-xxx.amazonaws.com"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;parse LB name from hostname prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;By name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;records = { nlb = { name = "my-nlb" } }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;aws_lb&lt;/code&gt; data source by name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;By tags&lt;/td&gt;
&lt;td&gt;&lt;code&gt;records = { alb = { tags = {...} } }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;aws_lb&lt;/code&gt; data source by tag filter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three produce an &lt;code&gt;alias&lt;/code&gt; block with &lt;code&gt;evaluate_target_health = true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  escape hatches
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NS delegation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type_override = "NS"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MX at zone apex&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;type_override = "MX"&lt;/code&gt; + &lt;code&gt;zone_override = "example.com"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TXT verification&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type_override = "TXT"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private hosted zone&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private_zone = true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom TTL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ttl = 300&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  the idea
&lt;/h2&gt;

&lt;p&gt;DNS configuration is mostly mechanical - you know the hostname or IP address(es), you know what it points to. The type, the zone, whether it needs an alias block - those are derivable. This module pushes that derivation into Terraform so callers don't repeat it.&lt;/p&gt;

&lt;p&gt;The tradeoff is explicitness. Plain Terraform is verbose, but every field is visible and validated. The module is terse, but inference can surprise you - especially the &lt;code&gt;records = "..."&lt;/code&gt; string that silently becomes an alias lookup against AWS instead of a literal CNAME value.&lt;/p&gt;

&lt;p&gt;It works best when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you manage many records across consistent naming conventions&lt;/li&gt;
&lt;li&gt;all your LBs follow predictable hostname patterns (&lt;code&gt;*.&amp;lt;region&amp;gt;.elb.amazonaws.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;your zones map cleanly to second-level domains (one zone per domain, no split-horizon edge cases)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It gets awkward when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you need fine-grained TTL control per record&lt;/li&gt;
&lt;li&gt;you have private hosted zones with the same name as public ones (need &lt;code&gt;private_zone = true&lt;/code&gt; consistently)&lt;/li&gt;
&lt;li&gt;your LB names are not stable - tag-based lookup is more resilient there&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  trade-off
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;records&lt;/code&gt; is &lt;code&gt;type = any&lt;/code&gt;. Terraform cannot validate at plan time. A typo in a map key (&lt;code&gt;naam&lt;/code&gt; instead of &lt;code&gt;name&lt;/code&gt;) silently falls through to CNAME instead of alias. Review plan output before apply - the wrong record type shows up as a different resource being created.&lt;/p&gt;

&lt;h2&gt;
  
  
  full example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CNAME&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;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"old-api.example.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# A record&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;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"203.0.113.11"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Alias to NLB by hostname&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;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-nlb-1234567890.elb.eu-west-1.amazonaws.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Alias to ALB by tags&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;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;alb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"api-alb"&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# NS delegation with zone override&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;name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"example.com"&lt;/span&gt;
  &lt;span class="nx"&gt;records&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ns1.example.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ns2.example.net"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;type_override&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NS"&lt;/span&gt;
  &lt;span class="nx"&gt;zone_override&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"example.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/route53-auto-detecting-terraform/" rel="noopener noreferrer"&gt;https://bard.sh/posts/route53-auto-detecting-terraform/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dns</category>
      <category>route53</category>
      <category>terraform</category>
    </item>
    <item>
      <title>What is Terragrunt and why you need it</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:43:55 +0000</pubDate>
      <link>https://dev.to/bard/what-is-terragrunt-and-why-you-need-it-38kf</link>
      <guid>https://dev.to/bard/what-is-terragrunt-and-why-you-need-it-38kf</guid>
      <description>&lt;h1&gt;
  
  
  What is Terragrunt and why you need it
&lt;/h1&gt;

&lt;h2&gt;
  
  
  the multi-env problem
&lt;/h2&gt;

&lt;p&gt;With plain Terraform, managing multiple environments means duplicating config - same modules, different variable files, same backend boilerplate repeated everywhere.&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        # copy
│   ├── variables.tf   # copy
│   └── backend.tf     # copy with different key
├── staging/
│   ├── main.tf        # copy
│   ├── variables.tf   # copy
│   └── backend.tf     # copy with different key
└── prod/
    ├── main.tf        # copy
    ├── variables.tf   # copy
    └── backend.tf     # copy with different key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any change to the module requires updating all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  what Terragrunt does
&lt;/h2&gt;

&lt;p&gt;Terragrunt is a thin wrapper around Terraform that adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DRY backend config - define once, inherit everywhere&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dependency&lt;/code&gt; blocks - wire modules together without a monolith&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run-all&lt;/code&gt; - apply/plan/destroy across multiple modules in one command&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;generate&lt;/code&gt; blocks - auto-generate files (provider configs, backend configs)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mock_outputs&lt;/code&gt; - unblock &lt;code&gt;plan&lt;/code&gt; in CI when dependencies haven't been applied yet&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  terraform vs terragrunt
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Terraform&lt;/th&gt;
&lt;th&gt;Terragrunt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend config&lt;/td&gt;
&lt;td&gt;per-module&lt;/td&gt;
&lt;td&gt;inherited from root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-module apply&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;&lt;code&gt;run-all&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Module wiring&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform_remote_state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dependency&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DRY config&lt;/td&gt;
&lt;td&gt;variables only&lt;/td&gt;
&lt;td&gt;full config inheritance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;asdf plugin-add terragrunt
asdf &lt;span class="nb"&gt;install &lt;/span&gt;terragrunt 0.67.0
asdf &lt;span class="nb"&gt;local &lt;/span&gt;terragrunt 0.67.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or &lt;a href="https://developer.hashicorp.com/terraform/tutorials/automation/terragrunt" rel="noopener noreferrer"&gt;developer.hashicorp.com/terraform/tutorials/automation/terragrunt&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  what the structure looks like
&lt;/h2&gt;

&lt;p&gt;Instead of duplicated &lt;code&gt;.tf&lt;/code&gt; files, you get one &lt;code&gt;root.hcl&lt;/code&gt; and small &lt;code&gt;terragrunt.hcl&lt;/code&gt; files per unit that only define what's different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root.hcl
environments/
├── dev/
│   ├── vpc/
│   │   └── terragrunt.hcl   # 5 lines: include + inputs
│   ├── rds/
│   │   └── terragrunt.hcl
│   └── eks/
│       └── terragrunt.hcl
└── prod/
    ├── vpc/
    │   └── terragrunt.hcl
    ├── rds/
    │   └── terragrunt.hcl
    └── eks/
        └── terragrunt.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical leaf &lt;code&gt;terragrunt.hcl&lt;/code&gt;:&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;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;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;"git::https://github.com/org/tf-modules.git//vpc?ref=v1.2.0"&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;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-vpc"&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&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;The backend, provider, and locking config all come from &lt;code&gt;root.hcl&lt;/code&gt; - no repetition.&lt;/p&gt;

&lt;p&gt;See &lt;a href="//terragrunt-folder-structure.md"&gt;folder structure and include&lt;/a&gt; for the full &lt;code&gt;root.hcl&lt;/code&gt; setup.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/what-is-terragrunt/" rel="noopener noreferrer"&gt;https://bard.sh/posts/what-is-terragrunt/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terragrunt</category>
      <category>terraform</category>
      <category>hcl</category>
    </item>
    <item>
      <title>Terragrunt run-all</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:43:14 +0000</pubDate>
      <link>https://dev.to/bard/terragrunt-run-all-j8f</link>
      <guid>https://dev.to/bard/terragrunt-run-all-j8f</guid>
      <description>&lt;h1&gt;
  
  
  Terragrunt run-all
&lt;/h1&gt;

&lt;p&gt;Runs a Terraform command across all units in the current directory tree, respecting &lt;code&gt;dependency&lt;/code&gt; 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;environments/prod
terragrunt run-all plan
terragrunt run-all apply
terragrunt run-all destroy   &lt;span class="c"&gt;# reverse order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  dependency-aware ordering
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;environments/prod/
├── vpc/
├── rds/          # depends on vpc
└── eks/          # depends on vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run-all apply&lt;/code&gt; applies &lt;code&gt;vpc&lt;/code&gt; first, then &lt;code&gt;rds&lt;/code&gt; and &lt;code&gt;eks&lt;/code&gt; in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  real CI pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Plan all&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;terragrunt run-all plan --terragrunt-non-interactive&lt;/span&gt;
  &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/prod&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply all&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;terragrunt run-all apply --terragrunt-non-interactive -auto-approve&lt;/span&gt;
  &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/prod&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detect drift in CI without applying:&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;# exit code 2 = changes detected, 0 = no changes, 1 = error&lt;/span&gt;
terragrunt run-all plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--terragrunt-non-interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-detailed-exitcode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  filtering
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# skip a unit&lt;/span&gt;
terragrunt run-all apply &lt;span class="nt"&gt;--terragrunt-exclude-dir&lt;/span&gt; environments/prod/rds

&lt;span class="c"&gt;# target a single unit and its dependencies&lt;/span&gt;
terragrunt run-all apply &lt;span class="nt"&gt;--terragrunt-include-dir&lt;/span&gt; environments/prod/eks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  parallelism
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# default is unlimited parallel - cap it if you hit API rate limits&lt;/span&gt;
terragrunt run-all apply &lt;span class="nt"&gt;--terragrunt-parallelism&lt;/span&gt; 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  output
&lt;/h2&gt;

&lt;p&gt;Each unit prefixes its own output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[environments/prod/vpc] Apply complete! Resources: 12 added.
[environments/prod/rds] Apply complete! Resources: 4 added.
[environments/prod/eks] Apply complete! Resources: 31 added.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/terragrunt-run-all/" rel="noopener noreferrer"&gt;https://bard.sh/posts/terragrunt-run-all/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terragrunt</category>
      <category>terraform</category>
      <category>hcl</category>
    </item>
    <item>
      <title>Terragrunt remote_state and generate blocks</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:43:09 +0000</pubDate>
      <link>https://dev.to/bard/terragrunt-remotestate-and-generate-blocks-481g</link>
      <guid>https://dev.to/bard/terragrunt-remotestate-and-generate-blocks-481g</guid>
      <description>&lt;h1&gt;
  
  
  Terragrunt remote_state and generate blocks
&lt;/h1&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;remote_state&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Defines the backend once in &lt;code&gt;root.hcl&lt;/code&gt; - Terragrunt auto-generates the backend config for every unit that includes 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;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;"my-tf-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;"eu-west-1"&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;"terraform-locks"&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;p&gt;&lt;code&gt;path_relative_to_include()&lt;/code&gt; resolves to the path of the calling &lt;code&gt;terragrunt.hcl&lt;/code&gt; relative to &lt;code&gt;root.hcl&lt;/code&gt; - e.g. &lt;code&gt;environments/prod/vpc&lt;/code&gt; - giving each unit a unique state key automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;generate&lt;/code&gt; block
&lt;/h2&gt;

&lt;p&gt;Writes any file before running Terraform - typically used for &lt;code&gt;provider.tf&lt;/code&gt; and &lt;code&gt;backend.tf&lt;/code&gt; so they don't need to be duplicated in every 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="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;
provider "aws" {
  region = "eu-west-1"

  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Environment = "prod"
    }
  }
}
&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;h2&gt;
  
  
  &lt;code&gt;if_exists&lt;/code&gt; options
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;overwrite_terragrunt&lt;/code&gt; - overwrite only if Terragrunt generated the file (safe default)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;overwrite&lt;/code&gt; - always overwrite&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;skip&lt;/code&gt; - skip if file exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt; - error if file exists&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  generating multiple files
&lt;/h2&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;"versions"&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;"versions.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.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&amp;gt; 5.0"
    }
  }
}
&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;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/terragrunt-remote-state/" rel="noopener noreferrer"&gt;https://bard.sh/posts/terragrunt-remote-state/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terragrunt</category>
      <category>terraform</category>
      <category>hcl</category>
    </item>
    <item>
      <title>Terragrunt folder structure and include</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:42:28 +0000</pubDate>
      <link>https://dev.to/bard/terragrunt-folder-structure-and-include-58bg</link>
      <guid>https://dev.to/bard/terragrunt-folder-structure-and-include-58bg</guid>
      <description>&lt;h1&gt;
  
  
  Terragrunt folder structure and include
&lt;/h1&gt;

&lt;p&gt;Terragrunt projects split config into two layers: a root &lt;code&gt;hcl&lt;/code&gt; file with shared settings, and per-unit &lt;code&gt;terragrunt.hcl&lt;/code&gt; files that inherit from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root.hcl
environments/
├── dev/
│   ├── vpc/
│   │   └── terragrunt.hcl
│   ├── rds/
│   │   └── terragrunt.hcl
│   └── eks/
│       └── terragrunt.hcl
└── prod/
    ├── vpc/
    │   └── terragrunt.hcl
    ├── rds/
    │   └── terragrunt.hcl
    └── eks/
        └── terragrunt.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each leaf &lt;code&gt;terragrunt.hcl&lt;/code&gt; is one Terraform unit - its own state file, its own apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;include&lt;/code&gt; - inherit from root
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# environments/prod/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="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;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;"git::https://github.com/org/tf-modules.git//vpc?ref=v1.2.0"&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;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-vpc"&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&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;h2&gt;
  
  
  root.hcl
&lt;/h2&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;env&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;get_terragrunt_dir&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;"eu-west-1"&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;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;"my-tf-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="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&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;"terraform-locks"&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;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;
provider "aws" {
  region = "${local.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;h2&gt;
  
  
  &lt;code&gt;find_in_parent_folders&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Walks up the directory tree until it finds the file - no hardcoded paths needed.&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;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 root.hcl by default&lt;/span&gt;
&lt;span class="p"&gt;}&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="c1"&gt;# explicit name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/terragrunt-folder-structure/" rel="noopener noreferrer"&gt;https://bard.sh/posts/terragrunt-folder-structure/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terragrunt</category>
      <category>terraform</category>
      <category>hcl</category>
    </item>
    <item>
      <title>Terragrunt dependency and mock_outputs</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:42:23 +0000</pubDate>
      <link>https://dev.to/bard/terragrunt-dependency-and-mockoutputs-4hih</link>
      <guid>https://dev.to/bard/terragrunt-dependency-and-mockoutputs-4hih</guid>
      <description>&lt;h1&gt;
  
  
  Terragrunt dependency and mock_outputs
&lt;/h1&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;dependency&lt;/code&gt; block
&lt;/h2&gt;

&lt;p&gt;Reads outputs from another Terragrunt unit - cleaner than &lt;code&gt;terraform_remote_state&lt;/code&gt; and aware of the apply order.&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;# environments/prod/eks/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;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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;dependency&lt;/span&gt; &lt;span class="s2"&gt;"rds"&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;"../rds"&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;vpc_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;vpc_id&lt;/span&gt;
  &lt;span class="nx"&gt;private_subnets&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;private_subnet_ids&lt;/span&gt;
  &lt;span class="nx"&gt;db_endpoint&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;rds&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;endpoint&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;mock_outputs&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Without mock outputs, &lt;code&gt;terragrunt plan&lt;/code&gt; fails if a dependency hasn't been applied yet - it can't fetch real outputs. Mock outputs provide placeholder values for &lt;code&gt;plan&lt;/code&gt; only.&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;vpc_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc-00000000"&lt;/span&gt;
    &lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"subnet-11111111"&lt;/span&gt;&lt;span class="p"&gt;]&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;"plan"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;mock_outputs_allowed_terraform_commands&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Controls which commands use mocks - &lt;code&gt;apply&lt;/code&gt; always uses real outputs.&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;mock_outputs_allowed_terraform_commands&lt;/span&gt; &lt;span class="err"&gt;=&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="s2"&gt;"validate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"destroy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  dependency graph
&lt;/h2&gt;

&lt;p&gt;Terragrunt resolves the graph automatically with &lt;code&gt;run-all&lt;/code&gt; - units apply in the right order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vpc  ──►  eks
     ──►  rds  ──►  app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# applies vpc first, then rds and eks in parallel, then app&lt;/span&gt;
terragrunt run-all apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/terragrunt-dependency/" rel="noopener noreferrer"&gt;https://bard.sh/posts/terragrunt-dependency/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terragrunt</category>
      <category>terraform</category>
      <category>hcl</category>
    </item>
    <item>
      <title>Terraform S3 backend with state locking</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:41:07 +0000</pubDate>
      <link>https://dev.to/bard/terraform-s3-backend-with-state-locking-24gg</link>
      <guid>https://dev.to/bard/terraform-s3-backend-with-state-locking-24gg</guid>
      <description>&lt;h1&gt;
  
  
  Terraform S3 backend with state locking
&lt;/h1&gt;

&lt;p&gt;S3 stores the state file, DynamoDB handles locking - prevents two &lt;code&gt;apply&lt;/code&gt; runs from corrupting state simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  setup
&lt;/h2&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;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-tf-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;"prod/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-east-1"&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;"terraform-locks"&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;h2&gt;
  
  
  create the S3 bucket and DynamoDB table
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&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-tf-state"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&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="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_server_side_encryption_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&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="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;apply_server_side_encryption_by_default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sse_algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AES256"&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_dynamodb_table"&lt;/span&gt; &lt;span class="s2"&gt;"tf_locks"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-locks"&lt;/span&gt;
  &lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LockID"&lt;/span&gt;

  &lt;span class="nx"&gt;attribute&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LockID"&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&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;h2&gt;
  
  
  state locking in action
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;terraform apply
Acquiring state lock. This may take a few moments...

&lt;span class="c"&gt;# if another process holds the lock:&lt;/span&gt;
Error: Error acquiring the state lock
Lock Info:
  ID:        abc-123
  Path:      prod/terraform.tfstate
  Operation: OperationTypeApply
  Who:       alice@machine
  Created:   2024-10-14 12:00:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  force-unlock (use carefully)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform force-unlock &amp;lt;lock-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  per-environment keys
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dev&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;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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# prod&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;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod/terraform.tfstate"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/terraform-s3-backend/" rel="noopener noreferrer"&gt;https://bard.sh/posts/terraform-s3-backend/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>hcl</category>
      <category>aws</category>
    </item>
    <item>
      <title>.terraform.lock.hcl - commit it</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 24 Apr 2026 09:41:01 +0000</pubDate>
      <link>https://dev.to/bard/terraformlockhcl-commit-it-4a0o</link>
      <guid>https://dev.to/bard/terraformlockhcl-commit-it-4a0o</guid>
      <description>&lt;h1&gt;
  
  
  &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; - commit it
&lt;/h1&gt;

&lt;p&gt;Terraform generates this file on &lt;code&gt;terraform init&lt;/code&gt; - it pins the exact provider versions and their checksums so every team member and CI run uses the same binaries.&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;provider&lt;/span&gt; &lt;span class="s2"&gt;"registry.terraform.io/hashicorp/aws"&lt;/span&gt; &lt;span class="p"&gt;{&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;"5.50.0"&lt;/span&gt;
  &lt;span class="nx"&gt;constraints&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="nx"&gt;hashes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"h1:abc123..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"zh:def456..."&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  what it contains
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;version&lt;/code&gt; - exact version that was selected&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;constraints&lt;/code&gt; - the constraint from &lt;code&gt;required_providers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hashes&lt;/code&gt; - checksums for each platform (linux, darwin, windows)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  why commit it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;reproducible builds - everyone gets the same provider binary&lt;/li&gt;
&lt;li&gt;audit trail - version changes are visible in git diff&lt;/li&gt;
&lt;li&gt;faster CI - Terraform can skip checksum verification with &lt;code&gt;-lockfile=readonly&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  updating it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# upgrade a specific provider&lt;/span&gt;
terraform init &lt;span class="nt"&gt;-upgrade&lt;/span&gt;

&lt;span class="c"&gt;# upgrade all providers&lt;/span&gt;
terraform init &lt;span class="nt"&gt;-upgrade&lt;/span&gt; &lt;span class="nt"&gt;-reconfigure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI flag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fails if lock file is out of date - use in CI&lt;/span&gt;
terraform init &lt;span class="nt"&gt;-lockfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;readonly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/terraform-lock-hcl/" rel="noopener noreferrer"&gt;https://bard.sh/posts/terraform-lock-hcl/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>hcl</category>
    </item>
  </channel>
</rss>
