How I Built Graft: An Overlay Engine for Terraform Modules — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogHow I Built Graft: An Overlay Engine for Terraform Modules
    Back to Blog
    How I Built Graft: An Overlay Engine for Terraform Modules
    terraform

    How I Built Graft: An Overlay Engine for Terraform Modules

    Heng Lu February 5, 2026
    0 views

    There's a Terraform GitHub issue that's been open for years: people want to customize modules without...

    There's a [Terraform GitHub issue](https://github.com/hashicorp/terraform/issues/27360) that's been open for years: people want to customize modules without forking them. Add a lifecycle block. Tweak a tag. Simple stuff. 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. I built Graft to solve this. It patches Terraform modules in place—no forks, no merge conflicts. And honestly, it's a middleware for something bigger I'm working on. But I'll save that for the next post. :) ## The Idea The goal: use declarative Terraform blocks to describe modifications to existing modules. It should: - Modify multi-layer (nested) modules - Work easily with existing modules - Stay compatible when modules update So I can define a graft manifest like this: ```hcl module "network" { override { # patches to modify the existing module } module "subnet" { override { # patches to modify the nested module } } } ``` 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. ## First Attempt: Override Files My first idea was to use Terraform's native override mechanism. If you create `override.tf`, it merges with your main config. ([Official docs](https://developer.hashicorp.com/terraform/language/files/override)) But override files have serious limitations: 1. You can't add new blocks—only modify existing ones 2. You can't delete blocks or attributes Not enough. ## Second Attempt: Enhanced Override Files Since the graft manifest is processed *before* Terraform runs, I have more control than native overrides. **Adding new blocks** was easy: check the source code, then generate a new file `_graft_add.tf` in the module directory. **Deleting things** 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? I introduced a special `_graft` block: ```hcl resource "azurerm_network_security_rule" "allow_all" { _graft { remove = ["self"] # Delete the entire resource } } resource "azurerm_virtual_network" "vnet" { _graft { remove = ["dns_servers", "tags"] # Delete specific attributes } } ``` 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 *belongs* in Terraform, even though Terraform itself can't do this. ## Referencing Original Values While testing the override strategy, I ran into an interesting problem with `count` and `for_each` resources. Say a module creates multiple subnets with `for_each`, and I want to modify just one of them. I can target a specific key: ```hcl resource "azurerm_subnet" "main" { for_each = var.subnets # Only modify subnet1 service_endpoints = each.key == "subnet1" ? ["Microsoft.Storage"] : ??? } ``` But what goes in the `???`? I need the *original* 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. This is where `graft.source` 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. ```hcl service_endpoints = each.key == "subnet1" ? ["Microsoft.Storage"] : graft.source ``` This also solves another frustration with Terraform's native override files: they use **shallow merge** for attributes. If you want to add one tag, you can't—your override *replaces* the entire `tags` map, wiping out the module's defaults. With `graft.source`, you can actually merge: ```hcl tags = merge(graft.source, { "Owner" = "Platform Team" }) ``` During patching, `graft.source` gets replaced with the actual original expression. You get true merging—and you don't need to know what the original value was. ## The Linker Problem Now I had patching working. But how do I make Terraform *use* the patched modules? My first idea: use an override file to redirect the module source to a local patched copy. ```hcl # file: _graft_override.tf # What I tried to generate module "network" { source = "./.graft/patched-network" } ``` It failed immediately. **You can't override `source` when there's a `version` constraint:** ```hcl # Original main.tf module "network" { source = "Azure/network/azurerm" version = "5.3.0" # ← This kills the override } ``` Terraform throws: *"Cannot apply a version constraint to module 'network' because it has a relative local path."* And you can't "unset" the version—override files can only add or modify, never delete. Dead end. ## The Breakthrough: Hijacking modules.json I started digging into how Terraform actually resolves modules. When you run `terraform init`, Terraform downloads modules and records their locations in `.terraform/modules/modules.json`: ```json { "Modules": [ { "Key": "network", "Source": "registry.terraform.io/Azure/network/azurerm", "Version": "5.3.0", "Dir": ".terraform/modules/network" } ] } ``` What if I just changed where `Dir` points? I tried it manually—edited `modules.json`, pointed `Dir` to a local folder with patched code. It worked. Terraform loaded my patched module while believing it was using the official registry version. No errors. No need to modify `main.tf`. I called this the **Linker Strategy**—like how linkers resolve symbols to addresses, Graft resolves modules to patched directories. ## The Scaffold Command 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. 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 `.terraform/modules`. So I added `graft scaffold`. It scans your `.terraform/modules` directory and generates a starter manifest with the full module tree: ```bash $ graft scaffold [+] Discovering modules in .terraform/modules... root ├── network (registry.terraform.io/Azure/network/azurerm, 5.3.0) │ └── [3 resources] └── compute (registry.terraform.io/Azure/compute/azurerm, 5.3.0) ├── [18 resources] └── compute.os (local: ./os) └── [2 resources] ✨ Graft manifest saved to scaffold.graft.hcl ``` Simple, but essential. Now users can see the hierarchy at a glance and start writing overrides immediately—without ever opening the module source. ## Try It ```bash go install github.com/ms-henglu/graft@latest ``` The workflow: ```bash terraform init graft scaffold # See the module tree, generate starter manifest # Edit manifest.graft.hcl graft build # Vendor, patch, and link terraform plan # Your patches are applied ``` Your `main.tf` never changes. When the upstream module releases a new version, bump the version, run `terraform init && graft build`, and your patches are reapplied. No forks. No merge conflicts. Check the [examples](https://github.com/ms-henglu/graft/tree/main/examples) for patterns like overriding values, injecting resources, removing attributes, and adding lifecycle rules. --- The full code is at [github.com/ms-henglu/graft](https://github.com/ms-henglu/graft). If you try it and hit issues—or have ideas—[open an issue](https://github.com/ms-henglu/graft/issues). I'd love to hear what breaks. Happy patching. 🌱

    Tags

    terraformdevopsazure

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.