<?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.us-east-2.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>Slack Webhooks in Production: Durable Queues, Distributed Locking, and the Testing Epiphany</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Tue, 22 Sep 2026 18:35:56 +0000</pubDate>
      <link>https://dev.to/bard/slack-webhooks-in-production-durable-queues-distributed-locking-and-the-testing-epiphany-ge2</link>
      <guid>https://dev.to/bard/slack-webhooks-in-production-durable-queues-distributed-locking-and-the-testing-epiphany-ge2</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The POC from Part 1 used a &lt;code&gt;go func()&lt;/code&gt; and hoped for the best. The production version has a Redis-backed event queue with sharded workers, distributed locking for multi-replica safety, an idempotency layer (because Slack &lt;em&gt;will&lt;/em&gt; retry), and integration tests against real Redis containers. This is what it took to make it reliable enough to run at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;go func()&lt;/code&gt; Betrayal
&lt;/h2&gt;

&lt;p&gt;The original handler acknowledged Slack's webhook with &lt;code&gt;200 OK&lt;/code&gt;, then spawned 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="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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Elegant. Wrong.&lt;/p&gt;

&lt;p&gt;Three things broke in production within the first week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Process restart during processing&lt;/strong&gt; - the goroutine dies, the event is lost, Slack never retries (it already got &lt;code&gt;200 OK&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replica contention&lt;/strong&gt; - with two pods, both process the same event, doubling every counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-order processing&lt;/strong&gt; - a &lt;code&gt;reaction_removed&lt;/code&gt; arrives before its &lt;code&gt;reaction_added&lt;/code&gt; because the goroutine scheduler interleaved them differently than the Event API delivered them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix was a durable event queue, but the implementation taught me more about Redis than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: What Changed
&lt;/h2&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%2F04-architecture-comparison.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%2F04-architecture-comparison.png" alt="Architecture Comparison" width="561" height="1057"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every event goes through a Redis list as a durable buffer. The HTTP handler acknowledges Slack immediately, then enqueues. Background workers dequeue and process. If the process dies mid-processing, the event stays on the queue. If another replica picks it up, the idempotency key prevents double-counting.&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%2F05-event-flow.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%2F05-event-flow.png" alt="Event Flow" width="684" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sharding Problem
&lt;/h2&gt;

&lt;p&gt;A single Redis list with multiple workers is a race: two workers both &lt;code&gt;BRPop&lt;/code&gt; the same list and process sibling events concurrently. A reply and its parent message, or a &lt;code&gt;reaction_added&lt;/code&gt; / &lt;code&gt;reaction_removed&lt;/code&gt; pair, lose ordering guarantees.&lt;/p&gt;

&lt;p&gt;The solution was &lt;strong&gt;routing keys + sharded queues&lt;/strong&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;// routingKey identifies the "thread of conversation" an event belongs to&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;routingKey&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;SlackEventData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&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;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="o"&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;channel&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="c"&gt;// reply → thread&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;channel&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;TS&lt;/span&gt;             &lt;span class="c"&gt;// top-level → itself&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"reaction_added"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"reaction_removed"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;channel&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;Item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TS&lt;/span&gt;        &lt;span class="c"&gt;// reaction → target message&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A FNV-32a hash of the routing key maps to one of N shards (&lt;code&gt;swh:queue:events:0&lt;/code&gt; through &lt;code&gt;swh:queue:events:N-1&lt;/code&gt;). Every event for the same thread lands on the same shard, consumed by the same worker, in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distributed Locking: The Shard Lock
&lt;/h2&gt;

&lt;p&gt;With multiple replicas (EKS, two+ pods), two workers from different pods can pop from the same shard simultaneously - defeating the ordering guarantee. Each shard can only have &lt;strong&gt;one consumer cluster-wide&lt;/strong&gt; at a time.&lt;/p&gt;

&lt;p&gt;Enter the shard lock: a Redis key (&lt;code&gt;swh:lock:shard:0&lt;/code&gt;) with a TTL-based lease. The worker acquires it via &lt;code&gt;SETNX&lt;/code&gt;, then periodically renews it via a Lua script that atomically checks ownership before extending TTL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- renewShardLockScript: only renew if still the owner&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PEXPIRE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conditional renewal is critical - without it, a worker that lost its lock (e.g. network partition) would blindly extend the lock held by another replica, breaking the mutual-exclusion guarantee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock Lifecycle
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Acquire&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SETNX lock:shard:0 instanceID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renew (every 5s)&lt;/td&gt;
&lt;td&gt;Lua: if GET == instanceID, PEXPIRE&lt;/td&gt;
&lt;td&gt;15s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release (shutdown)&lt;/td&gt;
&lt;td&gt;Lua: if GET == instanceID, DEL&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orphan (crash)&lt;/td&gt;
&lt;td&gt;TTL expires naturally&lt;/td&gt;
&lt;td&gt;15s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 5s renew interval with a 15s TTL gives three missed renewals before the lease expires - enough to tolerate a GC pause without losing ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency: Slack Retries Are the Least of Your Worries
&lt;/h2&gt;

&lt;p&gt;The original article covered Slack's 3-second retry. But there's a subtler case: what happens when the &lt;strong&gt;worker&lt;/strong&gt; crashes after dequeueing but before storing to Redis?&lt;/p&gt;

&lt;p&gt;The event is gone from the queue (BRPop removed it) but never processed. Slack doesn't retry because the HTTP handler already returned &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is two-layered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Event ID deduplication.&lt;/strong&gt; Before enqueueing, the handler tries &lt;code&gt;SETNX swh:processed:{event_id}&lt;/code&gt; with a 1-hour TTL. If the key already exists, the event was already accepted (by this pod or another) - return &lt;code&gt;200 OK&lt;/code&gt; without enqueueing again.&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="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;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duplicate&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&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;eventID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;  &lt;span class="c"&gt;// no event_id = can't dedupe&lt;/span&gt;
    &lt;span class="p"&gt;}&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;constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessedEventKeyPrefix&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;eventID&lt;/span&gt;
    &lt;span class="n"&gt;ok&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="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redisClient&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyTTL&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="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="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;  &lt;span class="c"&gt;// false = first time, true = already seen&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;Layer 2: Event backlog.&lt;/strong&gt; The queue itself (Redis list) provides durability against pod restarts during processing. An event stays in the queue until a worker successfully processes it. The only gap is the DEAD-letter scenario - event dequeued, worker crashes mid-dispatch. That's a known limitation we accept (rare enough that manual replay from Slack's API is acceptable).&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;/metrics&lt;/code&gt; endpoint exposes Prometheus counters with labels like &lt;code&gt;channel&lt;/code&gt;, &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;thread_id&lt;/code&gt;, and &lt;code&gt;reaction&lt;/code&gt;. Those labels come straight from Slack payloads - and they leak sensitive information.&lt;/p&gt;

&lt;p&gt;In the POC, metrics were wide open on the same port as the webhook handler. In production, &lt;code&gt;/metrics&lt;/code&gt; is gated by an IP allowlist:&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="n"&gt;metricsGroup&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;metricsGroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IPAllowlist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MetricsAllowedCIDRs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MountMetrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metricsGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allowlist defaults to RFC1918 private ranges + loopback - safe for an EKS pod whose Prometheus collector scrapes via the VPC. But there's a trap: &lt;strong&gt;this only works if &lt;code&gt;SetTrustedProxies&lt;/code&gt; is configured correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without trusted proxy configuration, Gin blindly trusts &lt;code&gt;X-Forwarded-For&lt;/code&gt; headers. Any client on the internet can forge their source IP to match the allowlist and access &lt;code&gt;/metrics&lt;/code&gt;. The fix is explicit:&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="n"&gt;trustedProxies&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrustedProxyCIDRs&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trustedProxies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;trustedProxies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultInternalCIDRs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTrustedProxies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trustedProxies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Gin: only respect &lt;code&gt;X-Forwarded-For&lt;/code&gt; from known proxies (ALBs, service mesh sidecars). Everything else gets &lt;code&gt;c.ClientIP()&lt;/code&gt; from the actual TCP connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Testing Epiphany
&lt;/h2&gt;

&lt;p&gt;The POC had zero tests. The production code has &lt;strong&gt;~5,000 lines of tests&lt;/strong&gt;. The biggest lesson was &lt;strong&gt;which kind of test catches which kind of bug&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Learned About Test Levels
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit (routing, validation)&lt;/td&gt;
&lt;td&gt;stdlib &lt;code&gt;testing&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Logic errors, edge cases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration (HTTP → Redis)&lt;/td&gt;
&lt;td&gt;testcontainers-go + real Redis 7-alpine&lt;/td&gt;
&lt;td&gt;Wire bugs, Redis protocol differences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker (queue + locking)&lt;/td&gt;
&lt;td&gt;miniredis + real Redis&lt;/td&gt;
&lt;td&gt;Lock race conditions, Lua script errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware (rate limit, IP allowlist)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;httptest.NewRecorder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Security bypasses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most valuable tests were the &lt;strong&gt;integration tests&lt;/strong&gt; that spin up a real Redis 7-alpine container per test via &lt;code&gt;testcontainers-go&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;suite&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;
    &lt;span class="n"&gt;srv&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;httptest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt;     &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testredis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;
    &lt;span class="n"&gt;appURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newSuite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ti&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;testredis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// real container via testcontainers&lt;/span&gt;
    &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&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;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;srv&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httptest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newRouter&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;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment in the codebase explains the decision:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;miniredis&lt;/code&gt; is a faithful reimplementation for simple cases, but it doesn't implement &lt;code&gt;BRPop&lt;/code&gt; with the same blocking semantics as real Redis. One worker test was consistently passing against miniredis and failing in staging. After eight hours of debugging, the root cause was a subtle difference in how miniredis handles concurrent blocking pops from the same list key. Real containers or nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Kill Your Container Mid-Test
&lt;/h3&gt;

&lt;p&gt;The test framework supports a &lt;code&gt;Kill()&lt;/code&gt; method that terminates the Redis container mid-test, allowing tests for behavior during Redis outages:&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Terminate&lt;/span&gt;&lt;span class="p"&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="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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"testredis: kill container: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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;Tests that exercise the worker's reconnection loop with a live-and-then-dead Redis caught the "infinite retry spam" bug - when Redis goes away, the worker's &lt;code&gt;BRPop&lt;/code&gt; error loop logs once per second forever. The fix was a backoff: &lt;code&gt;sleepOrDone(ctx, time.Second)&lt;/code&gt; on error before retrying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful Shutdown: The Final Mile
&lt;/h2&gt;

&lt;p&gt;The original handler had no shutdown logic. &lt;code&gt;SIGTERM&lt;/code&gt; would kill the process mid-webhook, mid-goroutine, mid-Redis-write. The production version uses Go's &lt;code&gt;signal.NotifyContext&lt;/code&gt; with proper context propagation:&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="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotifyContext&lt;/span&gt;&lt;span class="p"&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="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c"&gt;// workers stop when ctx is done&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;StartWorkers&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;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkerCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// server shutdown with 15s timeout&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;-&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;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;shutdownCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&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;WithTimeout&lt;/span&gt;&lt;span class="p"&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="m"&gt;15&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;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdownCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workers check &lt;code&gt;ctx.Done()&lt;/code&gt; at the top of every loop iteration. On shutdown, they finish their current event (up to &lt;code&gt;redisOpTimeout&lt;/code&gt;), then exit. No half-processed events, no abandoned locks (the lock TTL handles orphaned locks if a node hard-crashes).&lt;/p&gt;

&lt;h2&gt;
  
  
  Config Structure: From Hardcoded to 12-Factor
&lt;/h2&gt;

&lt;p&gt;The POC had constants scattered across files. The maintenance commit centralizes everything:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Port&lt;/span&gt;                &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;RedisURL&lt;/span&gt;            &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;RedisTLS&lt;/span&gt;            &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;RedisCluster&lt;/span&gt;        &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;RedisClusterAddrs&lt;/span&gt;   &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;RedisOperationTimeout&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;Duration&lt;/span&gt;
    &lt;span class="n"&gt;ReadTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WriteTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IdleTimeout&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;Duration&lt;/span&gt;
    &lt;span class="n"&gt;WorkerCount&lt;/span&gt;         &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;MetricsAllowedCIDRs&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;TrustedProxyCIDRs&lt;/span&gt;   &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;SlackSigningSecret&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;GinMode&lt;/span&gt;             &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every value comes from an environment variable with sensible defaults. Production mode (Gin release) &lt;strong&gt;requires&lt;/strong&gt; &lt;code&gt;SLACK_SIGNING_SECRET&lt;/code&gt; - the app refuses to start without it:&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="k"&gt;func&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;Config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;IsProduction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&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;GinMode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"release"&lt;/span&gt;
&lt;span class="p"&gt;}&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;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsProduction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;SlackSigningSecret&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"slack signing secret is required in production mode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&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 caught a near-miss in staging: someone forgot to set the signing secret in the Helm values, and the app refused to start rather than silently accepting unauthenticated webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Still Missing
&lt;/h2&gt;

&lt;p&gt;The queue gives durability against process restarts, but there's still a &lt;strong&gt;dead-letter gap&lt;/strong&gt;: if a worker dequeues an event and crashes before fully processing it, that event is lost. The fix would be a Redis-backed scheduled retry queue with a visibility timeout (like SQS), but that's a future iteration.&lt;/p&gt;

&lt;p&gt;Redis Cluster routing is also untested in CI - each integration test gets a single-node container. The shard-key hashing and lock-key prefixing might behave differently under &lt;code&gt;CLUSTER&lt;/code&gt; with actual shard slots. We validated this manually in staging, but it deserves automated coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The journey from &lt;code&gt;go func()&lt;/code&gt; to sharded workers with distributed locks was driven by concrete production failures, not premature optimization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event loss&lt;/strong&gt; → durable Redis queues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate metrics&lt;/strong&gt; → event ID deduplication + idempotency keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-order reactions&lt;/strong&gt; → routing keys + sharded queues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replica contention&lt;/strong&gt; → distributed shard locks with Lua-based lease management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security leaks&lt;/strong&gt; → IP allowlist + trusted proxy configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent startup failures&lt;/strong&gt; → config validation with production-mode guards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis outage chaos&lt;/strong&gt; → graceful error handling with backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undefined shutdown behavior&lt;/strong&gt; → signal handling + context propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each layer adds complexity but eliminates a real failure mode. The test suite grew 10:1 vs. production code, but every test justifies its line count by catching a bug that would otherwise need an incident post-mortem to discover.&lt;/p&gt;

&lt;p&gt;The full codebase is at &lt;code&gt;internal/handler/&lt;/code&gt; in the repository - about 800 lines of production code and 3,000 lines of tests. The most important file is &lt;code&gt;worker.go&lt;/code&gt; (290 lines): it contains the queue, the locks, the routing, and the graceful-shutdown logic. Everything else is plumbing around it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://bard.sh/posts/slack-metrics-pt2/" rel="noopener noreferrer"&gt;https://bard.sh/posts/slack-metrics-pt2/&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>Kill the Castle: How AWS IAM Replaces Network Perimeters</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Tue, 22 Sep 2026 18:35:50 +0000</pubDate>
      <link>https://dev.to/bard/kill-the-castle-how-aws-iam-replaces-network-perimeters-g0</link>
      <guid>https://dev.to/bard/kill-the-castle-how-aws-iam-replaces-network-perimeters-g0</guid>
      <description>&lt;h2&gt;
  
  
  The Premise: Network Location Is Not an Identity
&lt;/h2&gt;

&lt;p&gt;In traditional "castle-and-moat" architectures, being inside a VPC or private subnet granted baseline trust. Requests from &lt;code&gt;10.0.0.0/8&lt;/code&gt; were implicitly "safe." A compromised EC2 instance in the same subnet as an RDS database could query it freely because network adjacency &lt;em&gt;was&lt;/em&gt; the authorization model.&lt;/p&gt;

&lt;p&gt;Zero Trust inverts this completely:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Network location provides zero authorization. Every request-external, internal, same-node, same-pod-must be explicitly authenticated, authorized, and encrypted.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not a product you install. It's an architectural property that emerges from the disciplined combination of identity-centric policies, cryptographic attestation, microsegmented networking, layered data gates, and continuous verification. On AWS, implementing it requires working across five interlocking pillars.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 1: Identity as the Primary Perimeter
&lt;/h2&gt;

&lt;p&gt;AWS IAM is the root policy engine. In a Zero Trust model, the enforcement boundary shifts from IP CIDRs to identity tokens and cryptographic attestation. The corollary: &lt;strong&gt;if it has long-lived credentials, it's a trust anchor you haven't eliminated yet.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Eliminating Long-Lived Credentials
&lt;/h3&gt;

&lt;p&gt;Every static credential-IAM user access key, hardcoded database password, SSH private key checked into a repo-is a lateral movement vector. The Zero Trust mandate is absolute: &lt;strong&gt;no long-lived credentials anywhere in the system.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload Type&lt;/th&gt;
&lt;th&gt;Anti-Pattern&lt;/th&gt;
&lt;th&gt;Zero Trust Pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2 instances&lt;/td&gt;
&lt;td&gt;Access keys in &lt;code&gt;~/.aws/credentials&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Instance profiles with IAM roles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECS tasks&lt;/td&gt;
&lt;td&gt;Shared task execution role&lt;/td&gt;
&lt;td&gt;Per-task IAM roles via &lt;code&gt;taskRoleArn&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EKS pods&lt;/td&gt;
&lt;td&gt;Node-level instance profile&lt;/td&gt;
&lt;td&gt;EKS Pod Identity or IRSA (projected service account tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD pipelines&lt;/td&gt;
&lt;td&gt;Static "deployer" IAM user keys&lt;/td&gt;
&lt;td&gt;OIDC federation directly to IAM roles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human operators&lt;/td&gt;
&lt;td&gt;IAM users with console passwords&lt;/td&gt;
&lt;td&gt;IAM Identity Center (SSO) + external IdP + FIDO2/WebAuthn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-account access&lt;/td&gt;
&lt;td&gt;Shared credentials in Secrets Manager&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sts:AssumeRole&lt;/code&gt; with external ID and session policies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  OIDC Federation for CI/CD
&lt;/h4&gt;

&lt;p&gt;This eliminates the single most common credential leak vector in modern AWS deployments-the CI/CD deployer key:&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;# Terraform: GitHub Actions OIDC provider&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_openid_connect_provider"&lt;/span&gt; &lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt;
  &lt;span class="nx"&gt;client_id_list&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;thumbprint_list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"6938fd4d98bab03faadb97b34396831e3780aea1"&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_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"github_deploy"&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;"github-actions-deploy"&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;jsonencode&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;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Federated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github&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;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;StringLike&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;# Pin to specific repo AND branch - not org-wide&lt;/span&gt;
          &lt;span class="s2"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"repo:your-org/your-repo:ref:refs/heads/main"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sub&lt;/code&gt; claim pinning is critical. Without it, &lt;em&gt;any&lt;/em&gt; repository in the GitHub organization can assume the role-a common misconfiguration that defeats the purpose entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context-Aware Authorization (ABAC)
&lt;/h3&gt;

&lt;p&gt;Static IAM group membership (&lt;code&gt;Developers&lt;/code&gt;, &lt;code&gt;Admins&lt;/code&gt;) is a coarse authorization model. Attribute-Based Access Control (ABAC) evaluates request context dynamically.&lt;/p&gt;

&lt;p&gt;Consider a fintech company where data engineers, ML engineers, and analysts all need S3 access-but to different buckets with different sensitivity levels. Instead of maintaining separate policies per team, one ABAC policy handles it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABACTeamAndClassificationMatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::acme-datalake-*/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalTag/team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3:ResourceTag/team}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalTag/data-classification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3:ResourceTag/data-classification}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalTag/cost-center"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3:ResourceTag/cost-center}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringEqualsIfExists"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalTag/compliance-scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${s3:ResourceTag/compliance-scope}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"aws:MultiFactorAuthPresent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tag your IAM roles and S3 objects like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principal (IAM Role)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;team&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;data-classification&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;cost-center&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;compliance-scope&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;role/ml-training-pipeline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ml-platform&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;confidential&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cc-4200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;soc2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;role/analytics-dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;analytics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cc-3100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;role/fraud-detection-svc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ml-platform&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;restricted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cc-4200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pci-dss&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;S3 Object Prefix&lt;/th&gt;
&lt;th&gt;&lt;code&gt;team&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;data-classification&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;cost-center&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;compliance-scope&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acme-datalake-features/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ml-platform&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;confidential&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cc-4200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;soc2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acme-datalake-reports/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;analytics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cc-3100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acme-datalake-txn-raw/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ml-platform&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;restricted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cc-4200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pci-dss&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;fraud-detection-svc&lt;/code&gt; role (tagged &lt;code&gt;team=ml-platform&lt;/code&gt;, &lt;code&gt;data-classification=restricted&lt;/code&gt;, &lt;code&gt;compliance-scope=pci-dss&lt;/code&gt;) can access &lt;code&gt;acme-datalake-txn-raw/&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;acme-datalake-reports/&lt;/code&gt;-wrong team, wrong classification, wrong cost center. The &lt;code&gt;analytics-dashboard&lt;/code&gt; role cannot touch PCI-scoped data even if someone accidentally grants &lt;code&gt;s3:GetObject&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt;, because the tag conditions will fail.&lt;/p&gt;

&lt;p&gt;This single policy replaces potentially dozens of static resource-level policies. No MFA in the session? Denied regardless of tag match.&lt;/p&gt;

&lt;h4&gt;
  
  
  Session Policies: Runtime Permission Scoping
&lt;/h4&gt;

&lt;p&gt;When assuming a role via STS, you can attach a &lt;strong&gt;session policy&lt;/strong&gt; that further constrains the role's permissions for that specific session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assume_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;RoleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::123456789012:role/ml-training-pipeline&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RoleSessionName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feature-extraction-daily-2026-09-22&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Policy&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="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2012-10-17&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Statement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Effect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3:GetObject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:s3:::acme-datalake-txn-raw/2026/09/22/*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Condition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;StringEquals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3:ResourceTag/data-classification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3:ResourceTag/compliance-scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pci-dss&lt;/span&gt;&lt;span class="sh"&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="p"&gt;}),&lt;/span&gt;
    &lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triggered-by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;airflow-dag-feature-extract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-2026-09-22-0400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;DurationSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;  &lt;span class="c1"&gt;# 15 minutes - just enough for the ETL window
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting session can &lt;em&gt;only&lt;/em&gt; read today's PCI-scoped transaction partition, even though the underlying &lt;code&gt;ml-training-pipeline&lt;/code&gt; role has broader S3 access. The session tags (&lt;code&gt;triggered-by&lt;/code&gt;, &lt;code&gt;run-id&lt;/code&gt;) flow into CloudTrail, giving auditors a direct link from data access back to the specific Airflow DAG run that triggered it. This is least privilege applied at the session level-each job gets exactly the permissions it needs for exactly the duration it needs them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 2: Network Disintermediation &amp;amp; Microsegmentation
&lt;/h2&gt;

&lt;p&gt;Zero Trust does &lt;strong&gt;not&lt;/strong&gt; mean "networks don't matter." It means the network is hostile by default. Network controls act as guardrails and blast-radius containment, not trust anchors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eliminating Bastions and Open Ingress Ports
&lt;/h3&gt;

&lt;p&gt;Every open inbound port is an attack surface. SSH (22) and RDP (3389) have been the entry point for countless breaches. AWS Systems Manager Session Manager replaces them entirely:&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;# Security group: ZERO inbound rules&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"workload"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"zero-trust-workload-"&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;var&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;# No ingress rules at all&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_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;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# HTTPS to AWS APIs only&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSM, CloudWatch, S3 (gateway endpoint preferred)"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# SSM access is authenticated via IAM, logged to CloudTrail&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"ssm"&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;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workload&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="s2"&gt;"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traffic tunnels over TLS through the AWS API plane. Authentication is IAM. Every session is logged to CloudTrail and optionally streamed to S3/CloudWatch. There is no SSH key to rotate, no bastion to patch, no port to scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port forwarding through SSM&lt;/strong&gt; handles the "but I need to reach the database" objection:&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;# Forward local port 5432 to RDS through SSM - no inbound SG rules needed&lt;/span&gt;
aws ssm start-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; i-0abc123def456 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-name&lt;/span&gt; AWS-StartPortForwardingSessionToRemoteHost &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s1"&gt;'{"host":["mydb.cluster-xyz.us-east-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  VPC Lattice: Layer 7 Service Networking
&lt;/h3&gt;

&lt;p&gt;VPC Lattice replaces the complexity of Transit Gateway routing tables, VPC peering, and perimeter firewalls with service-to-service Layer 7 proxying that enforces IAM authorization &lt;strong&gt;per HTTP request&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                    VPC Lattice                          │
│                                                         │
│  ┌──────────┐    SigV4 Auth    ┌──────────┐            │
│  │ Service A ├────────────────►│ Service B │            │
│  │ (VPC-1)  │  IAM Policy      │ (VPC-2)  │            │
│  └──────────┘  Per-Request     └──────────┘            │
│                                                         │
│  Auth Policy:                                           │
│  ─ Principal matches role/service-a-prod                │
│  ─ HTTP method is GET or POST                           │
│  ─ Path matches /api/v2/*                               │
│  ─ Header X-Request-Source equals "internal"            │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fundamentally different from network-level controls. A Security Group can allow TCP/443 between two CIDR ranges. VPC Lattice can allow &lt;code&gt;GET /api/v2/users&lt;/code&gt; from &lt;code&gt;role/service-a-prod&lt;/code&gt; while denying &lt;code&gt;DELETE /api/v2/users&lt;/code&gt; from the same role. &lt;strong&gt;Authorization granularity moves from Layer 4 to Layer 7.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS PrivateLink: Eliminating Internet Transit
&lt;/h3&gt;

&lt;p&gt;Every AWS API call that traverses the public internet is an interception opportunity. PrivateLink creates VPC interface endpoints that keep traffic entirely within the AWS backbone:&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_vpc_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"s3_interface"&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;var&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;service_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.${var.region}.s3"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_endpoint_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Interface"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&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;private_subnet_ids&lt;/span&gt;

  &lt;span class="c1"&gt;# Endpoint policy: only this account's buckets, only from this VPC&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;jsonencode&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;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&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:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;Resource&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:s3:::acme-datalake-*/*"&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"aws:PrincipalOrgID"&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;org_id&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with a &lt;strong&gt;VPC endpoint policy&lt;/strong&gt;, you enforce that even traffic flowing through the endpoint is scoped to specific buckets and organizational principals. This prevents a compromised workload from exfiltrating data to an attacker-controlled S3 bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Egress Filtering: The Forgotten Half
&lt;/h3&gt;

&lt;p&gt;Most Zero Trust discussions focus on ingress. Egress is where data actually leaves. A compromised container that can reach &lt;code&gt;0.0.0.0/0&lt;/code&gt; on port 443 can exfiltrate to any HTTPS endpoint on the internet.&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;# Egress-only security group - whitelist, not blacklist&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"strict_egress"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"strict-egress-"&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;var&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;# AWS APIs via VPC endpoints only&lt;/span&gt;
  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS APIs via PrivateLink"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Internal service mesh&lt;/span&gt;
  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8443&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8443&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mesh_services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mTLS mesh traffic"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# DNS&lt;/span&gt;
  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;53&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;53&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"udp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&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;vpc_cidr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"VPC DNS resolver"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Everything else: DENIED by default&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine this with &lt;strong&gt;AWS Network Firewall&lt;/strong&gt; or &lt;strong&gt;DNS Firewall&lt;/strong&gt; for domain-level egress filtering when HTTPS inspection is required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 3: Workload Identity &amp;amp; East-West Traffic
&lt;/h2&gt;

&lt;p&gt;Within Kubernetes (EKS) or containerized architectures, east-west traffic inside the same VPC, same node, or even the same pod network namespace is &lt;strong&gt;untrusted&lt;/strong&gt;. A compromised sidecar container must not be able to impersonate the primary application container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mutual TLS (mTLS)
&lt;/h3&gt;

&lt;p&gt;Unencrypted east-west traffic is a passive eavesdropping vector. mTLS provides both wire encryption and cryptographic identity verification for every service-to-service hop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┐                          ┌──────────────┐
│   Service A  │                          │   Service B  │
│              │     mTLS Handshake       │              │
│  ┌────────┐  │◄────────────────────────►│  ┌────────┐  │
│  │Envoy   │  │  1. Present client cert  │  │Envoy   │  │
│  │Sidecar │  │  2. Verify server cert   │  │Sidecar │  │
│  │        │  │  3. Check SPIFFE ID      │  │        │  │
│  └────────┘  │  4. Encrypted channel    │  └────────┘  │
└──────────────┘                          └──────────────┘
         │                                         │
         └──────── Certificates from ──────────────┘
                   SPIRE / Istio CA / 
                   ACM Private CA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Implementation choices and their trade-offs:&lt;/strong&gt;&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;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Istio&lt;/strong&gt; (Envoy-based mesh)&lt;/td&gt;
&lt;td&gt;Full L7 policy, traffic management, observability&lt;/td&gt;
&lt;td&gt;High resource overhead (~100-200MB per sidecar), latency added per hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Linkerd&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lighter weight, Rust-based proxy&lt;/td&gt;
&lt;td&gt;Smaller ecosystem, fewer L7 features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPC Lattice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS-native, no sidecar overhead&lt;/td&gt;
&lt;td&gt;Limited to AWS, less granular than mesh L7 policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Cilium&lt;/strong&gt; (eBPF-based)&lt;/td&gt;
&lt;td&gt;Kernel-level, minimal latency impact&lt;/td&gt;
&lt;td&gt;Requires newer kernels, steeper learning curve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  EKS Pod Identity: Per-Pod IAM Roles
&lt;/h3&gt;

&lt;p&gt;The anti-pattern: all pods on a node share the node's EC2 instance profile. If any pod is compromised, the attacker inherits the union of all IAM permissions that node needs.&lt;/p&gt;

&lt;p&gt;EKS Pod Identity (the successor to IRSA) provides distinct IAM roles per Kubernetes service account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Kubernetes ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;payment-processor&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# EKS Pod Identity Association (via Terraform)&lt;/span&gt;
&lt;span class="c1"&gt;# This binds the K8s SA to a specific IAM role&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eks_pod_identity_association"&lt;/span&gt; &lt;span class="s2"&gt;"payment_processor"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eks_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments"&lt;/span&gt;
  &lt;span class="nx"&gt;service_account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payment-processor"&lt;/span&gt;
  &lt;span class="nx"&gt;role_arn&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_processor&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;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"payment_processor"&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;"eks-payment-processor"&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;jsonencode&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;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&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&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pods.eks.amazonaws.com"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sts:TagSession"&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;# This role can ONLY access the payments DynamoDB table&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"payment_processor"&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;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_processor&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;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&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;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dynamodb:GetItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb:PutItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb:Query"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:dynamodb:*:*:table/payments-*"&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;Now the &lt;code&gt;payment-processor&lt;/code&gt; pod has &lt;em&gt;only&lt;/em&gt; DynamoDB access to payment tables. A compromised logging sidecar on the same node cannot access payment data because it runs under a different service account with a different IAM role.&lt;/p&gt;

&lt;h3&gt;
  
  
  SPIFFE/SPIRE: Cross-Cloud Workload Identity
&lt;/h3&gt;

&lt;p&gt;For hybrid or multi-cloud deployments, cloud-provider-specific identity (IRSA, Pod Identity, GCP Workload Identity) creates lock-in and doesn't work across boundaries. SPIFFE (Secure Production Identity Framework for Everyone) defines a universal workload identity standard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPIFFE ID: spiffe://production.example.com/ns/payments/sa/payment-processor

┌────────────────┐        ┌──────────┐        ┌────────────────┐
│  EKS Workload  │        │  SPIRE   │        │  GCP Workload  │
│                │◄──────►│  Server  │◄──────►│                │
│  SVID Cert:    │        │          │        │  SVID Cert:    │
│  5min TTL      │        │  Central │        │  5min TTL      │
│                │        │  Attestor│        │                │
└────────────────┘        └──────────┘        └────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SPIRE issues short-lived X.509 certificates (SVIDs) with 5-minute TTLs. Workloads present these for mTLS authentication. The certificate's SPIFFE ID is the identity, not the IP address or cloud IAM role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signed Artifact Verification
&lt;/h3&gt;

&lt;p&gt;Zero Trust extends to the software supply chain. An admission controller must verify that container images are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Built by your CI/CD pipeline&lt;/strong&gt; (not pushed manually)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographically signed&lt;/strong&gt; (Sigstore/Cosign, AWS Signer)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scanned and clean&lt;/strong&gt; (no critical CVEs above threshold)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Kyverno ClusterPolicy: Enforce image signatures&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kyverno.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;verify-image-signatures&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validationFailureAction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enforce&lt;/span&gt;
  &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&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;verify-cosign-signature&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;kinds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
      &lt;span class="na"&gt;verifyImages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;imageReferences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123456789012.dkr.ecr.*.amazonaws.com/*"&lt;/span&gt;
          &lt;span class="na"&gt;attestors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;publicKeys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
                      &lt;span class="s"&gt;-----BEGIN PUBLIC KEY-----&lt;/span&gt;
                      &lt;span class="s"&gt;MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...&lt;/span&gt;
                      &lt;span class="s"&gt;-----END PUBLIC KEY-----&lt;/span&gt;
          &lt;span class="na"&gt;attestations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://cosign.sigstore.dev/attestation/vuln/v1&lt;/span&gt;
              &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scanner.result.criticalCount&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
                      &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LessThanOrEquals&lt;/span&gt;
                      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unsigned image or one with critical vulnerabilities is rejected at admission time-before it ever runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 4: Dual-Gate Data Protection
&lt;/h2&gt;

&lt;p&gt;This is where Zero Trust gets architecturally interesting. A single compromised IAM role should not be sufficient to access sensitive data. You need &lt;strong&gt;orthogonal policy gates&lt;/strong&gt; that require independent authorization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request ──► [Gate 1: Network]    ──► [Gate 2: IAM Policy]    ──► [Gate 3: Resource Policy] ──► [Gate 4: KMS Key Policy] ──► Data
             VPC Endpoint             sts:AssumeRole              S3 Bucket Policy                CMK Decrypt
             aws:sourceVpce           Principal perms             Explicit Allow                  Explicit Allow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All four gates must independently allow the request.&lt;/strong&gt; Compromise of any single gate is insufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation: S3 Bucket with Triple-Gate Protection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gate 1: S3 bucket policy - requires specific VPC endpoint AND org&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_policy"&lt;/span&gt; &lt;span class="s2"&gt;"sensitive_data"&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;sensitive&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;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&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;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&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;Sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DenyNonVPCE"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&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;Resource&lt;/span&gt; &lt;span class="p"&gt;=&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;sensitive&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;span class="s2"&gt;"${aws_s3_bucket.sensitive.arn}/*"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;StringNotEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"aws:sourceVpce"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc_endpoint&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;id&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="nx"&gt;Sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DenyOutsideOrg"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&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;Resource&lt;/span&gt; &lt;span class="p"&gt;=&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;sensitive&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;span class="s2"&gt;"${aws_s3_bucket.sensitive.arn}/*"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;StringNotEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"aws:PrincipalOrgID"&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;org_id&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="nx"&gt;Sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowAuthorizedRoles"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorized_role_arns&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;Action&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:GetObject"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${aws_s3_bucket.sensitive.arn}/*"&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;# Gate 2: KMS key policy - independent authorization&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_kms_key"&lt;/span&gt; &lt;span class="s2"&gt;"data_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CMK for sensitive data encryption"&lt;/span&gt;
  &lt;span class="nx"&gt;deletion_window_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
  &lt;span class="nx"&gt;enable_key_rotation&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;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&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;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&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;Sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"KeyAdministration"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::${var.account_id}:role/SecurityAdmin"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Create*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:Describe*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:Enable*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:List*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s2"&gt;"kms:Put*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:Update*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:Revoke*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:Disable*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s2"&gt;"kms:Get*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:Delete*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:ScheduleKeyDeletion"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&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;Sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"KeyUsage"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorized_role_arns&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:GenerateDataKey"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
        &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"kms:ViaService"&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3.${var.region}.amazonaws.com"&lt;/span&gt;
            &lt;span class="s2"&gt;"aws:PrincipalOrgID"&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;org_id&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="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Gate 3: Enforce SSE-KMS on all objects&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;"sensitive"&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;sensitive&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;"aws:kms"&lt;/span&gt;
      &lt;span class="nx"&gt;kms_master_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data_key&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;span class="nx"&gt;bucket_key_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;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; An attacker who compromises an IAM role with &lt;code&gt;s3:GetObject&lt;/code&gt; permissions still cannot read the data unless:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They are calling from within the designated VPC endpoint (network gate)&lt;/li&gt;
&lt;li&gt;The S3 bucket policy explicitly allows their principal (resource policy gate)&lt;/li&gt;
&lt;li&gt;The KMS key policy grants them &lt;code&gt;kms:Decrypt&lt;/code&gt; (encryption gate)&lt;/li&gt;
&lt;li&gt;Their principal belongs to the correct AWS Organization (organizational gate)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compromise of credentials &lt;em&gt;alone&lt;/em&gt; is insufficient. The attacker must also be in the right network position, which dramatically raises the difficulty.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 5: Continuous Verification &amp;amp; Blast Radius Containment
&lt;/h2&gt;

&lt;p&gt;Trust is never binary or permanent. It is continuously evaluated and immediately revoked when anomalies are detected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automated Credential Revocation Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GuardDuty Finding ──► EventBridge Rule ──► Lambda ──► Revoke STS Sessions
(Anomalous API call)   (Pattern match)    (Automated)  (Immediate)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Lambda: Revoke all active sessions for a compromised IAM role
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;iam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;iam&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&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;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;finding_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# GuardDuty findings indicating credential compromise
&lt;/span&gt;    &lt;span class="n"&gt;compromise_indicators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;UnauthorizedAccess:IAMUser/TorIPCaller&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Discovery:S3/MaliciousIPCaller&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;UnauthorizedAccess:IAMUser/MaliciousIPCaller.Custom&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&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;finding_type&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;compromise_indicators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Extract the compromised principal
&lt;/span&gt;    &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resource&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;access_key_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accessKeyDetails&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accessKeyId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;principal_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accessKeyDetails&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;principalId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accessKeyDetails&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;userName&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Revoke all sessions by applying an inline deny-all policy
&lt;/span&gt;    &lt;span class="c1"&gt;# with a time condition that invalidates tokens issued before now
&lt;/span&gt;    &lt;span class="n"&gt;revocation_policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2012-10-17&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Statement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Effect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deny&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Condition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DateLessThan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws:TokenIssueTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Apply to the role (for assumed-role principals)
&lt;/span&gt;    &lt;span class="n"&gt;role_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_role_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;principal_id&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;role_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_role_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;RoleName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;role_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;PolicyName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;EmergencySessionRevocation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;PolicyDocument&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="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;revocation_policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revoked all sessions for role: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Notify security team
&lt;/span&gt;    &lt;span class="n"&gt;sns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sns&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TopicArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:sns:us-east-1:123456789012:security-alerts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CREDENTIAL COMPROMISE: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role_name&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Message&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="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_role_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;principal_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extract role name from principal ID (AROA...)&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;principal_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="c1"&gt;# For assumed roles, principal_id format: AROAEXAMPLE:session-name
&lt;/span&gt;    &lt;span class="c1"&gt;# Look up the role via the access key or other detail
&lt;/span&gt;    &lt;span class="c1"&gt;# Implementation depends on your naming conventions
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The &lt;code&gt;aws:TokenIssueTime&lt;/code&gt; condition is the critical mechanism.&lt;/strong&gt; STS tokens cannot be individually revoked. Instead, you attach an inline policy that denies all actions for tokens issued before the current timestamp. All existing sessions are immediately invalidated. New sessions (post-revocation) are unaffected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Control Policies: Immutable Guardrails
&lt;/h3&gt;

&lt;p&gt;SCPs are non-negotiable boundaries that even account administrators cannot override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreventCloudTrailDisable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:StopLogging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:DeleteTrail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail:UpdateTrail"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringNotLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::*:role/OrganizationSecurityAudit"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreventKMSKeyDeletion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:ScheduleKeyDeletion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:DisableKey"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringNotLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::*:role/KMSAdministrator"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EnforceIMDSv2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:RunInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ec2:*:*:instance/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringNotEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ec2:MetadataHttpTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyPublicS3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutBucketPublicAccessBlock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutAccountPublicAccessBlock"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringNotEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"s3:PublicAccessBlockConfiguration/BlockPublicAcls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key SCPs for Zero Trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prevent CloudTrail tampering&lt;/strong&gt; - an attacker who disables logging can operate undetected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce IMDSv2&lt;/strong&gt; - IMDSv1 is an SSRF-to-credential-theft vector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevent KMS key deletion&lt;/strong&gt; - destroying encryption keys is an irreversible data destruction attack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block public S3&lt;/strong&gt; - no bucket should ever be publicly accessible in a Zero Trust environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  IAM Access Analyzer: Continuous Permission Auditing
&lt;/h3&gt;

&lt;p&gt;IAM Access Analyzer identifies two classes of risk:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;External access&lt;/strong&gt;: Resources (S3 buckets, KMS keys, SQS queues, Lambda functions) with policies granting access to principals outside your AWS Organization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unused access&lt;/strong&gt;: IAM roles and users with permissions they haven't exercised in 90+ days
&lt;/li&gt;
&lt;/ol&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_accessanalyzer_analyzer"&lt;/span&gt; &lt;span class="s2"&gt;"org_level"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analyzer_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"organization-analyzer"&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;"ORGANIZATION"&lt;/span&gt;

  &lt;span class="c1"&gt;# Generates findings for any resource accessible outside the org&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_accessanalyzer_analyzer"&lt;/span&gt; &lt;span class="s2"&gt;"unused_access"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analyzer_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"unused-access-analyzer"&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;"ACCOUNT_UNUSED_ACCESS"&lt;/span&gt;

  &lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;unused_access&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;unused_access_age&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&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;Feed findings into Security Hub, and set up automated remediation for high-severity findings.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reality Check: Common Failure Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The "Network Doesn't Matter" Trap
&lt;/h3&gt;

&lt;p&gt;An overcorrection where engineers ignore VPC routing, Security Group discipline, and egress filtering because "IAM and TLS handle security."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this fails:&lt;/strong&gt; If an application-layer token leaks (JWT in a log, STS credential in a stack trace), network controls are your last line of defense. A VPC endpoint condition in the S3 bucket policy means stolen credentials are useless outside your VPC. Egress filtering prevents a compromised container from reaching a C2 server. Defense-in-depth requires &lt;em&gt;both&lt;/em&gt; identity and network controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Policy Complexity Explosion
&lt;/h3&gt;

&lt;p&gt;A 200-microservice architecture where each service has its own IAM role, KMS key access, S3 bucket policy entry, VPC Lattice auth policy, and Kubernetes NetworkPolicy generates thousands of policy documents. Without automation, this becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unauditable&lt;/strong&gt;: No human can review 2,000 IAM policies for correctness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brittle&lt;/strong&gt;: A single typo in a resource ARN causes a production outage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow&lt;/strong&gt;: Adding a new service requires changes in 5+ policy systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt; Policy-as-code with automated generation. Use Terraform modules, OPA/Rego policy validation, and CI/CD pipelines that lint and test IAM policies before deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Shared Role Shortcut
&lt;/h3&gt;

&lt;p&gt;The most common Zero Trust failure in practice. Teams adopt Zero Trust on paper but run all pods under one or two broad IAM roles because managing per-service roles is operationally expensive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# What the architecture diagram shows:&lt;/span&gt;
Pod A → Role A (scoped to DynamoDB)
Pod B → Role B (scoped to S3)
Pod C → Role C (scoped to SQS)

&lt;span class="gh"&gt;# What actually runs in production:&lt;/span&gt;
Pod A → shared-role (DynamoDB + S3 + SQS + KMS + SNS + ...)
Pod B → shared-role
Pod C → shared-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt; Automated IAM policy generation from application code analysis. Tools like &lt;code&gt;iamlive&lt;/code&gt; can record actual AWS API calls during development and generate least-privilege policies. Pair with IAM Access Analyzer's unused access findings to continuously right-size permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Performance Impact of Per-Request Auth
&lt;/h3&gt;

&lt;p&gt;Enforcing SigV4 authentication on high-throughput east-west microservice calls adds CPU overhead (HMAC-SHA256 computation) and latency (additional auth lookups). At 10,000 RPS between two services, this is measurable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection reuse&lt;/strong&gt;: mTLS amortizes the TLS handshake cost over many requests on a persistent connection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token caching&lt;/strong&gt;: STS credentials are cached for their lifetime (typically 1 hour); not fetched per-request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benchmarking&lt;/strong&gt;: VPC Lattice and mesh proxies must be load-tested against your latency SLOs before production deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selective enforcement&lt;/strong&gt;: Not all east-west traffic requires the same level of verification. Health checks and metrics scraping may use simpler auth&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Maturity Model: Where to Start
&lt;/h2&gt;

&lt;p&gt;Zero Trust is a spectrum, not a binary state. A practical adoption path:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Key Actions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L0: Foundations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Credential hygiene&lt;/td&gt;
&lt;td&gt;Eliminate IAM users, enforce MFA, enable CloudTrail, IMDSv2 everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L1: Perimeter Hardening&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ingress/egress control&lt;/td&gt;
&lt;td&gt;SSM Session Manager, VPC endpoints for AWS services, strict SG egress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L2: Workload Identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-workload IAM&lt;/td&gt;
&lt;td&gt;EKS Pod Identity, OIDC for CI/CD, per-service IAM roles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L3: Data Protection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dual-gate access&lt;/td&gt;
&lt;td&gt;S3 bucket policies + KMS key policies with VPC endpoint conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L4: East-West Encryption&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;mTLS mesh&lt;/td&gt;
&lt;td&gt;Service mesh or VPC Lattice for all inter-service traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L5: Continuous Verification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated response&lt;/td&gt;
&lt;td&gt;GuardDuty → automated revocation, IAM Access Analyzer, Config rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most organizations find substantial security improvement at L2-L3 before tackling the operational complexity of L4-L5.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;Zero Trust on AWS is not a product, a checkbox, or a one-time migration. It's the sustained engineering discipline of treating every component-including your own infrastructure-as potentially compromised. The five pillars reinforce each other: identity without network controls is insufficient, network controls without identity are bypassable, and both without continuous verification give you a false sense of security.&lt;/p&gt;

&lt;p&gt;The hardest part is not the technology. AWS provides all the primitives. The hardest part is the organizational discipline to maintain fine-grained policies at scale without collapsing back to shared roles and broad permissions under delivery pressure.&lt;/p&gt;




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

</description>
      <category>aws</category>
      <category>security</category>
      <category>iam</category>
      <category>devops</category>
    </item>
    <item>
      <title>The 5W Method for DevOps and Platform Engineering</title>
      <dc:creator>Bartłomiej Danek</dc:creator>
      <pubDate>Fri, 29 May 2026 13:45:11 +0000</pubDate>
      <link>https://dev.to/bard/the-5w-method-for-devops-and-platform-engineering-4a3k</link>
      <guid>https://dev.to/bard/the-5w-method-for-devops-and-platform-engineering-4a3k</guid>
      <description>&lt;h1&gt;
  
  
  The 5W Method for DevOps and Platform Engineering
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Why most DevOps communication fails
&lt;/h2&gt;

&lt;p&gt;Every engineer has seen it: a Slack message that says "API is broken" a Jira ticket that says "upgrade Kubernetes," or a pull request description that says "fix infra." These statements feel productive but carry almost no useful information. They force everyone else to ask follow-up questions, guess context, or worse - act on incomplete understanding.&lt;/p&gt;

&lt;p&gt;The 5W method fixes this. It is a structured thinking tool borrowed from journalism and root cause analysis that forces clarity before action. In DevOps, where unclear communication leads directly to outages, failed changes, and wasted engineering time, it is one of the simplest high-impact habits you can adopt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the 5W method?
&lt;/h2&gt;

&lt;p&gt;Five questions. Answer them before you write an incident report, file a change request, plan a task, or escalate a problem.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What it means in DevOps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What happened, or what needs to change?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Why is it needed? Why did it happen? What is the business impact?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Who is affected? Who owns it? Who needs to approve?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which environment, service, cluster, region, account, repository, or pipeline?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When did it start? When should it be done? How urgent is it?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some teams add a sixth - &lt;strong&gt;How?&lt;/strong&gt; - making it 5W1H. The "How" covers implementation approach, rollback plan, and verification steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order depends on the situation
&lt;/h2&gt;

&lt;p&gt;The 5W is not a rigid sequence. The entry point changes depending on whether you are reacting or planning:&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%2F5w-method-flows.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%2F5w-method-flows.png" alt="5W Method: Incident vs Planning Flow" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During an incident, you start with &lt;strong&gt;What happened?&lt;/strong&gt; and work toward the fix. During planning, you start with &lt;strong&gt;Why do we need this?&lt;/strong&gt; and work toward execution. The questions are the same - the flow is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Incident analysis
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Without 5W:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"API is broken."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This tells you nothing. You don't know what is broken, who is affected, or how urgent it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With 5W:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What?
&lt;/h3&gt;

&lt;p&gt;Users cannot log in. &lt;code&gt;/auth/login&lt;/code&gt; returns HTTP 500.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why?
&lt;/h3&gt;

&lt;p&gt;Initial suspicion: database connection errors following the latest deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Affected: all users in production&lt;/li&gt;
&lt;li&gt;Owner: backend team&lt;/li&gt;
&lt;li&gt;Support impact: customer support is receiving tickets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Production EKS cluster&lt;/li&gt;
&lt;li&gt;Service: &lt;code&gt;auth-api&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Region: &lt;code&gt;eu-central-1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Database: production PostgreSQL&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Started around 09:40&lt;/li&gt;
&lt;li&gt;First alert fired at 09:43&lt;/li&gt;
&lt;li&gt;Last deployment happened at 09:35&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now anyone reading this - an on-call engineer, a manager, a support agent - immediately understands the situation and can act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 2: Change request
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Without 5W:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We need to upgrade Kubernetes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This will sit in a backlog forever because nobody knows the scope, urgency, or ownership.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With 5W:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What?
&lt;/h3&gt;

&lt;p&gt;Upgrade EKS clusters from version 1.33 to 1.34.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why?
&lt;/h3&gt;

&lt;p&gt;Current version is approaching end of support. We need access to newer security patches and compatibility fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Platform team owns the upgrade&lt;/li&gt;
&lt;li&gt;Application teams need to validate their workloads&lt;/li&gt;
&lt;li&gt;Security team should be informed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;All non-production clusters first&lt;/li&gt;
&lt;li&gt;Production clusters after validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Dev clusters: this week&lt;/li&gt;
&lt;li&gt;Staging: next week&lt;/li&gt;
&lt;li&gt;Production: after successful validation and rollback plan approval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This change request is now reviewable and approvable without a single follow-up question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Platform engineering task
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Without 5W:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"CI is too slow, let's fix it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;With 5W + How:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What?
&lt;/h3&gt;

&lt;p&gt;Reduce average CI pipeline duration from 18 minutes to below 10 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why?
&lt;/h3&gt;

&lt;p&gt;Slow feedback delays developers and increases context switching. It affects every engineer on every push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Platform team owns the pipeline&lt;/li&gt;
&lt;li&gt;Backend and frontend teams are affected daily&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where?
&lt;/h3&gt;

&lt;p&gt;GitHub Actions workflows across backend and frontend repositories.&lt;/p&gt;

&lt;h3&gt;
  
  
  When?
&lt;/h3&gt;

&lt;p&gt;Target: before the next release cycle. Priority: high - it affects all engineers.&lt;/p&gt;

&lt;h3&gt;
  
  
  How?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add dependency caching&lt;/li&gt;
&lt;li&gt;Split slow jobs into parallel stages&lt;/li&gt;
&lt;li&gt;Run tests concurrently&lt;/li&gt;
&lt;li&gt;Avoid rebuilding unchanged Docker layers&lt;/li&gt;
&lt;li&gt;Measure pipeline duration before and after to validate improvement&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The "How" turns a vague improvement idea into a concrete, measurable plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to apply the 5W method
&lt;/h2&gt;

&lt;p&gt;Use it anywhere DevOps work needs to be communicated clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incident reports&lt;/strong&gt; - so responders have full context immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmortems&lt;/strong&gt; - so root causes are described precisely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change requests&lt;/strong&gt; - so reviewers can approve without guessing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture decisions&lt;/strong&gt; - so trade-offs are explicit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment plans&lt;/strong&gt; - so rollout scope and rollback are clear&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFCs and design docs&lt;/strong&gt; - so proposals are complete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging summaries&lt;/strong&gt; - so handoffs between shifts work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production readiness reviews&lt;/strong&gt; - so nothing is assumed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritization discussions&lt;/strong&gt; - so urgency is justified, not felt&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A reusable template
&lt;/h2&gt;

&lt;p&gt;Copy this into your wiki, your incident template, or your change request form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 5W DevOps Analysis&lt;/span&gt;

&lt;span class="gu"&gt;### What?&lt;/span&gt;
What happened or what do we want to change?

&lt;span class="gu"&gt;### Why?&lt;/span&gt;
Why does it matter? What problem are we solving? What is the impact if we do nothing?

&lt;span class="gu"&gt;### Who?&lt;/span&gt;
Who is affected? Who owns it? Who needs to approve?

&lt;span class="gu"&gt;### Where?&lt;/span&gt;
Which service, environment, cluster, region, account, repository, or pipeline?

&lt;span class="gu"&gt;### When?&lt;/span&gt;
When did it happen? When should it be fixed? How urgent is it?

&lt;span class="gu"&gt;### How? (optional)&lt;/span&gt;
How will we solve it? What is the rollback plan? How will we verify success?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The practical rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use 5W before implementation. Every time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bad DevOps flow:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's change Terraform and see what happens."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Better DevOps flow:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What are we changing, why, who is affected, where does it apply, when does it need to happen, and how do we verify it worked?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first approach leads to random fixes, unclear ownership, and risky production changes. The second takes 5 minutes of thinking and prevents hours of cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The 5W method is not a process framework or a methodology. It is a thinking habit. It costs nothing to adopt, requires no tooling, and works whether you are a junior engineer writing your first incident report or a CTO reviewing a platform migration plan.&lt;/p&gt;

&lt;p&gt;The best DevOps teams are not the ones with the most sophisticated tools. They are the ones that communicate clearly under pressure. Five questions is all it takes.&lt;/p&gt;




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

</description>
      <category>devops</category>
      <category>platform</category>
      <category>methodology</category>
    </item>
    <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;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;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" width="777" height="458"&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" width="799" height="362"&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" width="799" height="474"&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 plaintext"&gt;&lt;code&gt;LRANGE replies:{channel}:* 0 -1
&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>
  </channel>
</rss>
