<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jloudon.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jloudon.com/" rel="alternate" type="text/html" /><updated>2025-11-28T15:04:56+11:00</updated><id>https://jloudon.com/feed.xml</id><title type="html">Jesse Loudon</title><subtitle>Microsoft MVP (Azure), Policy as Code advocate</subtitle><author><name>Jesse</name></author><entry><title type="html">Terragrunt Tips and Tricks From The Field</title><link href="https://jloudon.com/cloud/Terragrunt-Tips-and-Tricks-from-the-Field/" rel="alternate" type="text/html" title="Terragrunt Tips and Tricks From The Field" /><published>2025-11-27T00:00:00+11:00</published><updated>2025-11-27T00:00:00+11:00</updated><id>https://jloudon.com/cloud/Terragrunt%20Tips%20and%20Tricks%20from%20the%20Field</id><content type="html" xml:base="https://jloudon.com/cloud/Terragrunt-Tips-and-Tricks-from-the-Field/"><![CDATA[<p>Hey folks in this blog post I’ll be describing three distinct Terragrunt file patterns for orchestrating your Terraform codebase at scale. These Terragrunt tips and tricks will help you through tricky situations should your Terraform inputs rise in complexity and scale.</p>

<blockquote>
  <p>Terragrunt is an orchestrator for Terraform/OpenTofu and is designed for scalable deployments across multiple environments (e.g. DEV, TEST, UAT, PROD)</p>
</blockquote>

<p>The three patterns we’ll explore:</p>

<table>
  <thead>
    <tr>
      <th>Pattern</th>
      <th>When You Need It</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Simple Input Mapping</strong></td>
      <td>Multi-team configuration with simple data</td>
    </tr>
    <tr>
      <td><strong>TFVars Generation for Large Datasets</strong></td>
      <td>Large datasets hitting CLI argument limits</td>
    </tr>
    <tr>
      <td><strong>Advanced Input Aggregation</strong></td>
      <td>Multi-service architectures with varying schemas</td>
    </tr>
  </tbody>
</table>

<h2 id="pattern-1-simple-input-mapping">Pattern 1: Simple Input Mapping</h2>

<p><strong>Example scenario</strong>: You’re managing platform resources across multiple business units, and each team wants to maintain their own configuration without stepping on each other’s toes.</p>

<p>This is the pattern I typically recommend when you’re starting your <a href="https://terragrunt.gruntwork.io/docs/getting-started/quick-start/">Terragrunt</a> journey or when you have simple data that needs to be aggregated from multiple sources.</p>

<p>The traditional approach would be one massive configuration file that becomes a bottleneck for changes. Not ideal!</p>

<p>Here’s how the simple input mapping pattern might solve this problem. This approach allows different teams to maintain their own configuration files independently. Each business unit can modify their settings without creating merge conflicts or requiring coordination with other teams.</p>

<blockquote>
  <p>For context I used this pattern to manage GitHub repositories and teams across 9 business units with approximately 150+ repositories and 50+ teams.</p>
</blockquote>

<p>First we define each source environment file separately to pull in all HCL data from them into our Terragrunt deployment.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="c1"># Read each file separately</span>
  <span class="nx">business_unit1_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit1-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit2_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit2-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit3_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit3-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit4_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit4-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit5_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit5-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit6_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit6-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit7_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit7-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit8_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit8-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit9_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit9-env.hcl"</span><span class="err">)</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p><a href="https://terragrunt.gruntwork.io/docs/reference/built-in-functions/#read_terragrunt_config"><code class="language-plaintext highlighter-rouge">read_terragrunt_config()</code></a> as shown above allows each team to manage their own config file. The Business Unit 2 team might manage <code class="language-plaintext highlighter-rouge">business-unit2-env.hcl</code> without affecting other team configurations.</p>
</blockquote>

<p>Here’s where we combine the data from the multiple sources above while handling missing or malformed files gracefully. The <a href="https://developer.hashicorp.com/terraform/language/functions/concat"><code class="language-plaintext highlighter-rouge">concat()</code></a> function safely combines lists from multiple sources. The <a href="https://developer.hashicorp.com/terraform/language/functions/try"><code class="language-plaintext highlighter-rouge">try()</code></a> function here can be essential for handling missing config files that might cause deployment failures.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Concatenate the lists from all files into the single variable</span>
<span class="nx">repositories</span> <span class="err">=</span> <span class="nx">concat</span><span class="err">(</span> <span class="c1">#concat used for combining lists/arrays. Use merge for combining maps/objects.</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit1_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit2_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit3_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit4_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit5_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit6_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit7_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit8_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit9_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">)</span>
<span class="err">)</span>

<span class="c1"># Concatenate the lists from all files into the single variable</span>
<span class="nx">teams</span> <span class="err">=</span> <span class="nx">concat</span><span class="err">(</span> <span class="c1">#concat used for combining lists/arrays. Use merge for combining maps/objects.</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit1_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit2_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit3_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit4_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit5_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit6_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit7_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit8_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
  <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit9_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">)</span>
<span class="err">)</span>
</code></pre></div></div>

<p>The final step is straightforward - we pass our aggregated and processed data as inputs to the Terraform module. This keeps the interface clean and predictable.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">inputs</span> <span class="err">=</span> <span class="p">{</span>
  <span class="c1"># Map module variables to local variables</span>
  <span class="nx">repositories</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">repositories</span>
  <span class="nx">teams</span>        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">teams</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="pattern-1-complete-example">Pattern 1: Complete Example</h3>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># terragrunt.hcl - GitHub Platform Onboarding</span>
<span class="c1"># Pattern 1: Simple Input Mapping</span>

<span class="c1"># Include the root configuration</span>
<span class="nx">include</span> <span class="p">{</span>
  <span class="nx">path</span> <span class="p">=</span> <span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"root-terragrunt.hcl"</span><span class="err">)</span>
<span class="p">}</span>

<span class="c1"># Set Terraform stack source location</span>
<span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"${get_repo_root()}/stacks/onboarding"</span>
<span class="p">}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="c1"># Read each file separately</span>
  <span class="nx">business_unit1_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit1-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit2_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit2-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit3_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit3-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit4_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit4-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit5_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit5-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit6_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit6-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit7_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit7-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit8_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit8-env.hcl"</span><span class="err">)</span>
  <span class="nx">business_unit9_env</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"business-unit9-env.hcl"</span><span class="err">)</span>

  <span class="c1"># Concatenate the lists from all files into the single variable</span>
  <span class="nx">repositories</span> <span class="p">=</span> <span class="nx">concat</span><span class="err">(</span> <span class="c1">#concat used for combining lists/arrays. Use merge for combining maps/objects.</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit1_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit2_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit3_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit4_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit5_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit6_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit7_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit8_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit9_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">repositories</span><span class="err">,</span> <span class="p">[]</span><span class="err">)</span>
  <span class="err">)</span>

  <span class="c1"># Concatenate the lists from all files into the single variable</span>
  <span class="nx">teams</span> <span class="p">=</span> <span class="nx">concat</span><span class="err">(</span> <span class="c1">#concat used for combining lists/arrays. Use merge for combining maps/objects.</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit1_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit2_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit3_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit4_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit5_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit6_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit7_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit8_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">),</span>
    <span class="nx">try</span><span class="err">(</span><span class="nx">local</span><span class="err">.</span><span class="nx">business_unit9_env</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">teams</span><span class="err">,</span> <span class="p">[]</span><span class="err">)</span>
  <span class="err">)</span>
<span class="p">}</span>

<span class="nx">inputs</span> <span class="err">=</span> <span class="p">{</span>
  <span class="c1"># Map module variables to local variables</span>
  <span class="nx">repositories</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">repositories</span>
  <span class="nx">teams</span>        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">teams</span>
<span class="p">}</span>

<span class="nx">remote_state</span> <span class="p">{</span>
  <span class="nx">backend</span> <span class="p">=</span> <span class="s2">"azurerm"</span>
  <span class="nx">generate</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">path</span>      <span class="p">=</span> <span class="s2">"_backend.tf"</span>
    <span class="nx">if_exists</span> <span class="p">=</span> <span class="s2">"overwrite_terragrunt"</span>
  <span class="p">}</span>
  <span class="nx">config</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">subscription_id</span>      <span class="p">=</span> <span class="s2">"__TFSTATE_SUBSCRIPTION_ID__"</span>
    <span class="nx">tenant_id</span>            <span class="p">=</span> <span class="s2">"__AZURE_TENANT_ID__"</span>
    <span class="nx">client_id</span>            <span class="p">=</span> <span class="s2">"__AZURE_CLIENT_ID__"</span>
    <span class="nx">use_azuread_auth</span>     <span class="p">=</span> <span class="kc">true</span>
    <span class="nx">use_oidc</span>             <span class="p">=</span> <span class="kc">true</span>
    <span class="nx">resource_group_name</span>  <span class="p">=</span> <span class="s2">"__TFSTATE_RESOURCE_GROUP_NAME__"</span>
    <span class="nx">storage_account_name</span> <span class="p">=</span> <span class="s2">"__TFSTATE_STORAGE_ACCOUNT_NAME__"</span>
    <span class="nx">container_name</span>       <span class="p">=</span> <span class="s2">"__TFSTATE_AZURE_PLATFORM_CONTAINER__"</span>
    <span class="nx">key</span>                  <span class="p">=</span> <span class="s2">"github/terraform.tfstate"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="pattern-2-tfvars-generation-for-large-datasets">Pattern 2: TFVars Generation for Large Datasets</h2>

<p><strong>Example scenario</strong>: You’re managing enterprise infrastructure with thousands of resources and assignments, and your CI/CD workflows keep failing with “argument list too long” errors.</p>

<p>This pattern addresses enterprise scale implementations where CLI argument length limits become a constraint. It provides a practical approach for managing large datasets effectively. When you pass large datasets through Terragrunt inputs, they get converted to command-line arguments for the underlying Terraform execution. And there are limits!</p>

<blockquote>
  <p>I used this pattern to manage Azure RBAC across multiple Azure subscriptions with 2000+ role assignments, 500+ service principals, and 200+ Azure AD groups. But only after I started hitting the limits described above, and in hindsight we should have split up and managed the non-dependent resources separately instead of combining them.</p>
</blockquote>

<p>First, we load configuration from multiple HCL environment inputs sources in the directory hierarchy. Using <a href="https://terragrunt.gruntwork.io/docs/reference/built-in-functions/#find_in_parent_folders"><code class="language-plaintext highlighter-rouge">find_in_parent_folders()</code></a> here is effective for certain folder hierarchies as it automatically discovers configuration files up the directory tree.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="c1"># Automatically load environment-level variables</span>
  <span class="nx">rbac_groups_azure_env_vars</span>  <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"rbac-groups-azure-env.hcl"</span><span class="err">))</span>
  <span class="nx">rbac_groups_github_env_vars</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"rbac-groups-github-env.hcl"</span><span class="err">))</span>
  <span class="nx">rbac_spns_env_vars</span>          <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"rbac-spns-env.hcl"</span><span class="err">))</span>
  <span class="nx">platform_kv_appreg_env_vars</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"platform-kv-appreg-env.hcl"</span><span class="err">))</span>

  <span class="c1"># Map local variables to environment variables</span>
  <span class="nx">subscription_id</span>                             <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_groups_azure_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">subscription_id</span>
  <span class="nx">groups</span>                                      <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_groups_azure_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">groups</span>
  <span class="nx">groups_github</span>                               <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_groups_github_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">groups_github</span>
  <span class="nx">spn_app_registrations</span>                       <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_spns_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">spn_app_registrations</span>
  <span class="nx">spn_role_assignments</span>                        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_spns_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">spn_role_assignments</span>
  <span class="nx">managed_identity_directory_role_assignments</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_spns_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">managed_identity_directory_role_assignments</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is the key insight - we handle small and large datasets differently. Small variables use normal Terragrunt inputs, while large datasets get written to auto-loaded files to avoid CLI limits. This demonstrates the core functionality of <a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#generate">Terragrunt’s <code class="language-plaintext highlighter-rouge">generate</code> blocks</a>. Small, simple variables might use the normal inputs mechanism, while large, complex datasets could be written to <code class="language-plaintext highlighter-rouge">.auto.tfvars.json</code> files that <a href="https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files">Terraform automatically loads</a>.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Generate a tfvars file with the large variable inputs to avoid command-line length limits </span>
<span class="c1"># e.g. 'argument list too long' errors in GitHub workflow runs</span>
<span class="c1"># Terraform will automatically load *.auto.tfvars.json files</span>
<span class="nx">generate</span> <span class="s2">"large_vars"</span> <span class="p">{</span>
  <span class="nx">path</span>              <span class="p">=</span> <span class="s2">"large_vars.auto.tfvars.json"</span>
  <span class="nx">if_exists</span>         <span class="p">=</span> <span class="s2">"overwrite"</span>
  <span class="nx">disable_signature</span> <span class="p">=</span> <span class="kc">true</span>
  <span class="nx">contents</span>  <span class="p">=</span> <span class="nx">jsonencode</span><span class="err">(</span><span class="p">{</span>
    <span class="nx">spn_app_registrations</span>                       <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">spn_app_registrations</span>
    <span class="nx">spn_role_assignments</span>                        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">spn_role_assignments</span>
    <span class="nx">managed_identity_directory_role_assignments</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">managed_identity_directory_role_assignments</span>
    <span class="c1"># add more large variables here as needed if you are seeing 'argument list too long' errors in the GitHub workflow runs.</span>
  <span class="p">}</span><span class="err">)</span>
<span class="p">}</span>

<span class="nx">inputs</span> <span class="err">=</span> <span class="p">{</span>
  <span class="c1"># Map smaller variables via inputs (these are safe for command-line)</span>
  <span class="nx">subscription_id</span>      <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">subscription_id</span>
  <span class="nx">groups</span>               <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">groups</span>
  <span class="nx">groups_github</span>        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">groups_github</span>
  <span class="nx">app_reg_key_vault_id</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">platform_kv_appreg_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">app_reg_key_vault_id</span>

  <span class="c1"># Large variables (spn_app_registrations, spn_role_assignments, managed_identity_directory_role_assignments)</span>
  <span class="c1"># are written to .terraform/large_vars.auto.tfvars.json by the generate 'large_vars' block above.</span>
  <span class="c1"># Terraform will automatically load them from the file</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="pattern-2-complete-example">Pattern 2: Complete Example</h3>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># terragrunt.hcl - Enterprise RBAC Management</span>
<span class="c1"># Pattern 2: TFVars Generation for Large Datasets</span>

<span class="c1"># Include the root configuration</span>
<span class="nx">include</span> <span class="p">{</span>
  <span class="nx">path</span> <span class="p">=</span> <span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"root-terragrunt.hcl"</span><span class="err">)</span>
<span class="p">}</span>

<span class="c1"># Set Terraform stack source location</span>
<span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"${get_repo_root()}/stacks/rbac"</span>
<span class="p">}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="c1"># Automatically load environment-level variables</span>
  <span class="nx">rbac_groups_azure_env_vars</span>  <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"rbac-groups-azure-env.hcl"</span><span class="err">))</span>
  <span class="nx">rbac_groups_github_env_vars</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"rbac-groups-github-env.hcl"</span><span class="err">))</span>
  <span class="nx">rbac_spns_env_vars</span>          <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"rbac-spns-env.hcl"</span><span class="err">))</span>
  <span class="nx">platform_kv_appreg_env_vars</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"platform-kv-appreg-env.hcl"</span><span class="err">))</span>

  <span class="c1"># Map local variables to environment variables</span>
  <span class="nx">subscription_id</span>                             <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_groups_azure_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">subscription_id</span>
  <span class="nx">groups</span>                                      <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_groups_azure_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">groups</span>
  <span class="nx">groups_github</span>                               <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_groups_github_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">groups_github</span>
  <span class="nx">spn_app_registrations</span>                       <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_spns_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">spn_app_registrations</span>
  <span class="nx">spn_role_assignments</span>                        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_spns_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">spn_role_assignments</span>
  <span class="nx">managed_identity_directory_role_assignments</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">rbac_spns_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">managed_identity_directory_role_assignments</span>
<span class="p">}</span>

<span class="c1"># Generate a tfvars file with the large variable inputs to avoid command-line length limits</span>
<span class="c1"># e.g. 'argument list too long' errors in GitHub workflow runs</span>
<span class="c1"># Terraform will automatically load *.auto.tfvars.json files</span>
<span class="nx">generate</span> <span class="s2">"large_vars"</span> <span class="p">{</span>
  <span class="nx">path</span>              <span class="p">=</span> <span class="s2">"large_vars.auto.tfvars.json"</span>
  <span class="nx">if_exists</span>         <span class="p">=</span> <span class="s2">"overwrite"</span>
  <span class="nx">disable_signature</span> <span class="p">=</span> <span class="kc">true</span>
  <span class="nx">contents</span>  <span class="p">=</span> <span class="nx">jsonencode</span><span class="err">(</span><span class="p">{</span>
    <span class="nx">spn_app_registrations</span>                       <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">spn_app_registrations</span>
    <span class="nx">spn_role_assignments</span>                        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">spn_role_assignments</span>
    <span class="nx">managed_identity_directory_role_assignments</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">managed_identity_directory_role_assignments</span>
    <span class="c1"># add more large variables here as needed if you are seeing 'argument list too long' errors in the GitHub workflow runs.</span>
  <span class="p">}</span><span class="err">)</span>
<span class="p">}</span>

<span class="nx">inputs</span> <span class="err">=</span> <span class="p">{</span>
  <span class="c1"># Map smaller variables via inputs (these are safe for command-line)</span>
  <span class="nx">subscription_id</span>      <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">subscription_id</span>
  <span class="nx">groups</span>               <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">groups</span>
  <span class="nx">groups_github</span>        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">groups_github</span>
  <span class="nx">app_reg_key_vault_id</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">platform_kv_appreg_env_vars</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">app_reg_key_vault_id</span>

  <span class="c1"># Large variables (spn_app_registrations, spn_role_assignments, managed_identity_directory_role_assignments)</span>
  <span class="c1"># are written to .terraform/large_vars.auto.tfvars.json by the generate 'large_vars' block above.</span>
  <span class="c1"># Terraform will automatically load them from the file</span>
<span class="p">}</span>

<span class="c1"># Generate an Azure provider block</span>
<span class="nx">generate</span> <span class="s2">"provider"</span> <span class="p">{</span>
  <span class="nx">path</span>      <span class="p">=</span> <span class="s2">"_provider.tf"</span>
  <span class="nx">if_exists</span> <span class="p">=</span> <span class="s2">"overwrite"</span> <span class="c1">#overwriting root terragrunt file</span>
  <span class="nx">contents</span>  <span class="p">=</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "&gt;=4.41.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "3.5.0"
    }
  }
}

provider "azurerm" {
  use_oidc                        = true
  resource_provider_registrations = "extended"
  storage_use_azuread             = true
  subscription_id                 = var.subscription_id
  features {}
}

provider "azuread" {
  tenant_id = "__AZURE_TENANT_ID__"
}
</span><span class="no">
EOF
</span><span class="p">}</span>
</code></pre></div></div>

<h2 id="pattern-3-advanced-input-aggregation">Pattern 3: Advanced Input Aggregation</h2>

<p><strong>Example scenario</strong>: You’re managing complex infrastructure configurations across multiple applications and services, each with different requirements and schemas.</p>

<p>This represents the most advanced pattern for managing complex, multi-service architectures. While this pattern requires careful consideration, it becomes essential for maintaining organised configurations at scale.</p>

<blockquote>
  <p>I used this pattern to manage an Azure Application Gateway with 50+ backend pools, 30+ SSL certificates, 100+ routing rules across multiple applications, plus Azure Front Door with 20+ custom domains and 40+ origins</p>
</blockquote>

<p>First we explicitly define which configuration files to process.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">app_gateway_base</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"app-gateway-env.hcl"</span><span class="err">)</span> <span class="c1"># Contains all the base level configuration for App Gateway</span>
  <span class="nx">frontdoor_base</span>   <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"frontdoor-env.hcl"</span><span class="err">)</span>   <span class="c1"># Contains all the base level configuration for Front Door</span>

  <span class="c1"># List of config files that have various app specific configs, such as Backend pools, HTTP listeners, SSL certs, etc. that need to be merged</span>
  <span class="nx">app_gateway_config_files</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"app-gateway-env.hcl"</span><span class="p">,</span>           <span class="c1">// Base App Gateway Config</span>
    <span class="s2">"app-gateway-application1.hcl"</span><span class="p">,</span>  <span class="c1">// Application1 specific config</span>
    <span class="s2">"app-gateway-application2.hcl"</span><span class="p">,</span>  <span class="c1">// Application2 specific config</span>
    <span class="s2">"app-gateway-application3.hcl"</span><span class="p">,</span>  <span class="c1">// Application3 specific config</span>
    <span class="c1">// Add other config files here as they are added</span>
  <span class="p">]</span>

  <span class="c1"># List of config files that have various frontdoor specific configs, such as Origins, Routes, Custom Domains, etc. that need to be merged</span>
  <span class="nx">frontdoor_config_files</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"frontdoor-env.hcl"</span><span class="p">,</span>           <span class="c1">// Base Front Door Config</span>
    <span class="s2">"frontdoor-application1.hcl"</span><span class="p">,</span>  <span class="c1">// Application1 specific config</span>
    <span class="s2">"frontdoor-application3.hcl"</span><span class="p">,</span>  <span class="c1">// Application3 specific config</span>
    <span class="c1">// Add other config files here as they are added</span>
  <span class="p">]</span>

  <span class="c1"># Read in all the app gateway and frontdoor config files</span>
  <span class="nx">app_gateway_configs</span> <span class="p">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">file</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_config_files</span> <span class="err">:</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">file</span><span class="err">)</span><span class="p">]</span> <span class="c1"># Read in all the app gateway config files</span>
  <span class="nx">frontdoor_configs</span>   <span class="p">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">file</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_config_files</span> <span class="err">:</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">file</span><span class="err">)</span><span class="p">]</span>   <span class="c1"># Read in all the frontdoor config files</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Rather than reading each file individually, we use for expressions to process all configuration files in a single operation. This reduces repetitive code and makes the pattern more maintainable. Using <a href="https://developer.hashicorp.com/terraform/language/expressions/for"><code class="language-plaintext highlighter-rouge">for</code> expressions</a> allows you to process all configuration files efficiently rather than manually reading each one individually.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Read in all the app gateway and frontdoor config files</span>
<span class="nx">app_gateway_configs</span> <span class="err">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">file</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_config_files</span> <span class="err">:</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">file</span><span class="err">)</span><span class="p">]</span>
<span class="nx">frontdoor_configs</span>   <span class="err">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">file</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_config_files</span> <span class="err">:</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">file</span><span class="err">)</span><span class="p">]</span>
</code></pre></div></div>

<p>This is where the real complexity lies - combining data from multiple sources while respecting data types and handling missing values. Each data type requires a specific approach.</p>

<p>Different data types require specific merging approaches:</p>

<ul>
  <li>Lists get flattened with <a href="https://developer.hashicorp.com/terraform/language/functions/flatten"><code class="language-plaintext highlighter-rouge">flatten()</code></a></li>
  <li>Maps get merged with <a href="https://developer.hashicorp.com/terraform/language/functions/merge"><code class="language-plaintext highlighter-rouge">merge()</code></a> and the spread operator</li>
  <li>Everything is wrapped with <a href="https://developer.hashicorp.com/terraform/language/functions/try"><code class="language-plaintext highlighter-rouge">try()</code></a> for safe access</li>
</ul>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">inputs</span> <span class="err">=</span> <span class="p">{</span>
  <span class="c1"># Map application gateway module variables to local variables</span>
  <span class="nx">subscription_id</span>                       <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">subscription_id</span>
  <span class="nx">name</span>                                  <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">name</span>
  <span class="nx">location</span>                              <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span>                   <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">resource_group_name</span>
  <span class="nx">tags</span>                                  <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">tags</span>
  
  <span class="c1"># Complex merging strategies for different data types</span>
  <span class="nx">per_app_waf_policies</span>                  <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">waf_policies</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">backend_address_pools</span>                 <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">backend_address_pools</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">backend_http_settings</span>                 <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">backend_http_settings</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">ssl_certificates</span>                      <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">ssl_certificates</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">http_listeners</span>                        <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">http_listeners</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">request_routing_rules</span>                 <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">request_routing_rules</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  
  <span class="c1"># Map frontdoor module variables to local variables  </span>
  <span class="nx">frontdoor_custom_domains</span>    <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontdoor_custom_domains</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">origins</span>                     <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">origins</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">origin_group_configuration</span>  <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">origin_group_configuration</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">routes</span>                      <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">routes</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Some key callouts:</p>

<ul>
  <li><strong>Data type compatibility</strong>: Using <code class="language-plaintext highlighter-rouge">concat()</code> on maps or <code class="language-plaintext highlighter-rouge">merge()</code> on lists will cause errors</li>
  <li><strong>Spread operator usage</strong>: <code class="language-plaintext highlighter-rouge">merge([map1, map2]...)</code> flattens the list of maps before merging</li>
</ul>

<p>This pattern is appropriate when:</p>

<ul>
  <li>Multiple services have different configuration schemas</li>
  <li>Adding new services requires touching multiple configuration areas</li>
  <li>Team members frequently need to locate specific configuration settings</li>
  <li>Configuration management overhead impacts development productivity</li>
</ul>

<h3 id="pattern-3-complete-example">Pattern 3: Complete Example</h3>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># terragrunt.hcl - Application Connectivity (App Gateway + Front Door)</span>
<span class="c1"># Pattern 3: Advanced Input Aggregation</span>

<span class="c1"># Include the root configuration</span>
<span class="nx">include</span> <span class="p">{</span>
  <span class="nx">path</span> <span class="p">=</span> <span class="nx">find_in_parent_folders</span><span class="err">(</span><span class="s2">"root-terragrunt.hcl"</span><span class="err">)</span>
<span class="p">}</span>

<span class="c1"># Set Terraform stack source location</span>
<span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"${get_repo_root()}/stacks/app-connectivity"</span>
<span class="p">}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">app_gateway_base</span> <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"app-gateway-env.hcl"</span><span class="err">)</span> <span class="c1"># Contains all the base level configuration for App Gateway</span>
  <span class="nx">frontdoor_base</span>   <span class="p">=</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="s2">"frontdoor-env.hcl"</span><span class="err">)</span>   <span class="c1"># Contains all the base level configuration for Front Door</span>

  <span class="c1"># List of config files that have various app specific configs, such as Backend pools, HTTP listeners, SSL certs, etc. that need to be merged</span>
  <span class="nx">app_gateway_config_files</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"app-gateway-env.hcl"</span><span class="p">,</span>           <span class="c1">// Base App Gateway Config</span>
    <span class="s2">"app-gateway-application1.hcl"</span><span class="p">,</span>  <span class="c1">// Application1 specific config</span>
    <span class="s2">"app-gateway-application2.hcl"</span><span class="p">,</span>  <span class="c1">// Application2 specific config</span>
    <span class="s2">"app-gateway-application3.hcl"</span><span class="p">,</span>  <span class="c1">// Application3 specific config</span>
    <span class="c1">// Add other config files here as they are added</span>
  <span class="p">]</span>

  <span class="c1"># List of config files that have various frontdoor specific configs, such as Origins, Routes, Custom Domains, etc. that need to be merged</span>
  <span class="nx">frontdoor_config_files</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"frontdoor-env.hcl"</span><span class="p">,</span>           <span class="c1">// Base Front Door Config</span>
    <span class="s2">"frontdoor-application1.hcl"</span><span class="p">,</span>  <span class="c1">// Application1 specific config</span>
    <span class="s2">"frontdoor-application3.hcl"</span><span class="p">,</span>  <span class="c1">// Application3 specific config</span>
    <span class="c1">// Add other config files here as they are added</span>
  <span class="p">]</span>

  <span class="c1"># Read in all the app gateway and frontdoor config files</span>
  <span class="nx">app_gateway_configs</span> <span class="p">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">file</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_config_files</span> <span class="err">:</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">file</span><span class="err">)</span><span class="p">]</span> <span class="c1"># Read in all the app gateway config files</span>
  <span class="nx">frontdoor_configs</span>   <span class="p">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">file</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_config_files</span> <span class="err">:</span> <span class="nx">read_terragrunt_config</span><span class="err">(</span><span class="nx">file</span><span class="err">)</span><span class="p">]</span>   <span class="c1"># Read in all the frontdoor config files</span>
<span class="p">}</span>

<span class="nx">inputs</span> <span class="err">=</span> <span class="p">{</span>
  <span class="c1"># Map application gateway module variables to local variables</span>
  <span class="nx">subscription_id</span>                       <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">subscription_id</span>
  <span class="nx">name</span>                                  <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">name</span>
  <span class="nx">location</span>                              <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span>                   <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">resource_group_name</span>
  <span class="nx">tags</span>                                  <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">tags</span>
  <span class="nx">sku</span>                                   <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">sku</span>
  <span class="nx">gateway_ip_configuration_name</span>         <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">gateway_ip_configuration_name</span>
  <span class="nx">gateway_ip_configuration_subnet_id</span>    <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">gateway_ip_configuration_subnet_id</span>
  <span class="nx">frontend_port_name</span>                    <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontend_port_name</span>
  <span class="nx">frontend_port_number</span>                  <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontend_port_number</span>
  <span class="nx">frontend_ip_configuration</span>             <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontend_ip_configuration</span>
  <span class="nx">private_link_configurations</span>           <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">private_link_configurations</span>
  <span class="nx">enable_diagnostics</span>                    <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">enable_diagnostics</span>
  <span class="nx">diagnostic_log_category_groups</span>        <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">diagnostic_log_category_groups</span>
  <span class="nx">diagnostic_metrics</span>                    <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">diagnostic_metrics</span>
  <span class="nx">diagnostic_log_analytics_workspace_id</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">diagnostic_log_analytics_workspace_id</span>
  <span class="nx">alerts_resource_group_name</span>            <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">alerts_resource_group_name</span>
  <span class="nx">action_groups</span>                         <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">action_groups</span>
  <span class="nx">application_gateway_base_alert_config</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">base_alert_config</span>
  <span class="nx">trusted_root_certificates</span>             <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">trusted_root_certificates</span>
  <span class="nx">zones</span>                                 <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">zones</span>
  <span class="nx">waf_policy</span>                            <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">waf_policy</span>
  
  <span class="c1"># Complex merging strategies for different data types</span>
  <span class="nx">per_app_waf_policies</span>                  <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">waf_policies</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">backend_address_pools</span>                 <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">backend_address_pools</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">backend_http_settings</span>                 <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">backend_http_settings</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">ssl_certificates</span>                      <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">ssl_certificates</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">http_listeners</span>                        <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">http_listeners</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">request_routing_rules</span>                 <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">request_routing_rules</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">rewrite_rule_sets</span>                     <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">rewrite_rule_sets</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>
  <span class="nx">probes</span>                                <span class="p">=</span> <span class="nx">flatten</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">app_gateway_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">probes</span><span class="p">,</span> <span class="p">[]</span><span class="err">)</span><span class="p">]</span><span class="err">)</span>

  <span class="c1"># Map frontdoor module variables to local variables</span>
  <span class="nx">deploy_frontdoor</span>            <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">deploy_frontdoor</span>
  <span class="nx">frontdoor_profile_name</span>      <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontdoor_profile_name</span>
  <span class="nx">frontdoor_sku</span>               <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontdoor_sku</span>
  <span class="nx">frontdoor_endpoints</span>         <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontdoor_endpoints</span>
  <span class="nx">enable_frontdoor_waf_policy</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">enable_frontdoor_waf_policy</span>
  <span class="nx">frontdoor_base_alert_config</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_base</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">base_alert_config</span>
  <span class="nx">frontdoor_custom_domains</span>    <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">frontdoor_custom_domains</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">origins</span>                     <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">origins</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">origin_group_configuration</span>  <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">origin_group_configuration</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
  <span class="nx">routes</span>                      <span class="p">=</span> <span class="nx">merge</span><span class="err">(</span><span class="p">[</span><span class="nx">for</span> <span class="nx">config</span> <span class="nx">in</span> <span class="nx">local</span><span class="err">.</span><span class="nx">frontdoor_configs</span> <span class="err">:</span> <span class="nx">try</span><span class="err">(</span><span class="nx">config</span><span class="err">.</span><span class="nx">locals</span><span class="err">.</span><span class="nx">routes</span><span class="p">,</span> <span class="p">{}</span><span class="err">)</span><span class="p">]</span><span class="err">...)</span>
<span class="p">}</span>

<span class="c1"># Generate an Azure provider block</span>
<span class="nx">generate</span> <span class="s2">"provider"</span> <span class="p">{</span>
  <span class="nx">path</span>      <span class="p">=</span> <span class="s2">"_provider.tf"</span>
  <span class="nx">if_exists</span> <span class="p">=</span> <span class="s2">"overwrite"</span> <span class="c1">#overwriting root terragrunt file</span>
  <span class="nx">contents</span>  <span class="p">=</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~&gt; 4.35"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "~&gt; 3.4"
    }
  }
}

provider "azurerm" {
  subscription_id                 = var.subscription_id
  use_oidc                        = true
  resource_provider_registrations = "extended" #A larger set of resource providers that provides coverage for the most common supported resources. See this doco https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#resource-provider-registrations
  resource_providers_to_register  = [] #Pass a list of strings here to register specific resource providers for example "Microsoft.AlertsManagement". See this list https://github.com/hashicorp/terraform-provider-azurerm/blob/main/internal/resourceproviders/required.go
  storage_use_azuread             = true
  features {}
}

provider "azuread" {
  tenant_id = "__AZURE_TENANT_ID__"
}
</span><span class="no">
EOF
</span><span class="p">}</span>
</code></pre></div></div>

<h2 id="choosing-the-right-pattern-for-your-team">Choosing the Right Pattern for Your Team</h2>

<p>After implementing these patterns across different organisations and use cases, here’s my decision framework:</p>

<h3 id="start-with-simple-input-mapping-if-you-have">Start with Simple Input Mapping if you have</h3>

<ul>
  <li>Configuration that’s relatively small (under 50 resources per environment)</li>
  <li>Data structure that’s uniform across sources</li>
</ul>

<h3 id="move-to-tfvars-generation-for-large-datasets-when-you-encounter">Move to TFVars Generation for Large Datasets when you encounter</h3>

<ul>
  <li>Large datasets that are hitting CLI limits (you’ll know when it happens!)</li>
</ul>

<h3 id="use-advanced-input-aggregation-when-youre-managing">Use Advanced Input Aggregation when you’re managing</h3>

<ul>
  <li>Multi-service architectures with varying configuration schemas</li>
  <li>Services that need flexible addition/removal capabilities</li>
  <li>Configuration complexity that varies significantly between components</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>These three Terragrunt patterns shown above address common infrastructure management challenges at different scales. Each pattern emerged from practical requirements for managing Terraform configurations effectively.</p>

<p><a href="https://terragrunt.gruntwork.io/">Terragrunt</a> can provide flexibility to adapt configuration patterns as your Terraform/OpenTofu requirements grow.</p>

<p>Thanks for reading, looking forward to your thoughts below,</p>

<p>Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="terragrunt" /><category term="terraform" /><category term="infrastructure as code" /><summary type="html"><![CDATA[Discover three distinct Terragrunt file patterns for managing your Terraform at scale.]]></summary></entry><entry><title type="html">Lessons Learned Adopting Azure Virtual WAN</title><link href="https://jloudon.com/cloud/Lessons-Learned-Adopting-Azure-Virtual-WAN/" rel="alternate" type="text/html" title="Lessons Learned Adopting Azure Virtual WAN" /><published>2025-11-24T00:00:00+11:00</published><updated>2025-11-24T00:00:00+11:00</updated><id>https://jloudon.com/cloud/Lessons%20Learned%20Adopting%20Azure%20Virtual%20WAN</id><content type="html" xml:base="https://jloudon.com/cloud/Lessons-Learned-Adopting-Azure-Virtual-WAN/"><![CDATA[<p>Hey folks in this blog post I’ll be stepping you through some lessons learned adopting Azure Virtual WAN from a traditional hub and spoke architecture. Azure Virtual WAN (vWAN) provides a solid foundation to modernise your network architecture. Given the maturity and feature status of vWAN I’m seeing a steady flow of customers choose it as their greenfield solution to replace an existing brownfield solution. If you’re just getting up to speed with vWAN’s architecture I recommend checking out the <a href="https://learn.microsoft.com/en-us/azure/virtual-wan/virtual-wan-about">Microsoft docs</a> and if possible getting hands-on with it in a lab environment.</p>

<p>Before we get cracking my assumption is that you have a basic to intermediate understanding of how Azure networking works so I can make this writeup as streamlined as possible!</p>

<p>Firstly let’s look at a potential high-level migration architecture diagram as shown in the below. This helps set the scene for what’s to come.</p>

<ul>
  <li>A brownfields hub and spoke architecture exists with IaaS-based shared services deployed into the hub VNET alongside an existing ExpressRoute Circuit providing connectivity to on-premises.</li>
  <li>A greenfields virtual WAN hub will be deployed with PaaS-based shared services and connected to the brownfields ExpressRoute Circuit to achieve hub to hub connectivity and hybrid connectivity to on-premises.</li>
</ul>

<p><img src="/assets/images/vwan-migration-arch.png" alt="Azure Virtual WAN Migration Architecture" title="Azure Virtual WAN Migration Architecture" /></p>

<p>Based on Microsoft’s published guidance on <a href="https://learn.microsoft.com/en-us/azure/virtual-wan/migrate-from-hub-spoke-topology">migrating to Azure Virtual WAN</a> the ideal sequence is:</p>

<ol>
  <li>Deploy Virtual WAN hub(s)</li>
  <li>Connect remote sites (ExpressRoute/VPN) to Virtual WAN</li>
  <li>Test hybrid connectivity via Virtual WAN</li>
  <li>Transition connectivity to Virtual WAN hub</li>
  <li>Old hub becomes shared services spoke</li>
  <li>Optimise on-premises connectivity to fully utilise Virtual WAN</li>
</ol>

<p>Whilst I won’t go into specific instructions/detail on each of the above steps (because Microsoft’s doco is solid with diagrams to help understand the changes) I will touch on a few related points that caught me out during a recent project.</p>

<h2 id="multi-region-architecture">Multi Region Architecture</h2>

<p>If you’re implementing a primary region and secondary region deployment of vWAN to cover multi-region requirements the thing which might not be obvious is that you will need to have vWAN Hub in both regions tied to the same vWAN.</p>

<p>So for example:</p>

<ul>
  <li>Australia East Region - AE vWAN + AE vWAN Hub deployed</li>
  <li>Australia SouthEast Region - ASE vWAN Hub deployed (and associated to AE vWAN)</li>
</ul>

<p>Having your vWAN Hubs associated to the same vWAN ensures that hub to hub connectivity and routing is automatically established and there’s no need to create VPN/ExpressRoute connections directly between your vWAN Hubs in either region.</p>

<h2 id="allowing-traffic-from-non-virtual-wan-networks">Allowing Traffic from Non-Virtual WAN Networks</h2>

<p>If you’re about to deploy Virtual WAN you should know that the default behaviour for enabling traffic from Non-Virtual WAN networks has changed (<a href="https://techcommunity.microsoft.com/blog/azurenetworkingblog/customisation-controls-for-connectivity-between-virtual-networks-over-expressrou/4147722">since 2024 I think</a>) and takes a few more steps to achieve.</p>

<p>I was caught out by this after creating an ExpressRoute connection to link my new vWAN hub to the old hub’s ExpressRoute Circuit then noticing that none of the old hub’s routes were being received on vWAN side :)</p>

<p>To resolve this you need to firstly ensure that your vWAN hub has this option “Allow traffic from non Virtual WAN networks” turned on.</p>

<p><img src="/assets/images/vwan-migration-1.png" alt="Allowing Traffic from Non-Virtual WAN Networks 1" title="Allowing Traffic from Non-Virtual WAN Networks 1" /></p>

<p>Additionally your vWAN ExpressRoute Gateway needs to have the “allowNonVirtualWanTraffic” property set to true.</p>

<p><img src="/assets/images/vwan-migration-2.png" alt="Allowing Traffic from Non-Virtual WAN Networks 2" title="Allowing Traffic from Non-Virtual WAN Networks 2" /></p>

<p>And finally on your old hub side - ensure the virtual network gateway for ExpressRoute has this option for “Allow traffic from remote Virtual WAN networks” turned on.</p>

<p><img src="/assets/images/vwan-migration-3.png" alt="Allowing Traffic from Non-Virtual WAN Networks 3" title="Allowing Traffic from Non-Virtual WAN Networks 3" /></p>

<p>So there’s a couple non-default hoops to jump through there but once you do enable the above you’ll get the delightful result of seeing your old hub’s routes coming through to your vWAN side.</p>

<h2 id="micro-segmentation-traffic-inspection">Micro Segmentation Traffic Inspection</h2>

<p>If you’re using Routing Intent on your vWAN hub to centrally manage routing for your connected spokes the vWAN’s default route table will automatically include all RFC1918 super-nets (10.0.0.0/8, 192.168.0.0/16 and 172.16.0.0/12) but did you know that traffic between subnets in the same VNET will not go through these RFC1918 routes and hence not be inspected by your hub firewall? The same applies for traffic between hosts in the same subnet.</p>

<p>This is because each VNET has a system route which, because it’s often a smaller prefix than the RFC1918 super-nets in the vWAN default route table, will remain active and in use.</p>

<p>To demonstrate this scenario in my lab I created a secured vWAN hub with routing intent policies enabled  and a connected spoke VNET (10.1.0.0/16) with 2 subnets 10.1.1.0/24 and 10.1.2.0/24.</p>

<p>Below you can see my next hop test result between subnets showing traffic sending to the VirtualNetwork via that system route I mentioned earlier. This is the default behaviour for subnet to subnet traffic even within a vWAN architecture with routing intent policies enabled, and it means traffic between these subnets and hosts in the same VNET is not being inspected by the hub firewall.</p>

<p><img src="/assets/images/vwan-migration-4.png" alt="Microsegmentation Traffic Inspection 1" title="Microsegmentation Traffic Inspection 1" /></p>

<p>Now to achieve micro segmentation I’ve created a route table associated to my subnets and a UDR for my VNET 10.1.0.0/16 to send to the firewall IP address 10.0.0.132. You can see based on the test result below that traffic between subnets is now directed to the firewall in the hub.</p>

<p><img src="/assets/images/vwan-migration-5.png" alt="Microsegmentation Traffic Inspection 2" title="Microsegmentation Traffic Inspection 2" /></p>

<p>This final test result also shows that with the route table and UDR addition, traffic between hosts in the same subnet is also now directed to the firewall in the hub.</p>

<p><img src="/assets/images/vwan-migration-6.png" alt="Microsegmentation Traffic Inspection 3" title="Microsegmentation Traffic Inspection 3" /></p>

<p>As demonstrated above, implementing micro segmentation so that host to host and subnet to subnet traffic within the same VNET is inspected at your hub firewall can be achieved within a vWAN architecture however I don’t recommend doing this at scale for every workload in Azure as it may not be supported for certain PaaS architectures. Consult the Microsoft documentation on this before jumping in!</p>

<h2 id="transitioning-azure-dns-from-old-hub-to-new-hub">Transitioning Azure DNS from Old Hub to New Hub</h2>

<p>When adopting Azure Virtual WAN in a greenfields deployment coming from a traditional hub and spoke architecture a common scenario may resemble the below diagram.</p>

<ul>
  <li>A brownfields hub and spoke with existing Private DNS Zones to service the brownfields landing zones and on-premises AD DC conditional forwarders setup to the brownfields hub.</li>
  <li>A greenfields Virtual WAN with Private DNS Resolver and new Private DNS Zones to service the greenfields landing zones.</li>
</ul>

<p><img src="/assets/images/dns-migration-arch.png" alt="Azure DNS Migration Architecture" title="Azure DNS Migration Architecture" /></p>

<p>Because it’s a high management overhead to run two Azure DNS hubs side by side for a long period of time I recommend planning and executing a transition to your greenfields deployment. Some key activities to consider and sequence are:</p>

<ul>
  <li>Scripted syncing of Private DNS records from existing brownfields hub Private DNS zones to greenfields hub Private DNS zones</li>
  <li>Updating any brownfields Private Endpoints to point to greenfields Private DNS zones.</li>
  <li>Enabling Route Table network policies for brownfields private endpoints on the subnets where Private Endpoints are deployed to <a href="https://learn.microsoft.com/en-us/azure/virtual-wan/how-to-routing-policies#troubleshooting-data-path">prevent asymmetric routing from on-prem traffic to private endpoints</a></li>
  <li>Updating on-premises AD DNS conditional forwarders to point to the greenfields Private DNS Resolver service.</li>
  <li>Removing brownfields Private DNS Zone virtual network links to the brownfields hub VNET to avoid private DNS resolution conflicts.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>I hope you enjoyed reading, looking forward to your thoughts below.</p>

<p>Cheers,
Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="azure virtual wan" /><category term="lessons learned" /><summary type="html"><![CDATA[Discover lessons learned adopting Azure Virtual WAN from a traditional hub and spoke architecture]]></summary></entry><entry><title type="html">Palo Alto Cloud NGFW SCM Deployment with Terraform</title><link href="https://jloudon.com/cloud/Deploying-Palo-Alto-Cloud-NGFW-with-Terraform-and-AzAPI/" rel="alternate" type="text/html" title="Palo Alto Cloud NGFW SCM Deployment with Terraform" /><published>2025-11-20T00:00:00+11:00</published><updated>2025-11-20T00:00:00+11:00</updated><id>https://jloudon.com/cloud/Deploying%20Palo%20Alto%20Cloud%20NGFW%20with%20Terraform%20and%20AzAPI</id><content type="html" xml:base="https://jloudon.com/cloud/Deploying-Palo-Alto-Cloud-NGFW-with-Terraform-and-AzAPI/"><![CDATA[<p>Hey folks in this blog post I’m going to cover how you can get started with deploying Palo Alto Cloud Next-Generation Firewall (NGFW) with Strata Cloud Manager integration using Terraform and AzAPI.</p>

<p>This blog will cover the IaC side of things and to keep this short and sweet I’m assuming you already have working knowledge of how to deploy your IaC via CICD workflows/pipelines.</p>

<p>Be sure to also checkout the <a href="https://docs.paloaltonetworks.com/cloud-ngfw-azure/getting-started/introducing-cloud-ngfw-for-azure">Palo Alto documentation on Cloud NGFW</a> to fill in any gaps for the overall deployment prerequisities and configuration. One thing that caught me out early on was onboarding the Azure Tenant to the Strata Cloud Manager account and ensuring the required Azure resource providers were registered on the target subscription.</p>

<h2 id="provider-config">Provider Config</h2>

<p>These are the providers I pinned to for my Palo Alto deployment. Note that I am using AzAPI provider because at the time of writing AzureRM did not have a resource covering my usecase to deploy a Palo Alto NGFW with Strata Cloud Manager (SCM) integration.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"&gt;= 1.3"</span>
  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">azurerm</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/azurerm"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"&gt;= 4.39.0"</span>
    <span class="p">}</span>
    <span class="nx">azapi</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"Azure/azapi"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"&gt;= 2.4"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="azurerm-resources">AzureRM Resources</h2>

<p>Here I’m creating the Palo Alto virtual network appliance which I’m associating to my virtual WAN hub ID.</p>

<p>There’s also 2 separate baseline public IPs resources to cover the Egress NAT traffic and standard traffic. Additional public IPs can be easily created by incrementing the related count variable value.</p>

<p>Finally there’s a user assigned managed identity which is required by the Palo Alto NGFW as it didn’t support a system managed identity.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"azurerm_client_config"</span> <span class="s2">"current"</span> <span class="p">{}</span>

<span class="k">resource</span> <span class="s2">"azurerm_palo_alto_virtual_network_appliance"</span> <span class="s2">"this"</span> <span class="p">{</span>
  <span class="nx">name</span>           <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">appliance_name</span>
  <span class="nx">virtual_hub_id</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">virtual_hub_id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"azurerm_public_ip"</span> <span class="s2">"egress_nat"</span> <span class="p">{</span>
  <span class="nx">for_each</span>            <span class="p">=</span> <span class="p">{</span> <span class="nx">for</span> <span class="nx">i</span> <span class="nx">in</span> <span class="nx">range</span><span class="p">(</span><span class="kd">var</span><span class="p">.</span><span class="nx">egress_nat_ip_address_count</span><span class="p">)</span> <span class="err">:</span> <span class="nx">i</span> <span class="p">=</span><span class="err">&gt;</span> <span class="nx">i</span> <span class="p">}</span>
  <span class="nx">name</span>                <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">firewall_name</span><span class="k">}</span><span class="s2">-egress-nat-pip-</span><span class="k">${</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">location</span>            <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">resource_group_name</span>
  <span class="nx">sku</span>                 <span class="p">=</span> <span class="s2">"Standard"</span>
  <span class="nx">allocation_method</span>   <span class="p">=</span> <span class="s2">"Static"</span>
  <span class="nx">tags</span>                <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">tags</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"azurerm_public_ip"</span> <span class="s2">"this"</span> <span class="p">{</span>
  <span class="nx">for_each</span>            <span class="p">=</span> <span class="p">{</span> <span class="nx">for</span> <span class="nx">i</span> <span class="nx">in</span> <span class="nx">range</span><span class="p">(</span><span class="kd">var</span><span class="p">.</span><span class="nx">public_ip_address_count</span><span class="p">)</span> <span class="err">:</span> <span class="nx">i</span> <span class="p">=</span><span class="err">&gt;</span> <span class="nx">i</span> <span class="p">}</span>
  <span class="nx">name</span>                <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">firewall_name</span><span class="k">}</span><span class="s2">-pip-</span><span class="k">${</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">location</span>            <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">resource_group_name</span>
  <span class="nx">sku</span>                 <span class="p">=</span> <span class="s2">"Standard"</span>
  <span class="nx">allocation_method</span>   <span class="p">=</span> <span class="s2">"Static"</span>
  <span class="nx">tags</span>                <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">tags</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"azurerm_user_assigned_identity"</span> <span class="s2">"this"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">firewall_name</span><span class="k">}</span><span class="s2">-uami"</span>
  <span class="nx">location</span>            <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">resource_group_name</span>
  <span class="nx">tags</span>                <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">tags</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="azapi-resources">AzAPI Resources</h2>

<p>The AzAPI resource here is fairly simple:</p>

<ul>
  <li>set the parent_id to the RG</li>
  <li>pass through the user assiged identity ID</li>
  <li>configure the FW to use Strata Cloud Manager or Panorama based on a variable value (var.cloud_managed_type)</li>
  <li>pass through the Strata Cloud Manager config or Panorama config based on a variable value (var.cloud_managed_type)</li>
  <li>for each on the public IPs based on the count</li>
  <li>link the FW to the network virtual appliance and VWAN Hub</li>
</ul>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"azapi_resource"</span> <span class="s2">"this"</span> <span class="p">{</span>
  <span class="nx">type</span>      <span class="p">=</span> <span class="s2">"PaloAltoNetworks.Cloudngfw/firewalls@2025-05-23"</span>
  <span class="nx">name</span>      <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">firewall_name</span>
  <span class="nx">parent_id</span> <span class="p">=</span> <span class="s2">"/subscriptions/</span><span class="k">${data</span><span class="p">.</span><span class="nx">azurerm_client_config</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">subscription_id</span><span class="k">}</span><span class="s2">/resourceGroups/</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">resource_group_name</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">location</span>  <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">tags</span>      <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">tags</span>
  <span class="nx">identity</span> <span class="p">{</span>
    <span class="nx">type</span>         <span class="p">=</span> <span class="s2">"UserAssigned"</span> <span class="c1"># Only user assigned identity is supported at this time</span>
    <span class="nx">identity_ids</span> <span class="p">=</span> <span class="p">[</span><span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">this</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="p">}</span>
  <span class="nx">body</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">properties</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">dnsSettings</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">dnsServers</span> <span class="p">=</span> <span class="p">[</span>
          <span class="nx">for</span> <span class="nx">dns_server</span> <span class="nx">in</span> <span class="kd">var</span><span class="p">.</span><span class="nx">dns_settings</span><span class="p">.</span><span class="nx">dns_servers</span> <span class="err">:</span> <span class="p">{</span>
            <span class="nx">address</span>    <span class="p">=</span> <span class="nx">dns_server</span><span class="p">.</span><span class="nx">address</span>
            <span class="nx">resourceId</span> <span class="p">=</span> <span class="nx">dns_server</span><span class="p">.</span><span class="nx">resourceId</span>
          <span class="p">}</span>
        <span class="p">]</span>
        <span class="nx">enabledDnsType</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">dns_settings</span><span class="p">.</span><span class="nx">enabled_dns_type</span>
        <span class="nx">enableDnsProxy</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">dns_settings</span><span class="p">.</span><span class="nx">enable_dns_proxy</span>
      <span class="p">}</span>
      <span class="nx">isStrataCloudManaged</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">cloud_managed_type</span> <span class="p">==</span> <span class="s2">"Strata"</span> <span class="err">?</span> <span class="kc">true</span> <span class="err">:</span> <span class="kc">false</span>
      <span class="nx">isPanoramaManaged</span>    <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">cloud_managed_type</span> <span class="p">==</span> <span class="s2">"Panorama"</span> <span class="err">?</span> <span class="kc">true</span> <span class="err">:</span> <span class="kc">false</span>
      <span class="nx">networkProfile</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">egressNatIp</span> <span class="p">=</span> <span class="p">[</span>
          <span class="nx">for</span> <span class="nx">i</span> <span class="nx">in</span> <span class="nx">range</span><span class="p">(</span><span class="kd">var</span><span class="p">.</span><span class="nx">egress_nat_ip_address_count</span><span class="p">)</span> <span class="err">:</span> <span class="p">{</span>
            <span class="nx">address</span>    <span class="p">=</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">egress_nat</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">ip_address</span>
            <span class="nx">resourceId</span> <span class="p">=</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">egress_nat</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">id</span>
          <span class="p">}</span>
        <span class="p">]</span>
        <span class="nx">enableEgressNat</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">enable_egress_nat</span>
        <span class="nx">networkType</span>     <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">network_type</span>
        <span class="nx">publicIps</span> <span class="p">=</span> <span class="p">[</span>
          <span class="nx">for</span> <span class="nx">i</span> <span class="nx">in</span> <span class="nx">range</span><span class="p">(</span><span class="kd">var</span><span class="p">.</span><span class="nx">public_ip_address_count</span><span class="p">)</span> <span class="err">:</span> <span class="p">{</span>
            <span class="nx">address</span>    <span class="p">=</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">this</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">ip_address</span>
            <span class="nx">resourceId</span> <span class="p">=</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">this</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">id</span>
          <span class="p">}</span>
        <span class="p">]</span>
        <span class="nx">trustedRanges</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">trusted_ranges</span>
        <span class="nx">vwanConfiguration</span> <span class="p">=</span> <span class="p">{</span>
          <span class="nx">networkVirtualApplianceId</span> <span class="p">=</span> <span class="nx">azurerm_palo_alto_virtual_network_appliance</span><span class="p">.</span><span class="nx">this</span><span class="p">.</span><span class="nx">id</span>
          <span class="nx">vHub</span> <span class="p">=</span> <span class="p">{</span>
            <span class="nx">resourceId</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">virtual_hub_id</span>
          <span class="p">}</span>
        <span class="p">}</span>
      <span class="p">}</span>
      <span class="nx">panoramaConfig</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">configString</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">cloud_managed_type</span> <span class="p">==</span> <span class="s2">"Panorama"</span> <span class="err">?</span> <span class="kd">var</span><span class="p">.</span><span class="nx">panorama_config_string</span> <span class="err">:</span> <span class="s2">""</span>
      <span class="p">}</span>
      <span class="nx">strataCloudManagerConfig</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">cloudManagerName</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">cloud_managed_type</span> <span class="p">==</span> <span class="s2">"Strata"</span> <span class="err">?</span> <span class="kd">var</span><span class="p">.</span><span class="nx">strata_cloud_manager_name</span> <span class="err">:</span> <span class="s2">""</span>
      <span class="p">}</span>
      <span class="nx">marketplaceDetails</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">offerId</span>     <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">marketplace_details</span><span class="p">.</span><span class="nx">offer_id</span>
        <span class="nx">publisherId</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">marketplace_details</span><span class="p">.</span><span class="nx">publisher_id</span>
      <span class="p">}</span>
      <span class="nx">planData</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">billingCycle</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">plan_data</span><span class="p">.</span><span class="nx">billing_cycle</span>
        <span class="nx">planId</span>       <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">plan_data</span><span class="p">.</span><span class="nx">plan_id</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="nx">depends_on</span> <span class="p">=</span> <span class="p">[</span><span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">this</span><span class="p">,</span> <span class="nx">azurerm_palo_alto_virtual_network_appliance</span><span class="p">.</span><span class="nx">this</span><span class="p">,</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">egress_nat</span><span class="p">,</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">this</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="variables">Variables</h2>

<p>These variables below complete the full IaC codebase for the Palo Alto Cloud NGFW deployment and I used them to ensure the module for the deployment was repeatable and as generic as possible.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"resource_group_name"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The name of the Resource Group where the Palo Alto Cloud NGFW Firewall will be deployed."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"appliance_name"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The name which should be used for this Palo Alto Local Network Virtual Appliance. Changing this forces a new Palo Alto Local Network Virtual Appliance to be created."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"virtual_hub_id"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The ID of the Virtual Hub to deploy this appliance onto. Changing this forces a new Palo Alto Local Network Virtual Appliance to be created."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"firewall_name"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The name of the Palo Alto Cloud NGFW Firewall."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"location"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The Azure region where the Palo Alto Cloud NGFW Firewall will be deployed."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"tags"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"A map of tags to assign to the Palo Alto Cloud NGFW Firewall."</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"network_type"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The type of network to deploy the Palo Alto Cloud NGFW Firewall onto. Allowed values: 'VWAN' or 'VNET'. Defaults to VWAN."</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"VWAN"</span>
  <span class="nx">validation</span> <span class="p">{</span>
    <span class="nx">condition</span>     <span class="p">=</span> <span class="nx">contains</span><span class="p">([</span><span class="s2">"VWAN"</span><span class="p">,</span> <span class="s2">"VNET"</span><span class="p">],</span> <span class="kd">var</span><span class="p">.</span><span class="nx">network_type</span><span class="p">)</span>
    <span class="nx">error_message</span> <span class="p">=</span> <span class="s2">"network_type must be either 'VWAN' or 'VNET'."</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"enable_egress_nat"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Enable Egress NAT for the Palo Alto Cloud NGFW Firewall. Allowed values: 'ENABLED' or 'DISABLED'. Defaults to 'ENABLED'."</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"ENABLED"</span>
  <span class="nx">validation</span> <span class="p">{</span>
    <span class="nx">condition</span>     <span class="p">=</span> <span class="nx">contains</span><span class="p">([</span><span class="s2">"ENABLED"</span><span class="p">,</span> <span class="s2">"DISABLED"</span><span class="p">],</span> <span class="kd">var</span><span class="p">.</span><span class="nx">enable_egress_nat</span><span class="p">)</span>
    <span class="nx">error_message</span> <span class="p">=</span> <span class="s2">"enable_egress_nat must be either 'ENABLED' or 'DISABLED'."</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"egress_nat_ip_address_count"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The number of Egress NAT IP addresses to allocate for the firewall."</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">1</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"public_ip_address_count"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The number of Public IP addresses to allocate for the firewall."</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">1</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"dns_settings"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="p">=</span> <span class="nx">object</span><span class="p">({</span>
    <span class="nx">dns_servers</span> <span class="p">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">object</span><span class="p">({</span>
      <span class="nx">address</span>    <span class="p">=</span> <span class="nx">string</span>
      <span class="nx">resourceId</span> <span class="p">=</span> <span class="nx">string</span>
    <span class="p">}))</span>
    <span class="nx">enabled_dns_type</span> <span class="p">=</span> <span class="nx">string</span>
    <span class="nx">enable_dns_proxy</span> <span class="p">=</span> <span class="nx">string</span>
  <span class="p">})</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"DNS settings for the Palo Alto Cloud NGFW Firewall."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"trusted_ranges"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"List of NON-RFC 1918 trusted ranges for the Palo Alto Cloud NGFW Firewall."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"cloud_managed_type"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Is this appliance managed by Panorama or Strata Cloud Manager? Allowed values: 'Panorama' or 'Strata'."</span>
  <span class="nx">validation</span> <span class="p">{</span>
    <span class="nx">condition</span>     <span class="p">=</span> <span class="nx">contains</span><span class="p">([</span><span class="s2">"Panorama"</span><span class="p">,</span> <span class="s2">"Strata"</span><span class="p">],</span> <span class="kd">var</span><span class="p">.</span><span class="nx">cloud_managed_type</span><span class="p">)</span>
    <span class="nx">error_message</span> <span class="p">=</span> <span class="s2">"cloud_managed_type must be either 'Panorama' or 'Strata'."</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"panorama_config_string"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Base64 encoded string representing Panorama parameters to be used by Firewall to connect to Panorama. This string is generated via azure plugin in Panorama"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">""</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"strata_cloud_manager_name"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The name of the Strata Cloud Manager which is intended to manage the policy for this firewall."</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">""</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"marketplace_details"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="p">=</span> <span class="nx">object</span><span class="p">({</span>
    <span class="nx">offer_id</span>     <span class="p">=</span> <span class="nx">string</span> <span class="c1"># e.g. "pan_swfw_cloud_ngfw"</span>
    <span class="nx">publisher_id</span> <span class="p">=</span> <span class="nx">string</span> <span class="c1"># e.g. "paloaltonetworks"</span>
  <span class="p">})</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Marketplace details for the Palo Alto Cloud NGFW Firewall."</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"plan_data"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="p">=</span> <span class="nx">object</span><span class="p">({</span>
    <span class="nx">billing_cycle</span> <span class="p">=</span> <span class="nx">string</span> <span class="c1"># e.g. "MONTHLY"</span>
    <span class="nx">plan_id</span>       <span class="p">=</span> <span class="nx">string</span> <span class="c1"># e.g. "panw-cloud-ngfw-payg"</span>
  <span class="p">})</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Plan data for the Palo Alto Cloud NGFW Firewall."</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I hope you enjoyed reading, looking forward to your thoughts below.</p>

<p>Cheers,
Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="palo alto" /><category term="cloud ngfw" /><category term="strata cloud manager" /><category term="terraform" /><category term="azapi" /><summary type="html"><![CDATA[Learn how to deploy Palo Alto Cloud Next-Generation Firewall with Strata Cloud Manager integration using Terraform.]]></summary></entry><entry><title type="html">Managing Azure Databricks Workspace IP Access Lists via CICD</title><link href="https://jloudon.com/cloud/Managing-Azure-Databricks-Workspace-IP-Access-Lists-via-CICD/" rel="alternate" type="text/html" title="Managing Azure Databricks Workspace IP Access Lists via CICD" /><published>2025-04-27T00:00:00+10:00</published><updated>2025-04-27T00:00:00+10:00</updated><id>https://jloudon.com/cloud/Managing%20Azure%20Databricks%20Workspace%20IP%20Access%20Lists%20via%20CICD</id><content type="html" xml:base="https://jloudon.com/cloud/Managing-Azure-Databricks-Workspace-IP-Access-Lists-via-CICD/"><![CDATA[<p>Hey folks in this blog post I’m going to cover how you can manage Azure Databricks (ADB) Workspace IP access lists via CICD and DevOps processes. Hopefully this blog may save you time and potential headaches as I feel that, based on what I’ve experienced in the past, the available documentation and tooling support for this particular area has been limited.</p>

<h1 id="intro-to-adb-ip-access-lists">Intro To ADB IP Access Lists</h1>

<p>Using ADB IP access lists allows us to control which networks can connect to our ADB account and workspaces - by default all connections from any IP address are allowed so most enterprises will want to further secure network access by configuring this feature either via CICD and DevOps processes or manually as a portal-driven change.</p>

<p>Currently ADB has two IP access list features:</p>

<ul>
  <li><strong>IP access lists for the account console (currently Public Preview)</strong> - IP access lists for the account console to allow users to connect to the account console UI and account-level REST APIs only through a set of approved IP addresses.</li>
  <li><strong>IP access lists for workspaces</strong> - IP access lists for Azure Databricks workspaces to allow users to connect to the workspace or workspace-level APIs only through a set of approved IP addresses.</li>
</ul>

<p>Access is checked according to this flow below. Image Source: <a href="https://learn.microsoft.com/en-us/azure/databricks/security/network/front-end/ip-access-list#details">Microsoft Docs</a></p>

<p><img src="/assets/images/databricks-ip-access-list-flow.png" alt="AzureDatabricksIPAccessListFLow" title="Azure Databricks IP access list flow" /></p>

<h1 id="problem-statement-and-rabbit-holes">Problem Statement and Rabbit Holes</h1>

<p>When working on a project to deploy and manage multiple ADB workspaces via code, I had already deployed the workspaces using Terraform AzureRM and I briefly explored the option of managing the workspace IP access configuration using the available <a href="https://registry.terraform.io/providers/databricks/databricks/latest/docs/resources/ip_access_list">Terraform Databricks IP Access List</a> resource. Ultimately I didn’t go down the path of using the Terraform Databricks provider for various reasons specific to that project but I am interested in getting hands-on with that provider in future if there was the right opportunity.</p>

<p>So I still needed to manage the ADB workspace IP access lists via CICD and the next best option was to leverage the latest <a href="https://learn.microsoft.com/en-us/azure/databricks/dev-tools/cli/commands">Databricks CLI</a> (currently Public Preview). This first led me down a bit of a rabbit hole of various Databricks CLI authentication options until I finally figured out that ‘magic’ set of environment variables to use for OAuth machine-to-machine (M2M) authetication to successfully work to the workspaces. If you’re interested in knowing more about Databricks CLI authentication, I have some high level details in my <a href="https://github.com/globalbao/azure-databricks-cicd?tab=readme-ov-file#databricks-workspace-authentication">repo’s README</a>.</p>

<p>The second rabbit hole was related to Databricks CLI itself. During this project I often found myself confused about what the actual supported cmdlets and inputs were for the CLI. I’ll try not to bore you too much with the details but in general I found the <a href="https://learn.microsoft.com/en-us/azure/databricks/dev-tools/cli/commands">Databricks CLI commands documentation</a> to be a bit incomplete in terms of available/supported commands that map to the REST APIs. This was further validated when I discovered various <code class="language-plaintext highlighter-rouge">//TODO</code> comments in the CLI’s .go files for the <code class="language-plaintext highlighter-rouge">workspace/ip-access-lists</code> command as shown <a href="https://github.com/databricks/cli/blob/ee55316007810446e975b2bd5173ab3daeaa3ff4/cmd/workspace/ip-access-lists/ip-access-lists.go#L86">here</a>. With Databricks saying the CLI is still in public preview I’m hoping these doco gaps are fleshed out and completed prior to it going GA.</p>

<h1 id="managing-azure-databricks-workspace-ip-access-lists-via-cicd">Managing Azure Databricks Workspace IP Access Lists via CICD</h1>

<p>So now let’s dig into how exactly I managed ADB IP access lists via CICD.</p>

<p>As part of my Azure DevOps build and release pipeline templates I needed to install the Databricks CLI to my Ubuntu agent pool. This was fairly easy following the <a href="https://learn.microsoft.com/en-us/azure/databricks/dev-tools/cli/install#curl-install">available doco</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sh
</code></pre></div></div>

<p>I then added a single step in my build and release pipeline template to:</p>

<ol>
  <li>echo the existing IP access lists to the logs (this is also how we can see the <code class="language-plaintext highlighter-rouge">ip_access_list_id</code> which is needed for <code class="language-plaintext highlighter-rouge">update</code> and <code class="language-plaintext highlighter-rouge">delete</code> operations)</li>
  <li>enable or disable the IP access lists based on a <a href="https://github.com/globalbao/azure-databricks-cicd/blob/main/devops/templates/ado-release-template.yml#L12">parameter input</a> from the calling pipeline</li>
  <li>echo the IP access list enablement status to the logs</li>
</ol>

<p>The below cmdlets gave me an easy way to toggle enablement or disablement of the workspace IP access lists via the Azure DevOps release pipeline.</p>

<script src="https://gist.github.com/jesseloudon/2283a226dfe3ca08d5c5268044b678f6.js"></script>

<p>Now onto the fun bit – standing up the capability to trigger create, update, and delete operations within the IP access list.</p>

<p>Noticing that the Databricks CLI currently only supports JSON inputs for IP access list changes I chose to store the workspace IP ACLs in dedicated .JSON files per environment within a single folder in my repo e.g. <code class="language-plaintext highlighter-rouge">./workspace-ip-access-lists</code> with each JSON object aligning to the below inputs as <a href="https://github.com/globalbao/azure-databricks-cicd/blob/main/workspace-ip-access-lists/dev.json">shown here</a>.</p>

<table>
  <thead>
    <tr>
      <th>Input name</th>
      <th>Type</th>
      <th>required</th>
      <th>example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>label</td>
      <td>string</td>
      <td>yes for <code class="language-plaintext highlighter-rouge">Create</code> and <code class="language-plaintext highlighter-rouge">Update</code> operations</td>
      <td><code class="language-plaintext highlighter-rouge">"ALLOW_AZURE_DATABRICKS_PRODFIX_SUBNETS"</code></td>
    </tr>
    <tr>
      <td>list_type</td>
      <td>string</td>
      <td>yes for <code class="language-plaintext highlighter-rouge">Create</code> and <code class="language-plaintext highlighter-rouge">Update</code> operations</td>
      <td><code class="language-plaintext highlighter-rouge">"ALLOW"</code> or <code class="language-plaintext highlighter-rouge">"BLOCK"</code></td>
    </tr>
    <tr>
      <td>ip_addresses</td>
      <td>array</td>
      <td>yes for <code class="language-plaintext highlighter-rouge">Create</code> and <code class="language-plaintext highlighter-rouge">Update</code> operations</td>
      <td><code class="language-plaintext highlighter-rouge">["10.0.0.0/25","10.0.100.0/25"]</code></td>
    </tr>
    <tr>
      <td>ip_access_list_id</td>
      <td>string</td>
      <td>yes for <code class="language-plaintext highlighter-rouge">Update</code> and <code class="language-plaintext highlighter-rouge">Delete</code> operations</td>
      <td><code class="language-plaintext highlighter-rouge">"a559572d-1730-4ce4-203z-75506242f04h"</code></td>
    </tr>
    <tr>
      <td>operation</td>
      <td>string</td>
      <td>yes always</td>
      <td><code class="language-plaintext highlighter-rouge">"CREATE"</code> or <code class="language-plaintext highlighter-rouge">"UPDATE"</code> or <code class="language-plaintext highlighter-rouge">"DELETE"</code></td>
    </tr>
  </tbody>
</table>

<p>Now that I had the IP ACLs defined in .JSON files within the repo I then added a single step in my release pipeline template which:</p>

<ol>
  <li>iterates over JSON objects: The <code class="language-plaintext highlighter-rouge">jq -c '.[]' "$json_file"</code> command extracts each object from the JSON file ($json_file) as a compact, single-line JSON string. The while loop reads each of these JSON objects one by one into the variable <code class="language-plaintext highlighter-rouge">json_object_creation</code></li>
  <li>extracts the operation field: For each JSON object, the script uses <code class="language-plaintext highlighter-rouge">jq -r '.operation'</code> to extract the value of the operation field. This value is stored in the operation variable. The -r flag ensures that the extracted value is output as a raw string, without quotes</li>
  <li>checks for “Create” operations: The script converts the operation value to lowercase using <code class="language-plaintext highlighter-rouge">${operation,,}</code> and checks if it starts with the word <code class="language-plaintext highlighter-rouge">"create"</code>. This is done using the <code class="language-plaintext highlighter-rouge">if [[ ${operation,,} == "create"* ]]; then</code> condition. If the condition is true, the script proceeds to execute the block of code inside the if statement</li>
  <li>creates Databricks IP Access Lists: If the operation is a <code class="language-plaintext highlighter-rouge">"create"</code> operation, the script logs a message to the console indicating that it is creating a new Databricks IP Access List. It then invokes the databricks ip-access-lists create command, passing the JSON object <code class="language-plaintext highlighter-rouge">($json_object_creation)</code> as input using the –json flag. The command also</li>
  <li>uses two parameters, BUNDLE_TARGET and DATABRICKS_LOG_LEVEL, which are dynamically substituted from the pipeline’s parameters (<code class="language-plaintext highlighter-rouge">$</code> and <code class="language-plaintext highlighter-rouge">$</code>)</li>
  <li>uses <code class="language-plaintext highlighter-rouge">|| true</code> at the end of the databricks command ensuring that the script does not fail if the command encounters an error</li>
</ol>

<p>This is the full pipeline step to create a new ADB IP access list:</p>

<script src="https://gist.github.com/jesseloudon/a5f0bd2688a4cfb701233835d54613ad.js"></script>

<p>An example of a valid JSON object block which would be read and used by the above pipeline step is below.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ALLOW_EXAMPLE_CORP_NETWORK1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"list_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ALLOW"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ip_addresses"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"192.168.0.0/23"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CREATE"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>To support the <code class="language-plaintext highlighter-rouge">'update'</code> operation I repeated the above pipeline step logic as above, but added additional input and logic to support extracting and passing in the <code class="language-plaintext highlighter-rouge">ip_access_list_id</code> which is required.</p>

<script src="https://gist.github.com/jesseloudon/6d92309ca9c48f62f68d043aef2e107a.js"></script>

<p>An example of a valid JSON object block which would be read and used by the above pipeline step is below. Note the <code class="language-plaintext highlighter-rouge">ip_access_list_id</code> value and <code class="language-plaintext highlighter-rouge">operation</code> value.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ALLOW_EXAMPLE_CORP_NETWORK1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"list_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ALLOW"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ip_addresses"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"192.168.0.0/23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"192.168.100.0/23"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"ip_access_list_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a559572d-1730-4ce4-203z-75506242f04h"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"UPDATE"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">'delete'</code> operation shown below follows a similar implementation logic as the ‘update’ operation to handle <code class="language-plaintext highlighter-rouge">ip_access_list_id</code> which is required.</p>

<script src="https://gist.github.com/jesseloudon/ca04e504ce69dfc98c377e5ab83c3a4d.js"></script>

<p>An example of a valid JSON object block which would be read and used by the above pipeline step is below. Note the <code class="language-plaintext highlighter-rouge">ip_access_list_id</code> value and <code class="language-plaintext highlighter-rouge">operation</code> value.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"label"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ALLOW_EXAMPLE_CORP_NETWORK1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"list_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ALLOW"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ip_addresses"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"192.168.0.0/23"</span><span class="p">,</span><span class="w"> </span><span class="s2">"192.168.100.0/23"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"ip_access_list_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a559572d-1730-4ce4-203z-75506242f04h"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DELETE"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>Now that I had the basic foundations to manage this all from a pipeline and repo, a typical DevOps flow to operationally consume the above capability would be to:</p>

<ol>
  <li>create a new branch of the <a href="https://github.com/globalbao/azure-databricks-cicd">github.com/globalbao/azure-databricks-cicd</a> repo</li>
  <li>update an existing .JSON file within <code class="language-plaintext highlighter-rouge">./workspace-ip-access-lists</code> with the desired ADB IP access list operation (create, update, delete)</li>
  <li>optionally, update the <code class="language-plaintext highlighter-rouge">'ADB_ENABLE_IP_ACCESS_LISTS'</code> parameter value to <code class="language-plaintext highlighter-rouge">true</code> or <code class="language-plaintext highlighter-rouge">false</code> from the <a href="https://github.com/globalbao/azure-databricks-cicd/blob/main/devops/ado-databricks-cicd.yml#L59">calling pipeline</a></li>
  <li>pull request and merge the new branch to main</li>
  <li>approve the latest pipeline run from main to deploy the IP access list changes</li>
</ol>

<p><img src="/assets/images/databricks-devops-pipeline.png" alt="AzureDatabricksDevOpsPipeline" title="Azure Databricks DevOps Pipeline" /></p>

<h1 id="final-thoughts">Final Thoughts</h1>

<p>Some improvements I can think of if I were to get a chance at this again:</p>

<ol>
  <li>add JSON input validation as part of the build pipeline step to catch typos and errors early on before a PR/merge to main</li>
  <li>add capability to manage ADB Account IP Access Lists as it appears to be supported through Databricks CLI</li>
  <li>add logic for finding and mapping the <code class="language-plaintext highlighter-rouge">ip_access_list_id</code> from created IP access lists to any ‘update’ and ‘delete’ operations so you don’t need to manually input that value into the .JSON files.</li>
  <li>experiment using the Terraform Databricks provider as an alternative to Databricks CLI (assuming both Workspace and Account IP Access Lists are supported)</li>
</ol>

<p>The past project, and this related blog post, were both fun and engaging pieces for me personally. I felt that I’ve been able to leverage past skills and experience in the DevOps space to prototype something fairly quickly and navigate through several rabbit holes that appeared.</p>

<p>I hope you enjoyed reading, looking forward to your thoughts below.</p>

<p>Cheers,
Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="azure databricks" /><category term="workspace-ip-access" /><category term="databricks devops" /><summary type="html"><![CDATA[How you can manage Azure Databricks (ADB) Workspace IP access lists via CICD and DevOps processes]]></summary></entry><entry><title type="html">Flexing your Security Governance with Azure Policy as Code</title><link href="https://jloudon.com/cloud/Flexing-your-Security-Governance-with-Azure-Policy-as-Code/" rel="alternate" type="text/html" title="Flexing your Security Governance with Azure Policy as Code" /><published>2022-02-02T00:00:00+11:00</published><updated>2022-02-02T00:00:00+11:00</updated><id>https://jloudon.com/cloud/Flexing%20your%20Security%20Governance%20with%20Azure%20Policy%20as%20Code</id><content type="html" xml:base="https://jloudon.com/cloud/Flexing-your-Security-Governance-with-Azure-Policy-as-Code/"><![CDATA[<p><img src="/assets/images/MR2022-FlexingYourSecurityGovernanceWithAzurePolicyAsCode-og.png" alt="MicrosoftReactor2022" title="Flexing Your Security Governance with Azure Policy as Code" /></p>

<p>G’day folks. I recently had the pleasure of presenting a livestream session via Microsoft Reactor Sydney on a subject close to my heart.</p>

<p>It was a great opportunity to share my experience in the subject domain of Security Governance and Azure Policy as Code workflows using Bicep language. And also a good opportunity to receive some questions and feedback in real time from an audience.</p>

<p>I’ve included some links below if you’re interested to dive into this subject a bit more.</p>

<h2 id="watch">Watch</h2>

<p>Watch the recorded session from Microsoft Reactor’s livestream event.</p>
<ul>
  <li><a href="https://youtu.be/SuH_TBBsvLI">YouTube: Flexing Your Security Governance with Azure Policy As Code</a></li>
</ul>

<p>Catchup on the latest (Dec 2021) Azure Governance &amp; Deployments news from Microsoft.</p>
<ul>
  <li><a href="https://youtu.be/oYC5Ns7kLCY">YouTube: Azure Governance &amp; Deployments Quarterly Customer Panel December 2021</a></li>
</ul>

<p>Watch a session on managing the logging and security of Azure Key Vaults at-scale with Azure Policy and Microsoft Sentinel.</p>
<ul>
  <li><a href="https://youtu.be/B03V3Tazcec">YouTube: Azure Sentinel and Policy as Code from Jesse Loudon and Casey Mullineaux</a></li>
</ul>

<p>Watch a session on deploying Azure Policy as Code with Bicep walking through levels 1-2-3 of configuration.</p>
<ul>
  <li><a href="https://youtu.be/qpnMJXw6pIg">YouTube: Policy as Code with Bicep for Enterprise Scale</a></li>
</ul>

<h2 id="learn--discover">Learn &amp; Discover</h2>

<p>Learn about what exactly is Azure Policy from Microsoft Docs.</p>
<ul>
  <li><a href="https://docs.microsoft.com/en-us/azure/governance/policy/overview?WT.mc_id=AZ-MVP-5004598">Microsoft Docs: What is Azure Policy?</a></li>
</ul>

<p>Learn about how Azure Policy effects work from Microsoft Docs.</p>
<ul>
  <li><a href="https://docs.microsoft.com/en-us/azure/governance/policy/concepts/effects?WT.mc_id=AZ-MVP-5004598">Understand Azure Policy effects</a></li>
</ul>

<p>Learn about scoping your Azure Policy assignments from Microsoft Docs.</p>
<ul>
  <li><a href="https://docs.microsoft.com/en-us/azure/governance/policy/concepts/scope?WT.mc_id=AZ-MVP-5004598">Understand scope in Azure Policy</a></li>
</ul>

<p>Learn about the Azure Security Benchmark from Microsoft Docs.</p>
<ul>
  <li><a href="https://docs.microsoft.com/en-us/security/benchmark/azure/introduction?WT.mc_id=AZ-MVP-5004598">Microsoft Docs: Azure Security Benchmark introduction</a></li>
</ul>

<p>Learn about Microsoft Security Best Practices from Microsoft Docs.</p>
<ul>
  <li><a href="https://docs.microsoft.com/en-us/security/compass/microsoft-security-compass-introduction?WT.mc_id=AZ-MVP-5004598">Microsoft Docs: What’s inside Microsoft Security Best Practices?</a></li>
</ul>

<p>Learn about what is Microsoft Defender for Cloud from Microsoft Docs..</p>
<ul>
  <li><a href="https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-cloud-introduction?WT.mc_id=AZ-MVP-5004598">Microsoft Docs: What is Microsoft Defender for Cloud?</a></li>
</ul>

<p>Discover how to apply and enforce Zero Trust concepts with Azure Policy from Microsoft Blogs.</p>
<ul>
  <li><a href="https://devblogs.microsoft.com/azuregov/enforcing-policy-for-zero-trust-with-azure-policy-4-of-6/?WT.mc_id=AZ-MVP-5004598">Microsoft Blogs: Enforcing Policy for Zero Trust with Azure Policy</a></li>
</ul>

<p>Discover how to enable Defender for Cloud plans to protect workflows in Azure from the community.</p>
<ul>
  <li><a href="https://samilamppu.com/2021/12/28/automatically-enable-microsoft-defender-for-cloud-enhanced-security-features/">Community Blog: Automatically Enable and Audit Microsoft Defender for Cloud Enhanced Security Features</a></li>
</ul>

<p>Discover how to create custom Microsoft Defender for Cloud recommendations with Azure Policy from the community.</p>
<ul>
  <li><a href="https://zimmergren.net/create-custom-security-center-recommendation-with-azure-policy/">Community Blog: Create a custom Azure Security Center recommendation with Azure Policy</a></li>
</ul>

<p>Discover how to create custom security recommendations within Microsoft Defender for Cloud from the community.</p>
<ul>
  <li><a href="https://msftplayground.com/2022/02/custom-security-recommendation-within-microsoft-defender-for-cloud/">Community Blog: Custom security recommendation within Microsoft Defender for Cloud</a></li>
</ul>

<p>Discover Security Posture Management with Azure Policy and Microsoft Defender for Cloud from the community.</p>
<ul>
  <li><a href="https://securecloud.blog/2021/12/17/security-posture-management-with-azure-policy-and-microsoft-defender-for-cloud/">Community Blog: Security Posture Management with Azure Policy and Microsoft Defender for Cloud</a></li>
</ul>

<h2 id="find">Find</h2>

<p>Find samples of built-in definitions for Azure Policy from Microsoft GitHub.</p>
<ul>
  <li><a href="https://github.com/azure/azure-policy">GitHub: Repository for Azure Policy built-in definitions and samples</a></li>
</ul>

<p>Find all the best Azure Policy official and community content available across the internet.</p>
<ul>
  <li><a href="https://github.com/globalbao/awesome-azure-policy">GitHub: A curated list of AWESOME Azure Policy learning resources and links</a></li>
</ul>

<p>Find and adopt working modules and CI/CD pipelines for Azure Policy as Code workflows.</p>
<ul>
  <li><a href="https://github.com/globalbao/azure-policy-as-code">GitHub: Bicep and Terraform code examples for policy-as-code workflows</a></li>
</ul>

<p>Find the related (this blog post) code used for the Bicep demo on Security Governance with Azure Policy as Code.</p>
<ul>
  <li><a href="https://github.com/globalbao/azure-policy-as-code/tree/main/Bicep/demos/security-governance">GitHub: Bicep Demo Security Governance</a></li>
</ul>

<p>Find a list of past security mistakes by CSPs.</p>
<ul>
  <li><a href="https://github.com/SummitRoute/csp_security_mistakes">GitHub: List of security mistakes by cloud service providers (AWS, GCP, and Azure)</a></li>
</ul>

<h2 id="keep-informed">Keep Informed</h2>

<p>Keep up to date with the latest changes and releases for Azure Governance capabilties (Policies, Aliases, RBAC roles, etc).</p>
<ul>
  <li><a href="https://www.azadvertizer.net/">Site: AzAdvertizer - Release and change tracking on Azure Governance capabilities</a></li>
</ul>

<p>Cheers,</p>

<p>Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="bicep" /><category term="security governance" /><category term="azure policy" /><category term="policy as code" /><category term="microsoft reactor sydney" /><summary type="html"><![CDATA[I recently had the pleasure of presenting a livestream session via Microsoft Reactor Sydney on a subject close to my heart.]]></summary></entry><entry><title type="html">Awesome Azure Policy Origin Story</title><link href="https://jloudon.com/cloud/Awesome-Azure-Policy-Origin-Story/" rel="alternate" type="text/html" title="Awesome Azure Policy Origin Story" /><published>2022-01-21T00:00:00+11:00</published><updated>2022-01-21T00:00:00+11:00</updated><id>https://jloudon.com/cloud/Awesome%20Azure%20Policy%20Origin%20Story</id><content type="html" xml:base="https://jloudon.com/cloud/Awesome-Azure-Policy-Origin-Story/"><![CDATA[<p>Welcome folks this is the Awesome Azure Policy origin story.</p>

<p>My first experience with Azure Policy was mid-2020 engaged as a consultant for an enterprise client in the retail industry. They had been steadily growing their Microsoft Azure footprint since before the start of the COVID pandemic. Then there appeared to be somewhat of an explosion in projects to onboard and migrate applications and infrastructure into their Azure tenancies. Due to this explosion of growth there also happened to be what I would call ‘governance drift’. Examples of which included:</p>

<ul>
  <li>Tags - Missing completely, incorrect or outdated keys/values, no inheritence of tags from resource group to resources.</li>
  <li>Data Protection - Gaps in compliance for virtual machine backups and disk encryption.</li>
  <li>Monitoring - No baseline logging and alerting for infrastructure.</li>
  <li>Security - RBAC drift on resource groups, resource locks missing from some critical infrastructure such as ExpressRoute.</li>
</ul>

<p>At the same I was learning HashiCorp Terraform for the first time in my life and being a complete novice (I still am!) I’m grateful for the help I received from Michael O’Leary (my colleague on the same project) and the Terraform community through their blogs and tutorials.</p>

<p>In the first week of January 2022 I was bouncing about the #TechTwitter space and was well down the rabbit hole with Open Policy Agent (OPA). My interest in OPA is largely thanks to having reviewed a couple chapters covering possible use cases and solutions with OPA. Courtesy of my role as a technical reviewer for PackT publishing which began in August 2021.</p>

<p>Long story short, if you’re also interested in OPA you may have come across <a href="https://github.com/anderseknert/awesome-opa">github.com/anderseknert/awesome-opa</a> by Anders Eknert. It’s a curated list of OPA related tools, frameworks, and articles. So as a newcomer to OPA I found this public list of resources helpful and having followed Anders on Twitter I have come to trust that he is a good source of information on the subject matter.</p>

<p>There was inspiration from that list and it was a great starting point for what I had in mind for an Azure Policy list.</p>

<p>You may be wondering how to go about creating a list of something where no list exists? For me, because I’ve been actively engaged in the Microsoft Azure community for a few years, through various social media channels and I’ve come to know of people who are leaders in their field and publicly sharing their knowledge with the community.</p>

<p>Regardless of whether you are searching for docs, code repos, tools, blogs, or videos, I have a feeling that 80% of content out there has been created with the blood sweat and tears from real people. Yes there are exceptions, but for the majority of stuff out there people are the ones with the ideas and experiences and they are what drive community engagement and growth. Find your people and you’ll find the community and the content you need to build your own list.</p>

<p>So here’s where you should start:</p>

<ol>
  <li>People - identify leaders in the community, stalk them (in a nice way), follow their announcements and content.</li>
  <li>Blogs - Search articles of content and look for links/references to other articles, tools, blogs, authors, etc.</li>
  <li>Twitter - use keyword/hashtag searching for public content.</li>
  <li>GitHub - use keyword searching for public repositories.</li>
  <li>YouTube - use keyword searching for published videos.</li>
  <li>Events - use keyword searching of annual events run by the community for public content.</li>
  <li>Browser - maybe you have bookmarked some gems? :).</li>
</ol>

<p>Guiding principles for organsing the README:</p>

<ul>
  <li>List items sorted in alphabetical order</li>
  <li>Links cannot be behind a paywall / must be freely accessible to everyone</li>
  <li>Sentence case for Blogs, Videos, Docs, etc</li>
  <li>Lower case for GitHub repositories</li>
  <li>No duplicates of items in lists</li>
  <li>Official links placed at the top</li>
</ul>

<p>That’s all I have for now. I hope you found this article interesting and I hope you find the <a href="https://github.com/globalbao/awesome-azure-policy">Awesome Azure Policy</a> list helpful. If you have any feedback or suggestions please feel free to leave comments below. Thanks for reading!</p>

<p>Cheers
Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="azure policy" /><category term="community" /><category term="awesome azure policy" /><category term="policy examples" /><summary type="html"><![CDATA[This is the Awesome Azure Policy origin story]]></summary></entry><entry><title type="html">Talking Azure Policy as Code on CtrlAltAzure podcast</title><link href="https://jloudon.com/cloud/Talking-Azure-Policy-as-Code-on-the-CtrlAltAzure-podcast/" rel="alternate" type="text/html" title="Talking Azure Policy as Code on CtrlAltAzure podcast" /><published>2021-11-25T00:00:00+11:00</published><updated>2021-11-25T00:00:00+11:00</updated><id>https://jloudon.com/cloud/Talking%20Azure%20Policy%20as%20Code%20on%20the%20CtrlAltAzure%20podcast</id><content type="html" xml:base="https://jloudon.com/cloud/Talking-Azure-Policy-as-Code-on-the-CtrlAltAzure-podcast/"><![CDATA[<p>Hey folks! I recently featured as a guest on the <strong>Ctrl+Alt+Azure</strong> podcast for episode 109 to talk <strong>Azure Policy as Code</strong> with hosts <strong>Tobias Zimmergren</strong> and <strong>Jussi Roine</strong>.</p>

<blockquote>
  <p>The Ctrl+Alt+Azure podcast was launched back in October 2019 by Microsoft Azure MVPs <a href="https://twitter.com/zimmergren">Tobias Zimmergren</a> and <a href="https://twitter.com/jussiroine">Jussi Roine</a>.</p>
</blockquote>

<p>Tobias had reached out to me over twitter to check my availability and interest in contributing to an upcoming episode dedicated to Azure Policy as Code.</p>

<p>Admittedly I was surprised by Tobias’s message, and other community events were already on my calendar, but the fact of the matter is that having actively advocated on this exact subject matter over the past 12+ months <strong>I would not be passing up such a wonderful opportunity!</strong></p>

<p>:studio_microphone: <strong>You can tune into the episode at <a href="https://ctrlaltazure.com/episodes/109-azure-policy-as-code">https://ctrlaltazure.com/episodes/109-azure-policy-as-code</a>.</strong> There you’ll also find helpful links to various tools and resources mentioned during our talk.</p>

<blockquote>
  <p>Starting from ground zero with Azure Policy? Check out episode 25 of the Ctrl+Alt+Azure podcast <a href="https://ctrlaltazure.com/episodes/025-azure-policies-the-how-the-what-and-the-why">Azure Policies - the how, the what, and the why?</a></p>
</blockquote>

<h1 id="azure-policy-as-code---episode-109---session-abstract">Azure Policy as Code - Episode 109 - Session Abstract</h1>

<p>:bulb: Don’t have 40 mins to listen to the entire episode but want to know what’s discussed? <strong>I’ve got you covered!</strong></p>

<p>Here’s <strong>13</strong> points of <strong>Azure Policy as Code goodness</strong> covered during episode 109! :muscle:</p>

<ol>
  <li>Brief introduction to <a href="https://docs.microsoft.com/en-us/azure/governance/policy/overview?WT.mc_id=AZ-MVP-5004598">Azure Policy</a> and <a href="https://docs.microsoft.com/en-us/azure/governance/policy/concepts/policy-as-code?WT.mc_id=AZ-MVP-5004598">Policy as Code</a> using <a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?WT.mc_id=AZ-MVP-5004598">Bicep</a> language - Microsoft’s Domain Specific Language for transpiling into ARM Templates.</li>
  <li>At what point might you decide to move from managing your Azure Policies via the Azure Portal (click-ops) to a Policy as Code workflow based on Infrastructure as Code and DevOps methodologies?</li>
  <li>Using Bicep’s <a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions-files#loadtextcontent?WT.mc_id=AZ-MVP-5004598">loadtextcontext</a> function to quickly onboard new policies to your Bicep modules.</li>
  <li>Is there a difference in capability for developers when managing your Azure Policies from the Azure Portal versus managing them as code via say Bicep/ARM templates?</li>
  <li>What are some key concepts to know about when working with Azure Policy as Code workflows?</li>
  <li>Designing your Service Principal <a href="https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles?WT.mc_id=AZ-MVP-5004598">RBAC roles</a> adhering to the principle of least privilege.</li>
  <li>How to approach testing methdologies for new custom Azure Policies before deploying them to prod.</li>
  <li>Programmatically triggering an Azure Policy <a href="https://docs.microsoft.com/en-us/azure/governance/policy/how-to/get-compliance-data#on-demand-evaluation-scan">on-demand compliance scan</a> at your desired scope.</li>
  <li>Recommended tools for getting started with Azure Policy as Code workflows using Bicep language.</li>
  <li>Available options for <a href="https://docs.microsoft.com/en-us/azure/governance/policy/how-to/export-resources?WT.mc_id=AZ-MVP-5004598">migrating</a> your Azure Policies from the Azure Portal to a code-driven workflow.</li>
  <li>Free community tools which can assist with your Azure Policy as Code journey.</li>
  <li>Infrastructure as Code tooling comparison.</li>
  <li>A surprise question to Jesse.</li>
</ol>

<blockquote>
  <p>:studio_microphone: <strong>Now that I’ve piqued your curiousity :smile: tune into the episode at <a href="https://ctrlaltazure.com/episodes/109-azure-policy-as-code">https://ctrlaltazure.com/episodes/109-azure-policy-as-code</a>.</strong></p>
</blockquote>

<h1 id="the-ctrlaltazure-podcast-journey">The Ctrl+Alt+Azure podcast journey</h1>

<p>My own research of the Ctrl+Alt+Azure podcast’s early beginnings and it’s <strong>journey from episode 1 to episode 52</strong> (late last year) has yielded many insightful blog posts from both Tobias and Jussi.</p>

<p>I highly recommend checking them out! For example I found a couple <strong>beautiful gems in their documented journey</strong> some of which include:</p>

<ul>
  <li>Choosing a podcast name. <em>(one of my favourites!)</em></li>
  <li>Finding time to record an episode.</li>
  <li>Staying flexible with scheduling.</li>
  <li>Leveraging peer pressure to collaborate.</li>
  <li>Finding their audience via distribution.</li>
  <li>Technology and tooling choices.</li>
</ul>

<p><strong>Launch annoucements</strong></p>
<ul>
  <li><a href="https://zimmergren.net/announcing-a-new-podcast-ctrl-alt-azure/">Announcing a new podcast Ctrl Alt Azure</a> - Tobias Zimmergren</li>
  <li><a href="https://jussiroine.com/2019/10/announcing-our-new-podcast-ctrlaltazure-the-first-episode-is-out-now/">Announcing our new podcast Ctrl Alt Azure the first episode is out now</a> - Jussi Roine</li>
</ul>

<p><strong>Getting started blog posts</strong></p>
<ul>
  <li><a href="https://zimmergren.net/how-we-planned-and-launched-a-podcast-the-technical-story/">How we planned and launched a podcast the technical story</a> - Tobias Zimmergren</li>
  <li><a href="https://jussiroine.com/2019/12/getting-started-with-a-podcast-the-equipment-setup-and-logistics/">Getting started with a podcast the equipment setup and logistics</a> - Jussi Roine</li>
</ul>

<p><strong>Podcast anniversaries</strong></p>
<ul>
  <li><a href="[https://jussiroine.com/2020/11/the-first-anniversary-of-the-ctrlaltazure-podcast/">The first anniversary of the Ctrl Alt Azure podcast</a> - Jussi Roine</li>
</ul>

<h1 id="conclusion">Conclusion</h1>

<p>It was an amazing opportunity to talk <strong>Azure Policy as Code</strong> with <strong>Tobias Zimmergren</strong> and <strong>Jussi Roine</strong>. I personally learned much from the discussions. Also very grateful for the wide scope of questions from Tobias and Jussi related to the topic and hope we were inclusive towards various perspectives, scenarios, and skillsets!</p>

<p>As to my own experience with contributing to a podcast episode let’s just say I now have a great appreciation for the amount of effort required to <strong>plan, setup, and record</strong> just one of these episodes! :smile:</p>

<p>Thanks for tuning in folks. I hope you found the episode interesting, informative, and helpful; as always I welcome your feedback and comments.</p>

<p>Cheers,</p>

<p>Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="azure policy" /><category term="policy as code" /><category term="azure governance" /><category term="bicep" /><category term="iac" /><category term="devops" /><category term="ctrlaltazure podcast" /><summary type="html"><![CDATA[Appearing as a guest on the Ctrl+Alt+Azure podcast to talk Azure Policy as Code with hosts Tobias Zimmergren and Jussi Roine]]></summary></entry><entry><title type="html">How to Win vs Azure Policy Non-Compliance</title><link href="https://jloudon.com/cloud/How-To-Win-vs-Azure-Policy-Non-Compliance/" rel="alternate" type="text/html" title="How to Win vs Azure Policy Non-Compliance" /><published>2021-07-25T00:00:00+10:00</published><updated>2021-07-25T00:00:00+10:00</updated><id>https://jloudon.com/cloud/How%20To%20Win%20vs%20Azure%20Policy%20Non%20Compliance</id><content type="html" xml:base="https://jloudon.com/cloud/How-To-Win-vs-Azure-Policy-Non-Compliance/"><![CDATA[<p>Hey folks in this blog post I’m going to share with you how to win the battle versus Azure Policy non-compliance.</p>

<p>I had a scope requirement for a recent customer engagement to implement diagnostic settings for several resource types - one of which was Azure Kubernetes Service (AKS) clusters.</p>

<p>These diagnostic settings needed to be customised per the design document and ultimately logs were to be forwarded to a log analytics workspace – perfect fit for leveraging policy-as-code and deployIfNotExists policies!</p>

<p>The following builtin policy seemed to fit my requirements above perfectly so I set about testing it in my development subscription.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Deploy - Configure diagnostic settings for Azure Kubernetes Service to Log Analytics workspace"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"policyType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BuiltIn"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Indexed"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Deploys the diagnostic settings for Azure Kubernetes Service to stream resource logs to a Log Analytics workspace."</span><span class="p">,</span><span class="w">
    </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Kubernetes"</span><span class="w">
    </span><span class="p">},</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>When testing deployIfNotExists policies you should verify (1) the Azure Resource Manager (ARM) template deployment for your non-compliant resources was successful, and (2) the resource is marked as compliant post-remediation task.</p>
</blockquote>

<p>In case you’re not familiar with deployIfNotExists policies this snippet gives a high-level overview of the JSON:</p>

<p><img src="/assets/images/azspringclean-dine-blog-image3.png" alt="AzurePolicyDeployIfNotExists" title="DeployIfNotExists policy" /></p>

<p>So here’s a simplistic image illustrating my flow when troubleshooting the root cause of this non-compliant policy. I’ll cover each highlighted point in more detail soon.</p>

<p><img src="/assets/images/policy-noncompliance0.svg" alt="AzurePolicyNonCompliance" title="How to Win vs Azure Policy Non-Compliance" /></p>

<h1 id="fighting-the-non-compliance-enemy">Fighting The ‘Non-Compliance’ Enemy</h1>

<p><code class="language-plaintext highlighter-rouge">Screenshot #1</code> (or <code class="language-plaintext highlighter-rouge">SS #1</code>) shows a non-compliant AKS cluster. This evaluation result is AFTER a remediation task had successfully configured the diagnostic settings per my requirements on the resource. Initially I was puzzled to see this non-compliant result but it became clear why this was was happening as I investigated the policy’s existenceCondition.</p>

<p><code class="language-plaintext highlighter-rouge">SS #2</code> shows the reason for non-compliance is because <strong>target value</strong> and <strong>current value</strong> for the <strong>evaluated field</strong> is <strong>not matching</strong>. The path for the evaluated field is also an array “properties.logs[*].enabled” which basically means there’s more than one element to evaluate.</p>

<blockquote>
  <p>I was able to view the reason for non-compliance by clicking into the <em>Details</em> link under the Compliance reason column – a crucial piece of evidence for troubleshooting – in the future I hope we’ll be able to query this exact data programmatically.</p>
</blockquote>

<p><code class="language-plaintext highlighter-rouge">SS #3</code> shows my verification of the successful remediation task on the resource. This confirms the policy’s nested Azure Resource Manager (ARM) template deployment was a SUCCESS.</p>

<p><code class="language-plaintext highlighter-rouge">SS #4</code> shows our root cause issue with one of the existenceCondition blocks where the alias “Microsoft.Insights/diagnosticSettings/logs.enabled” needs to equal to “True” for the resource to be marked as compliant.</p>

<blockquote>
  <p>Again, based on the fact that this alias maps to an array (as seen in <code class="language-plaintext highlighter-rouge">SS #2</code>) this condition is basically saying that every element in the array needs equal to “True” for the resource to be marked as compliant after an evaluation scan. A bit of an ‘opps’ moment here :smile:</p>
</blockquote>

<h1 id="winning-the-battle">Winning The Battle</h1>

<p><code class="language-plaintext highlighter-rouge">SS #5</code> and <code class="language-plaintext highlighter-rouge">SS #6</code> show how I resolved this issue by:</p>

<ol>
  <li>changing the condition from “<strong>equals</strong>” to “<strong>in</strong>”</li>
  <li>referencing each AKS log parameter name in the array aka “<strong>[parameters(‘kube-apiserver’)]</strong>” etc</li>
</ol>

<blockquote>
  <p>These parameter names also need to be in the right order. And as the original policy was a builtin type, I duplicated the JSON into a custom policy and modified the existenceCondition as shown in <code class="language-plaintext highlighter-rouge">SS #5</code>.</p>
</blockquote>

<p>After checking numerous builtin policies for configuring diagnostic settings I can confirm Microsoft have paramaterised the individual logs/metrics so you can specify during your policy assignment which logs/metrics you want to configure (by default they are all set to “True”).</p>

<p>This is great, as it allows developers/admins to be flexible with the policy’s settings without having to change/duplicate the policy definition JSON to get a desired result. <strong>However I believe most of these builtin policies have the same design flaw with the existenceCondition as outlined in this blog post.</strong></p>

<h1 id="battle-report">Battle Report</h1>

<p>I found that the builtin policy’s existenceCondition shown below only 100% works if the logs/metric parameter default values do not change e.g. from “True” to “False”.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"existenceCondition"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"allOf"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Insights/diagnosticSettings/logs.enabled"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"equals"</span><span class="p">:</span><span class="w"> </span><span class="s2">"True"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Insights/diagnosticSettings/metrics.enabled"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"equals"</span><span class="p">:</span><span class="w"> </span><span class="s2">"True"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Insights/diagnosticSettings/workspaceId"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"equals"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('logAnalytics')]"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<p>My definition of an 100% working deployIfNotExists policy is one which:</p>

<ul>
  <li>successfully deploys the policy’s nested ARM template to your non-compliant resource</li>
  <li>post-remediation marks the resource as compliant after an evaluation scan</li>
</ul>

<p>Now for my use-case I needed to set a few of these parameters to “False” per below example.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">parameter_values</span><span class="w"> </span><span class="err">=</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"AllMetrics"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">value</span><span class="w"> </span><span class="err">=</span><span class="w"> </span><span class="s2">"False"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="nl">"kube-apiserver"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">value</span><span class="w"> </span><span class="err">=</span><span class="w"> </span><span class="s2">"False"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="nl">"kube-controller-manager"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">value</span><span class="w"> </span><span class="err">=</span><span class="w"> </span><span class="s2">"False"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="nl">"kube-scheduler"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">value</span><span class="w"> </span><span class="err">=</span><span class="w"> </span><span class="s2">"False"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="nl">"cluster-autoscaler"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="err">value</span><span class="w"> </span><span class="err">=</span><span class="w"> </span><span class="s2">"False"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Because the builtin policy’s existenceCondition shown previously expected all values in the array alias for “<strong>Microsoft.Insights/diagnosticSettings/logs.enabled</strong>” to equal to “<strong>True</strong>” I was never going to get a compliant resource. The remediation tasks would be successful but the policy was only really 50% working ‘out-of-the-box’. Not cool!</p>

<blockquote>
  <p>This is good example of incomplete policy authoring and why testing new policies against a proven methodology and framework can surface the issues as described in this blog post, particularly when the policy’s parameter values are changing.</p>
</blockquote>

<p>The new existenceCondition shown below 100% works even if the logs/metric parameter default values have changed e.g. from “True” to “False”. This is what we need to aim for across all diagnostic settings policies using the deployIfNotExists effect.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"existenceCondition"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"allOf"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nl">"field"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Insights/diagnosticSettings/logs.enabled"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"in"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
            </span><span class="s2">"[parameters('kube-apiserver')]"</span><span class="p">,</span><span class="w"> 
            </span><span class="s2">"[parameters('kube-audit')]"</span><span class="p">,</span><span class="w"> 
            </span><span class="s2">"[parameters('kube-controller-manager')]"</span><span class="p">,</span><span class="w"> 
            </span><span class="s2">"[parameters('kube-scheduler')]"</span><span class="p">,</span><span class="w"> 
            </span><span class="s2">"[parameters('cluster-autoscaler')]"</span><span class="p">,</span><span class="w"> 
            </span><span class="s2">"[parameters('kube-audit-admin')]"</span><span class="p">,</span><span class="w"> 
            </span><span class="s2">"[parameters('guard')]"</span><span class="w">
            </span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nl">"field"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Insights/diagnosticSettings/metrics.enabled"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"equals"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('AllMetrics')]"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="nl">"field"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Insights/diagnosticSettings/workspaceId"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"equals"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('logAnalytics')]"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<p>Thanks for joining me today, I look forward to your feedback and questions.</p>

<p>Read more about Azure Kubernetes Service (AKS) logs <a href="https://docs.microsoft.com/en-us/azure/aks/view-control-plane-logs?WT.mc_id=AZ-MVP-5004598">here</a></p>

<p>Interested in authoring policies which evaluate array aliases? I recommend reading <a href="https://docs.microsoft.com/en-us/azure/governance/policy/how-to/author-policies-for-arrays#in-and-notin?WT.mc_id=AZ-MVP-5004598">this</a></p>

<p>For a breakdown of available policy evaluation conditions check out this <a href="https://docs.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure#conditions?WT.mc_id=AZ-MVP-5004598">link</a></p>

<p>Keep fighting the good fight!</p>

<p>Jesse</p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="azure policy" /><category term="troubleshooting" /><category term="deployifnotexists" /><category term="non-compliance" /><category term="existenceCondition" /><category term="aks diagnostic settings" /><summary type="html"><![CDATA[Fixing a design flaw with the existenceCondition for builtin policies]]></summary></entry><entry><title type="html">HashiTalks ANZ: DRY Coding with Terraform, CSVs, ForEach</title><link href="https://jloudon.com/cloud/HashiTalks-ANZ-DRY-Coding-with-Terraform-CSVs-ForEach/" rel="alternate" type="text/html" title="HashiTalks ANZ: DRY Coding with Terraform, CSVs, ForEach" /><published>2021-04-28T00:00:00+10:00</published><updated>2021-04-28T00:00:00+10:00</updated><id>https://jloudon.com/cloud/HashiTalks%20ANZ%20DRY%20Coding%20with%20Terraform%20CSVs%20ForEach</id><content type="html" xml:base="https://jloudon.com/cloud/HashiTalks-ANZ-DRY-Coding-with-Terraform-CSVs-ForEach/"><![CDATA[<p>The recent inaugural HashiTalks Australia &amp; New Zealand event was an incredible opportunity to present a 30min session on how combining Terraform with CSVs and ForEach we can deploy at scale from large datasets and achieve a Don’t Repeat Yourself (DRY) methodology.</p>

<p>Firstly, here’s what you can expect from this article:</p>
<ul>
  <li>further context not captured by the recorded session</li>
  <li>insights into pros and cons (very important to know these before diving in)</li>
  <li>another usage pattern for your Terraform infrastructure-as-code journey</li>
</ul>

<blockquote>
  <p>You can find my HashiTalk video link/slides at the end of this post</p>
</blockquote>

<h1 id="further-context--proscons">Further Context + Pros/Cons</h1>

<p>CSVs and ForEach are not a ‘silver bullet’ for all your IaC scaling problems. I’m absolutely not saying go out and use CSVs and ForEach for ALL your Terraform IaC initiatives. If you’re reading this you’re hopefully taking the time to do your research and testing to evaluate the pattern for yourself!</p>

<p>There’ll be situations where you should be using native HCL for your data/inputs to avoid complexity whilst adhering to the KISS principle. What I’ve learnt from deploying large datasets are that these situations are not as common as I would like. Particularly if you need to manage an application that is NOT cloud-native and requires a vast range of IaaS resources to operate in your cloud of choice e.g. Microsoft Azure.</p>

<p>An elephant in the room is the question of why use CSV when you have YAML? The short answer is that, having tried both data formats, I’ve found having my inputs/variables stored within CSVs makes management of large datasets easier at scale and helps achieve a DRY coding methodology.</p>

<p>What I’ve also learnt in the CSV vs YAML debate:</p>

<ul>
  <li>When using YAML for IaC, the keys need to be duplicated for each new resource. Not very DRY. CSVs don’t have this issue because each header column represents the unique key in the resulting map.</li>
  <li>YAML info is presented vertically to you, so if you have 100s to 1000s of resources to manage from a single YAML file this makes management at scale difficult and tiresome! I prefer having the info horizontally presented to me using a CSV/Excel file so I can scroll across rather than scroll down. I’ve leveraged my IDE’s ctrl+find and fold/unfold options heavily with YAML datasets, but these shortcuts don’t change the overall experience much when it comes to large datasets.</li>
  <li>With CSV datasets I can open the files using Excel to use all the wonderful native Excel functions and features to manage/manipulate my dataset e.g. concat strings, freeze panes, column filters, pivot tables, etc! To my knowledge this exact functionality isn’t possible today with YAML datasets!</li>
  <li>With YAML it seems easier to spot extra spaces/rows in your datasets that shouldn’t be there. When using CSVs I’ve leveraged the ‘Edit CSV’ vscode extension to assist me in this area with great success.</li>
</ul>

<p>Take a look at these 2x NSG rules represented in two data formats. Then imagine managing 100s to 1000s of these NSG rules across multiple NSGs and multiple environments.</p>

<p><strong>YAML example</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">nsg1</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">Allow</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s">Allow RDP from Corp</span>
    <span class="na">destination_details</span><span class="pi">:</span> <span class="s">VirtualNetwork</span>
    <span class="na">destination_port_ranges</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3389"</span>
    <span class="na">destination_type</span><span class="pi">:</span> <span class="s">Tag</span>
    <span class="na">direction</span><span class="pi">:</span> <span class="s">Inbound</span>
    <span class="na">priority</span><span class="pi">:</span> <span class="m">100</span>
    <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
    <span class="na">rule_name</span><span class="pi">:</span> <span class="s">AllowRDPfromCorp</span>
    <span class="na">source_details</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10.10.0.0/24"</span>
    <span class="na">source_port_ranges</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
    <span class="na">source_type</span><span class="pi">:</span> <span class="s">IP Address</span>
  <span class="pi">-</span> <span class="na">action</span><span class="pi">:</span> <span class="s">Deny</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s">Deny RDP from Internet</span>
    <span class="na">destination_details</span><span class="pi">:</span> <span class="s">VirtualNetwork</span>
    <span class="na">destination_port_ranges</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3389"</span>
    <span class="na">destination_type</span><span class="pi">:</span> <span class="s">Tag</span>
    <span class="na">direction</span><span class="pi">:</span> <span class="s">Inbound</span>
    <span class="na">priority</span><span class="pi">:</span> <span class="m">200</span>
    <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
    <span class="na">rule_name</span><span class="pi">:</span> <span class="s">DenyRDPfromInternet</span>
    <span class="na">source_details</span><span class="pi">:</span> <span class="s">Internet</span>
    <span class="na">source_port_ranges</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
    <span class="na">source_type</span><span class="pi">:</span> <span class="s">Tag</span>
</code></pre></div></div>

<p><strong>CSV example</strong></p>
<pre><code class="language-csv">action,description,destination_details,destination_port_ranges,destination_type,direction,priority,protocol,rule_name,source_details,source_port_ranges,source_type
Allow,Allow RDP from Corp,VirtualNetwork,3389,Tag,Inbound,100,TCP,AllowRDPfromCorp,10.10.0.0/24,*,IP Address
Deny,Deny RDP from Internet,VirtualNetwork,3389,Tag,Inbound,200,TCP,DenyRDPfromInternet,Internet,*,Tag
</code></pre>

<p>Regardless of if you choose YAML or CSV watch out for these common IaC issues relating to:</p>
<ul>
  <li><strong>Bad data</strong> - this is where, due to the human-factor, you’ll have incorrect/errorneous values to investigate and ultimately fix-up before your IaC workflow is healthy.</li>
  <li><strong>Multiple sources of truth</strong> - working with customers to build environments as IaC often means working across teams of people who have their own process and habits for maintaining their IaC source of truth. This often differs from your own which leads to confusion/frustration.</li>
  <li><strong>Exceptions to your usage patterns</strong> - there’ll be times when the dataset and Terraform consumption pattern isn’t fit-for-purpose to meet every use-case and you’ll need to create an exception/ad-hoc implementation pattern.</li>
  <li><strong>Inconsistent naming</strong> - this often happens organically for your Terraform modules as they grow in scale and has been the cause of many ‘terraform state mv’ cmdlets in my experience.</li>
  <li><strong>API timeouts and resource dependencies</strong> - the larger your IaC implementation gets (I’m talking hundreds of resources as code in a single TFstate file) you’ll need to manage your changes in a staggered fashion to avoid seeing API timeouts and errors relating to dependencies not being ready.</li>
  <li><strong>TF provider parity with ARM</strong> - I’ve had a few cases where the AzureRM provider didn’t support something I needed as IaC and needed to use ARM templates or AzureCLI or even click-ops to acheive the desired outcome!</li>
</ul>

<p>I’ve enjoyed diving into this usage pattern and exploring the pros and cons with you today. I hope you’ve gained some value from my ranting and can relate to my IaC struggles of today :smile:</p>

<p>Cheers,</p>

<p>Jesse</p>

<h3 id="session-slides">Session Slides:</h3>
<p><img src="/assets/gifs/hashitalksanz2021.gif" alt="HashiTalksANZ2021" title="HashiTalks ANZ: DRY Coding with Terraform, CSVs, ForEach" /></p>

<h3 id="recorded-session">Recorded Session:</h3>
<p><a href="https://www.youtube.com/watch?v=JuzBolDyGdg&amp;t=976s">HashiTalks: Australia / New Zealand - YouTube</a></p>

<h3 id="vscode-extensions">VSCode extensions:</h3>
<p><a href="https://marketplace.visualstudio.com/items?itemName=janisdd.vscode-edit-csv">Edit csv</a></p>

<p><a href="https://marketplace.visualstudio.com/items?itemName=mechatroner.rainbow-csv">Rainbox CSV</a></p>

<h3 id="additional-reading">Additional Reading:</h3>
<p><a href="https://lucian.blog/terraform-csvs-netflix/">Terraform + CSVs = Netflix</a></p>

<p><a href="https://www.terraform.io/docs/language/functions/csvdecode.html">CSVDecode</a></p>

<p><a href="https://www.terraform.io/docs/language/meta-arguments/for_each.html">ForEach</a></p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="hashitalks" /><category term="terraform" /><category term="iac pattern" /><category term="csvs foreach" /><summary type="html"><![CDATA[How combining Terraform with CSVs and ForEach we can deploy at scale from large datasets]]></summary></entry><entry><title type="html">Global Azure: Policy as Code with Bicep for Enterprise Scale</title><link href="https://jloudon.com/cloud/Global-Azure-Policy-as-Code-with-Bicep-for-Enterprise-Scale/" rel="alternate" type="text/html" title="Global Azure: Policy as Code with Bicep for Enterprise Scale" /><published>2021-04-18T00:00:00+10:00</published><updated>2021-04-18T00:00:00+10:00</updated><id>https://jloudon.com/cloud/Global%20Azure%20Policy%20as%20Code%20with%20Bicep%20for%20Enterprise%20Scale</id><content type="html" xml:base="https://jloudon.com/cloud/Global-Azure-Policy-as-Code-with-Bicep-for-Enterprise-Scale/"><![CDATA[<p><img src="/assets/images/globalazure2021-2.png" alt="GlobalAzure2021" title="Global Azure 2021" /></p>

<blockquote>
  <p><a href="https://globalazure.net/">Global Azure</a> is a community event about the Microsoft Azure platform. On April 15-17 the Global Azure community goes online to share, learn, and have community Azure fun together.</p>
</blockquote>

<p>This year I was fortunate to have a session accepted for Global Azure 2021 titled: <strong>Policy as Code with Bicep for Enterprise Scale</strong></p>

<p>Recently I’ve been diving into Microsoft’s new DSL Bicep and as I’m passionate about Azure Policy I thought why not combine both aspects into a session?</p>

<p>Admittedly, I left my <a href="https://sessionize.com/JesseLoudon/">sessionize submission</a> very late and probably just scraped in. Even so it was a wonderful surprise to see my session accepted followed by a frantic scramble to prepare my presentation (YT links below) and code which can be found here <a href="https://github.com/globalbao/azure-policy-as-code">https://github.com/globalbao/azure-policy-as-code</a>.</p>

<h2 id="presentation-structure">Presentation Structure</h2>
<p>I structured my presentation into 3x levels so that beginners could start with a small proof of concept deployment and then scale up in complexity and advanced logic as their comfort levels increased with Bicep and Azure Policy.</p>

<p>More experienced users can jump straight to Level 3 and learn/adopt a Policy as Code workflow with Bicep.</p>

<p>Links to skip to specific content levels in the recorded session are included below!</p>

<h3 id="level-1"><a href="https://github.com/globalbao/azure-policy-as-code/tree/main/Bicep/Level1">Level 1</a></h3>

<ul>
  <li>Uses built-in policies</li>
  <li>Uses an initiative and assignment</li>
  <li>1x main.bicep</li>
  <li>Manual CLI deployment</li>
</ul>

<p><a href="https://www.youtube.com/watch?v=qpnMJXw6pIg&amp;t=16m10s">YouTube Video Timestamp 16m 10s</a></p>

<h3 id="level-2"><a href="https://github.com/globalbao/azure-policy-as-code/tree/main/Bicep/Level2">Level 2</a></h3>

<ul>
  <li>Uses built-in policies and custom policies</li>
  <li>Uses multiple initiatives and assignments</li>
  <li>1x main.bicep</li>
  <li>Manual CLI deployment</li>
  <li>Targeting multiple Azure environments</li>
  <li>Uses parameter files for environment-specfic values passed during deployment</li>
</ul>

<p><a href="https://www.youtube.com/watch?v=qpnMJXw6pIg&amp;t=50m3s">YouTube Video Timestamp 50m 3s</a></p>

<h3 id="level-3"><a href="https://github.com/globalbao/azure-policy-as-code/tree/main/Bicep/Level3">Level 3</a></h3>

<ul>
  <li>Uses built-in policies and custom policies</li>
  <li>Uses multiple initiatives and assignments</li>
  <li>Custom policyDefinitionReferenceId for initiatives</li>
  <li>Custom non-compliance msgs for assignments targeted to the policyDefinitionReferenceId</li>
  <li>Advanced modules organised per resource type</li>
  <li>CI/CD workflow automation with GitHub Actions YAML</li>
  <li>Targeting multiple Azure environments with authentication via GitHub secrets</li>
</ul>

<p><a href="https://www.youtube.com/watch?v=qpnMJXw6pIg&amp;t=1h11m45s">YouTube Video Timestamp 1h 11m 45s</a></p>

<h2 id="kudos">Kudos</h2>

<p>Finally, I’d like to thank <a href="https://twitter.com/rahulpnath">Rahul Nath</a> for reaching out to me prior to the session and helping me out with YouTube streaming/gearing tips. Much appreciated Rahul!</p>

<p>This year’s Global Azure event was the biggest yet with over 560 speakers and 530 sessions from across the world! So a big kudos to the organisers and session reviewers who contributed their time to make this happen!</p>

<p>Looking forward to next year’s Global Azure!</p>

<p>Jesse</p>

<p><img src="/assets/images/globalazure2021sessions.png" alt="GlobalAzure2021Sessions" title="Global Azure 2021 Sessions" /></p>]]></content><author><name>Jesse</name></author><category term="cloud" /><category term="bicep" /><category term="global azure" /><category term="azure policy" /><category term="policy as code" /><summary type="html"><![CDATA[Recently I've been diving into Microsoft's new DSL Bicep and as I'm passionate about Azure Policy I thought why not combine both aspects into a session?]]></summary></entry></feed>