<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Heng Lu</title>
    <description>The latest articles on DEV Community by Heng Lu (@mshenglu).</description>
    <link>https://dev.to/mshenglu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3754293%2Fa3052aea-1572-4b61-a901-94f389f8c47f.jpeg</url>
      <title>DEV Community: Heng Lu</title>
      <link>https://dev.to/mshenglu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mshenglu"/>
    <language>en</language>
    <item>
      <title>How I Built Graft Absorb: Turning Terraform Drift into Code</title>
      <dc:creator>Heng Lu</dc:creator>
      <pubDate>Fri, 13 Feb 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/mshenglu/how-i-built-graft-absorb-turning-terraform-drift-into-code-55g</link>
      <guid>https://dev.to/mshenglu/how-i-built-graft-absorb-turning-terraform-drift-into-code-55g</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/mshenglu/how-i-built-graft-an-overlay-engine-for-terraform-modules-5d8i"&gt;last post&lt;/a&gt;, I introduced Graft—a tool for patching Terraform modules without forking them. I mentioned it was middleware for something bigger. This is that something.&lt;/p&gt;

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

&lt;p&gt;Terraform modules should be black boxes. We manage them through input variables, and ideally, we never need to know what's inside. But when drift happens—someone changes a resource in the portal, an external process modifies a tag, a compliance tool enforces a setting—&lt;code&gt;terraform plan&lt;/code&gt; exposes the internals. Suddenly you're staring at resource-level changes deep inside a module you didn't write.&lt;/p&gt;

&lt;p&gt;The typical response is painful: read through the plan output, find every difference, trace each change back to its source, and manually update configuration files to match the actual state. Even worse, if the module doesn't expose the right variables, there's simply no way to update the configuration to eliminate the drift.&lt;/p&gt;

&lt;p&gt;The common workaround is &lt;code&gt;lifecycle { ignore_changes }&lt;/code&gt;. But ignoring changes is not the same as managing them. You're telling Terraform to look the other way—which means your code no longer reflects reality. I think recording the actual desired state in code is always better than ignoring changes blindly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;What if we could automate this? Read a &lt;code&gt;terraform plan&lt;/code&gt;, extract the drift, and generate the code changes needed to make the configuration match reality.&lt;/p&gt;

&lt;p&gt;In the ideal scenario, such a tool would trace drift back to module input variables and update them directly. But I found this impractical—the mapping between inputs and resource attributes isn't always straightforward. A module might derive a resource's &lt;code&gt;tags&lt;/code&gt; from a combination of multiple variables, local values, and conditional logic. Reverse-engineering that reliably is a dead end.&lt;/p&gt;

&lt;p&gt;But Graft already solves the hard part. It can apply changes directly to module resources, bypassing module inputs entirely. So instead of trying to update variables, I can generate a Graft manifest that patches the drifted resources in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;graft absorb&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I created a new command—&lt;code&gt;graft absorb&lt;/code&gt;—that takes a Terraform plan file as input, analyzes every detected drift, and generates a Graft manifest describing the changes needed to bring configuration in line with actual state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tfplan
terraform show &lt;span class="nt"&gt;-json&lt;/span&gt; tfplan &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plan.json
graft absorb plan.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The generated manifest is a standard &lt;code&gt;.graft.hcl&lt;/code&gt; file. Review it, run &lt;code&gt;graft build&lt;/code&gt;, and your next &lt;code&gt;terraform plan&lt;/code&gt; should show zero changes.&lt;/p&gt;

&lt;p&gt;Multi-layer modules work out of the box. Graft already knows how to navigate nested module hierarchies and apply patches at the correct level. And since the manifest format is just Terraform HCL, it's expressive enough to handle complex changes—attribute overrides, block additions, removals, all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Part: Indexed Resources
&lt;/h2&gt;

&lt;p&gt;The simple resources with no indexing are easy. The real challenge is &lt;code&gt;count&lt;/code&gt; and &lt;code&gt;for_each&lt;/code&gt; resources.&lt;/p&gt;

&lt;p&gt;Consider a resource with &lt;code&gt;count = 5&lt;/code&gt;, where only 2 instances have drifted. I don't want to generate a manifest that overrides all 5 instances. It should target only the drifted ones—and still work correctly when the module updates and the instance count changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attribute Drift
&lt;/h3&gt;

&lt;p&gt;For attributes, I settled on a &lt;code&gt;lookup()&lt;/code&gt; pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# count-indexed resources&lt;/span&gt;
&lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"drifttest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"graft"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"drifttest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"graft"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;graft&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# for_each-indexed resources&lt;/span&gt;
&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"westus"&lt;/span&gt;
  &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"centralus"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;graft&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The map contains only the drifted instances. &lt;code&gt;lookup&lt;/code&gt; checks whether the current &lt;code&gt;count.index&lt;/code&gt; or &lt;code&gt;each.key&lt;/code&gt; has an entry. If it does, the new value is used. If not, &lt;code&gt;graft.source&lt;/code&gt; kicks in—referencing the original expression from the module source, leaving un-drifted instances completely untouched.&lt;/p&gt;

&lt;p&gt;This is resilient to module updates. If the upstream module adds new instances, they'll fall through to &lt;code&gt;graft.source&lt;/code&gt; and behave exactly as the module author intended.&lt;/p&gt;

&lt;h3&gt;
  
  
  Block Drift
&lt;/h3&gt;

&lt;p&gt;Block-type attributes (like &lt;code&gt;security_rule&lt;/code&gt; or &lt;code&gt;os_disk&lt;/code&gt;) need a different approach. You can't use &lt;code&gt;graft.source&lt;/code&gt; as a fallback for blocks because the original source has static block definitions, not list expressions that &lt;code&gt;dynamic&lt;/code&gt; blocks can iterate over.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;graft absorb&lt;/code&gt; generates &lt;code&gt;dynamic&lt;/code&gt; blocks with &lt;code&gt;lookup()&lt;/code&gt; in the &lt;code&gt;for_each&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"nsg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;_graft&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"security_rule"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Remove original static blocks&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="s2"&gt;"security_rule"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow-ssh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow-https"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow-http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"deny-all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;security_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="nx"&gt;priority&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;security_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;
      &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;security_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each key in the lookup map is an instance index (or &lt;code&gt;each.key&lt;/code&gt; for &lt;code&gt;for_each&lt;/code&gt; resources), and the value is a list of block objects for that instance. The &lt;code&gt;content&lt;/code&gt; block uses &lt;code&gt;security_rule.value.attr&lt;/code&gt; to reference each attribute.&lt;/p&gt;

&lt;p&gt;The fallback here is &lt;code&gt;[]&lt;/code&gt;—an empty list—not &lt;code&gt;graft.source&lt;/code&gt;. This means instances without an entry in the lookup map produce no dynamic block iterations. Combined with &lt;code&gt;_graft { remove = ["security_rule"] }&lt;/code&gt;, which strips the original static blocks, the dynamic blocks become the sole source of truth.&lt;/p&gt;

&lt;p&gt;For single nested blocks like &lt;code&gt;os_disk&lt;/code&gt;, the same pattern applies—the lookup value is just a single-element list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="s2"&gt;"os_disk"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;caching&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ReadWrite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disk_size_gb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;storage_account_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Premium_LRS"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;caching&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ReadOnly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;disk_size_gb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;storage_account_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StandardSSD_LRS"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;caching&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os_disk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;caching&lt;/span&gt;
    &lt;span class="nx"&gt;disk_size_gb&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os_disk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disk_size_gb&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os_disk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage_account_type&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tfplan
terraform show &lt;span class="nt"&gt;-json&lt;/span&gt; tfplan &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plan.json
graft absorb plan.json        &lt;span class="c"&gt;# Generate the manifest&lt;/span&gt;
&lt;span class="c"&gt;# Review absorb.graft.hcl&lt;/span&gt;
graft build                   &lt;span class="c"&gt;# Apply patches&lt;/span&gt;
terraform plan                &lt;span class="c"&gt;# Should show zero changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full code and more examples are at &lt;a href="https://github.com/ms-henglu/graft" rel="noopener noreferrer"&gt;github.com/ms-henglu/graft&lt;/a&gt;. For a step-by-step walkthrough with a real public module, check the &lt;a href="https://github.com/ms-henglu/graft/blob/main/docs/absorb-guide.md" rel="noopener noreferrer"&gt;absorb guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you hit edge cases or have ideas for improving the generated output, &lt;a href="https://github.com/ms-henglu/graft/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I Built Graft: An Overlay Engine for Terraform Modules</title>
      <dc:creator>Heng Lu</dc:creator>
      <pubDate>Thu, 05 Feb 2026 07:08:59 +0000</pubDate>
      <link>https://dev.to/mshenglu/how-i-built-graft-an-overlay-engine-for-terraform-modules-5d8i</link>
      <guid>https://dev.to/mshenglu/how-i-built-graft-an-overlay-engine-for-terraform-modules-5d8i</guid>
      <description>&lt;p&gt;There's a &lt;a href="https://github.com/hashicorp/terraform/issues/27360" rel="noopener noreferrer"&gt;Terraform GitHub issue&lt;/a&gt; that's been open for years: people want to customize modules without forking them. Add a lifecycle block. Tweak a tag. Simple stuff.&lt;/p&gt;

&lt;p&gt;I understand why Terraform doesn't support this natively—modules are supposed to be black boxes, and breaking the encapsulation is not ideal. But in practice, modules often need tweaks.&lt;/p&gt;

&lt;p&gt;I built Graft to solve this. It patches Terraform modules in place—no forks, no merge conflicts. &lt;/p&gt;

&lt;p&gt;And honestly, it's a middleware for something bigger I'm working on. But I'll save that for the next post. :)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;The goal: use declarative Terraform blocks to describe modifications to existing modules. It should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modify multi-layer (nested) modules&lt;/li&gt;
&lt;li&gt;Work easily with existing modules&lt;/li&gt;
&lt;li&gt;Stay compatible when modules update&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I can define a graft manifest like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"network"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;override&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# patches to modify the existing module&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;override&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;# patches to modify the nested module&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nested structure mirrors the module hierarchy. This makes it easy to locate exactly which blocks you want to modify—just navigate down the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Attempt: Override Files
&lt;/h2&gt;

&lt;p&gt;My first idea was to use Terraform's native override mechanism. If you create &lt;code&gt;override.tf&lt;/code&gt;, it merges with your main config. (&lt;a href="https://developer.hashicorp.com/terraform/language/files/override" rel="noopener noreferrer"&gt;Official docs&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;But override files have serious limitations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can't add new blocks—only modify existing ones&lt;/li&gt;
&lt;li&gt;You can't delete blocks or attributes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second Attempt: Enhanced Override Files
&lt;/h2&gt;

&lt;p&gt;Since the graft manifest is processed &lt;em&gt;before&lt;/em&gt; Terraform runs, I have more control than native overrides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding new blocks&lt;/strong&gt; was easy: check the source code, then generate a new file &lt;code&gt;_graft_add.tf&lt;/code&gt; in the module directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleting things&lt;/strong&gt; required a new approach. The implementation wasn't hard—just parse the manifest and remove matching blocks from the source files. But the design was tricky: how do you express "delete this" in a way that feels native to Terraform?&lt;/p&gt;

&lt;p&gt;I introduced a special &lt;code&gt;_graft&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_rule"&lt;/span&gt; &lt;span class="s2"&gt;"allow_all"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;_graft&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"self"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Delete the entire resource&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_network"&lt;/span&gt; &lt;span class="s2"&gt;"vnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;_graft&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dns_servers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Delete specific attributes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks like regular HCL. It nests inside the resource block you're targeting. It follows Terraform's declarative style. That's what I wanted—something that feels like it &lt;em&gt;belongs&lt;/em&gt; in Terraform, even though Terraform itself can't do this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencing Original Values
&lt;/h2&gt;

&lt;p&gt;While testing the override strategy, I ran into an interesting problem with &lt;code&gt;count&lt;/code&gt; and &lt;code&gt;for_each&lt;/code&gt; resources.&lt;/p&gt;

&lt;p&gt;Say a module creates multiple subnets with &lt;code&gt;for_each&lt;/code&gt;, and I want to modify just one of them. I can target a specific key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnets&lt;/span&gt;

  &lt;span class="c1"&gt;# Only modify subnet1&lt;/span&gt;
  &lt;span class="nx"&gt;service_endpoints&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"subnet1"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Storage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="err"&gt;???&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what goes in the &lt;code&gt;???&lt;/code&gt;? I need the &lt;em&gt;original&lt;/em&gt; value to avoid affecting other subnets. Without knowing what the module originally set, I'd have to hardcode it—or worse, accidentally break the other subnets.&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;graft.source&lt;/code&gt; came from. It references the original value—no matter how complicated the expression is. I don't need to look it up in the module source code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;service_endpoints&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"subnet1"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Storage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;graft&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also solves another frustration with Terraform's native override files: they use &lt;strong&gt;shallow merge&lt;/strong&gt; for attributes. If you want to add one tag, you can't—your override &lt;em&gt;replaces&lt;/em&gt; the entire &lt;code&gt;tags&lt;/code&gt; map, wiping out the module's defaults.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;graft.source&lt;/code&gt;, you can actually merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;graft&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Owner"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Platform Team"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During patching, &lt;code&gt;graft.source&lt;/code&gt; gets replaced with the actual original expression. You get true merging—and you don't need to know what the original value was.&lt;/p&gt;

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

&lt;p&gt;Now I had patching working. But how do I make Terraform &lt;em&gt;use&lt;/em&gt; the patched modules?&lt;/p&gt;

&lt;p&gt;My first idea: use an override file to redirect the module source to a local patched copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# file: _graft_override.tf&lt;/span&gt;
&lt;span class="c1"&gt;# What I tried to generate&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"network"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./.graft/patched-network"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It failed immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can't override &lt;code&gt;source&lt;/code&gt; when there's a &lt;code&gt;version&lt;/code&gt; constraint:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Original main.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"network"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Azure/network/azurerm"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"5.3.0"&lt;/span&gt;  &lt;span class="c1"&gt;# ← This kills the override&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform throws: &lt;em&gt;"Cannot apply a version constraint to module 'network' because it has a relative local path."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And you can't "unset" the version—override files can only add or modify, never delete.&lt;/p&gt;

&lt;p&gt;Dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough: Hijacking modules.json
&lt;/h2&gt;

&lt;p&gt;I started digging into how Terraform actually resolves modules.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;terraform init&lt;/code&gt;, Terraform downloads modules and records their locations in &lt;code&gt;.terraform/modules/modules.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Modules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registry.terraform.io/Azure/network/azurerm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5.3.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Dir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".terraform/modules/network"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What if I just changed where &lt;code&gt;Dir&lt;/code&gt; points? I tried it manually—edited &lt;code&gt;modules.json&lt;/code&gt;, pointed &lt;code&gt;Dir&lt;/code&gt; to a local folder with patched code.&lt;/p&gt;

&lt;p&gt;It worked. Terraform loaded my patched module while believing it was using the official registry version. No errors. No need to modify &lt;code&gt;main.tf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I called this the &lt;strong&gt;Linker Strategy&lt;/strong&gt;—like how linkers resolve symbols to addresses, Graft resolves modules to patched directories.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scaffold Command
&lt;/h2&gt;

&lt;p&gt;One thing bothered me. Graft's whole point is that you shouldn't need to understand a module's internals—just declare what you want to change.&lt;/p&gt;

&lt;p&gt;But when I actually used it, I kept opening module source files anyway. Which nested module contains that resource? What's the hierarchy? Even as the author, I couldn't write a manifest without digging through &lt;code&gt;.terraform/modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I added &lt;code&gt;graft scaffold&lt;/code&gt;. It scans your &lt;code&gt;.terraform/modules&lt;/code&gt; directory and generates a starter manifest with the full module tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;graft scaffold

&lt;span class="o"&gt;[&lt;/span&gt;+] Discovering modules &lt;span class="k"&gt;in&lt;/span&gt; .terraform/modules...
root
├── network &lt;span class="o"&gt;(&lt;/span&gt;registry.terraform.io/Azure/network/azurerm, 5.3.0&lt;span class="o"&gt;)&lt;/span&gt;
│   └── &lt;span class="o"&gt;[&lt;/span&gt;3 resources]
└── compute &lt;span class="o"&gt;(&lt;/span&gt;registry.terraform.io/Azure/compute/azurerm, 5.3.0&lt;span class="o"&gt;)&lt;/span&gt;
    ├── &lt;span class="o"&gt;[&lt;/span&gt;18 resources]
    └── compute.os &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;: ./os&lt;span class="o"&gt;)&lt;/span&gt;
        └── &lt;span class="o"&gt;[&lt;/span&gt;2 resources]

✨ Graft manifest saved to scaffold.graft.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, but essential. Now users can see the hierarchy at a glance and start writing overrides immediately—without ever opening the module source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/ms-henglu/graft@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
graft scaffold    &lt;span class="c"&gt;# See the module tree, generate starter manifest&lt;/span&gt;
&lt;span class="c"&gt;# Edit manifest.graft.hcl&lt;/span&gt;
graft build       &lt;span class="c"&gt;# Vendor, patch, and link&lt;/span&gt;
terraform plan    &lt;span class="c"&gt;# Your patches are applied&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;main.tf&lt;/code&gt; never changes. When the upstream module releases a new version, bump the version, run &lt;code&gt;terraform init &amp;amp;&amp;amp; graft build&lt;/code&gt;, and your patches are reapplied.&lt;/p&gt;

&lt;p&gt;No forks. No merge conflicts.&lt;/p&gt;

&lt;p&gt;Check the &lt;a href="https://github.com/ms-henglu/graft/tree/main/examples" rel="noopener noreferrer"&gt;examples&lt;/a&gt; for patterns like overriding values, injecting resources, removing attributes, and adding lifecycle rules.&lt;/p&gt;




&lt;p&gt;The full code is at &lt;a href="https://github.com/ms-henglu/graft" rel="noopener noreferrer"&gt;github.com/ms-henglu/graft&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you try it and hit issues—or have ideas—&lt;a href="https://github.com/ms-henglu/graft/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;. I'd love to hear what breaks.&lt;/p&gt;

&lt;p&gt;Happy patching. 🌱&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>azure</category>
    </item>
  </channel>
</rss>
