<?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: Zachary Loeber</title>
    <description>The latest articles on DEV Community by Zachary Loeber (@zloeber).</description>
    <link>https://dev.to/zloeber</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%2F135969%2Fc0b98bdd-480d-46da-bcfd-821ab422983a.jpg</url>
      <title>DEV Community: Zachary Loeber</title>
      <link>https://dev.to/zloeber</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zloeber"/>
    <language>en</language>
    <item>
      <title>3 LLM Underdogs of 2025</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Thu, 08 Jan 2026 17:29:53 +0000</pubDate>
      <link>https://dev.to/zloeber/3-llm-underdogs-of-2025-2e1i</link>
      <guid>https://dev.to/zloeber/3-llm-underdogs-of-2025-2e1i</guid>
      <description>&lt;p&gt;2025 has been a flurry of AI madness that has been hard to keep up with. I've been deep in learning and experimenting in the AI space and noticed that while everyone's hyping up the latest GPT variant or Claude release, there are some genuinely impressive open-source models that feel like they just flew under the radar in 2025. These aren't just "good for their size", they're legit excellent models that you can run locally, for free, and deserve more attention.&lt;/p&gt;




&lt;p&gt;This isn't a benchmark shootout or a comparison article. Instead, I want to shine a light on three models that I think are more important than they're given credit for. They range in parameter size and are: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Technology Innovation Institute's &lt;a href="https://falconllm.tii.ae/falcon-h1r-7b.html" rel="noopener noreferrer"&gt;Falcon H1R 7B&lt;/a&gt; - A super fast edge-ready math wiz and reasoning model&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NVIDEA's Nemotron Nano &lt;a href="https://huggingface.co/nvidia/Llama-3.1-Nemotron-Nano-8B-v1" rel="noopener noreferrer"&gt;8b&lt;/a&gt; and &lt;a href="https://research.nvidia.com/labs/nemotron/files/NVIDIA-Nemotron-3-Nano-Technical-Report.pdf" rel="noopener noreferrer"&gt;30b&lt;/a&gt; - This model sports a massive maximum context length of 1 million tokens!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ServiceNow's &lt;a href="https://huggingface.co/ServiceNow-AI/Apriel-1.6-15b-Thinker" rel="noopener noreferrer"&gt;Apriel 1.6 15b Thinker&lt;/a&gt; - A mid-sized LLM that trounces other models on tool usage&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each brings something unique to the table lets proceed and try to gain some understanding as to what makes them special and where you might want to put them to work for ya.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; If you want to compare/contrast these and other open source models in this class they are all technically in the 'small' class of models at &lt;a href="https://artificialanalysis.ai/models/open-source/small" rel="noopener noreferrer"&gt;Artificial Analysis&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Falcon H1R 7B: Efficiency Meets Reasoning
&lt;/h2&gt;

&lt;p&gt;Let's start with &lt;a href="https://www.tii.ae/about-us" rel="noopener noreferrer"&gt;TII&lt;/a&gt;'s Falcon H1R 7B, which just dropped (literally, like hours ago as I'm writing this). This one caught my attention immediately because it challenges a fundamental assumption we've all been making: that you need massive models for serious reasoning tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Makes It Special
&lt;/h3&gt;

&lt;p&gt;The Falcon H1R 7B uses a hybrid Transformer-Mamba architecture, which is a fancy way of saying they've combined two different approaches to get better performance with fewer parameters. The result? This 7-billion parameter model is punching way above its weight class. It scored &lt;strong&gt;88.1% on AIME-24 mathematics benchmarks&lt;/strong&gt;, outperforming ServiceNow's Apriel 1.5 at 15B parameters. Yeah, a model with less than half the parameters performing better on advanced math.&lt;/p&gt;

&lt;p&gt;But here's where it gets really interesting: it processes up to 1,500 tokens per second per GPU at batch size 64. That's nearly &lt;strong&gt;double the speed of comparable models&lt;/strong&gt;. For anyone building multi-agent systems or handling high-volume inference workloads, this matters tremendously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where It Shines
&lt;/h3&gt;

&lt;p&gt;As mentioned already, this model is quite good at math but that's not it's main superpower. The sweet spot for Falcon H1R 7B is anywhere you need reliable reasoning without the compute overhead of larger models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge deployments&lt;/strong&gt;: Running on constrained hardware where every parameter counts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time applications&lt;/strong&gt;: That token throughput makes it viable for interactive systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Math and coding tasks&lt;/strong&gt;: It delivers 68.6% accuracy on coding and agentic tasks, best-in-class for models under 8B&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Energy-conscious deployments&lt;/strong&gt;: Lower memory and energy consumption while maintaining near-perfect scores on benchmarks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why It's Important
&lt;/h3&gt;

&lt;p&gt;The open-source AI community has been in an arms race of parameter counts. Falcon H1R 7B demonstrates that architectural innovations can matter more than raw size. It's released under the Falcon TII License, making it accessible for both research and commercial use. For developers building on limited budgets or those who need to deploy at scale, this efficiency-without-compromise approach is exactly what we need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nemotron Nano: Massive Context Length
&lt;/h2&gt;

&lt;p&gt;NVIDIA's Nemotron Nano family represents a different kind of innovation. The 8B variant (Llama-3.1-Nemotron-Nano-8B-v1) and the more recent 30B variant are part of what NVIDIA calls their most efficient family of open models with leading accuracy for agentic AI applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Makes Them Special
&lt;/h3&gt;

&lt;p&gt;The Nemotron Nano models use a hybrid Mixture-of-Experts (MoE) architecture combined with Mamba-2 layers. The 30B model is actually a 31.6B total parameter model that activates only about 3.6B parameters per token. This sparse activation approach means you get the intelligence of a much larger model with the speed and memory footprint of a smaller one.&lt;/p&gt;

&lt;p&gt;The context window is another standout feature: &lt;strong&gt;1 million tokens&lt;/strong&gt;! Yes, you read that right. For comparison, that's enough to fit several entire codebases or extensive documentation in a single context. The implications for code review, documentation generation, and long-form reasoning tasks are significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where They Shine
&lt;/h3&gt;

&lt;p&gt;Aside from the large context, the Nemotron Nano models excel in scenarios where you need both reasoning capability and practical throughput:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent systems&lt;/strong&gt;: The 30B variant delivers 4x higher throughput than Nemotron 2 Nano, making it ideal for systems where multiple agents need to collaborate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software development&lt;/strong&gt;: Best-in-class performance on SWE-Bench among models in its size class&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic workflows&lt;/strong&gt;: Built specifically for tasks like software debugging, content summarization, and information retrieval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-context tasks&lt;/strong&gt;: That 1M token window makes it perfect for analyzing large codebases or extensive documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 8B variant is particularly interesting for edge deployments or when you want reasoning capabilities on more modest hardware. NVIDIA optimized it specifically for PC and edge use cases, and it shows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why They're Important
&lt;/h3&gt;

&lt;p&gt;NVIDIA isn't just releasing models, they're releasing the entire ecosystem. The Nemotron 3 family comes with open training datasets (25T tokens worth), reinforcement learning environments through NeMo Gym, and the full training recipe. This level of transparency is rare and incredibly valuable for researchers and practitioners who want to understand not just what works, but why it works.&lt;/p&gt;

&lt;p&gt;The hybrid MoE architecture is also proving to be a game-changer for efficiency. By activating only a subset of parameters per token, these models achieve what researchers call the "Pareto frontier", optimal speed without sacrificing quality. This architectural approach could influence how we think about model design going forward.&lt;/p&gt;




&lt;h2&gt;
  
  
  Apriel 1.6 15B Thinker: Tool Wielding Reasoning Model
&lt;/h2&gt;

&lt;p&gt;Now let's talk about Apriel 1.6 15B Thinker, which might be the most underrated model in this entire lineup. ServiceNow has been quietly building something impressive with their Apriel SLM series, and version 1.6 demonstrates what's possible when you focus on both performance and efficiency.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Makes It Special
&lt;/h3&gt;

&lt;p&gt;Apriel 1.6 is a multimodal reasoning model, meaning it can work with both text and images. It scored 57 on the Artificial Analysis Index, putting it on par with models like Qwen3 235B A22B and DeepSeek-v3.2 (all models that are 15x larger).&lt;/p&gt;

&lt;p&gt;If you look closer at this model compared to others in it's class you will find that it absolutely trounces almost all the other's in &lt;a href="https://artificialanalysis.ai/models/open-source/small" rel="noopener noreferrer"&gt;tool use&lt;/a&gt;. It outperforms larger parameter models like gpt-oss-20b by almost 10% in some of the tests. Looking through the various test charts it is almost funny to see how many of the models with 2x the parameters score less than Apriel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; The ability to use tools well is the difference between a toy LLM you play with and a machine you can use for real work. A model that can use tools well can also be supplemented with MCP servers to give them additional skills and capabilities beyond their training as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Where It Shines
&lt;/h3&gt;

&lt;p&gt;Apriel 1.6 excels in domains where you need both vision and reasoning in the enterprise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document understanding&lt;/strong&gt;: OCR, chart analysis, and structured data extraction from images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise applications&lt;/strong&gt;: It scores 69 on Tau2 Bench Telecom and 69 on IFBench (key benchmarks for enterprise domains)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function calling and tool use&lt;/strong&gt;: The simplified chat template and special tokens make it easier to integrate with agentic systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource-constrained deployments&lt;/strong&gt;: At 15B parameters, it fits on a single GPU while delivering frontier-level performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why It's Important
&lt;/h3&gt;

&lt;p&gt;Apriel 1.6 represents a crucial evolution in how we think about multimodal AI. Most multimodal models are either massive (100B+ parameters) or sacrifice significant capability to stay small. ServiceNow has found a middle ground that makes advanced vision-language capabilities accessible.&lt;/p&gt;

&lt;p&gt;The training approach is also noteworthy. Trained on NVIDIA's GB200 Grace Blackwell Superchips, the entire mid-training pipeline required approximately 10,000 GPU hours, a relatively small compute footprint achieved through careful data strategy and training methodology. This efficiency-first mindset shows that throwing more compute at the problem isn't always the answer.&lt;/p&gt;

&lt;p&gt;For developers building enterprise AI applications, Apriel 1.6 offers something unique: production-ready multimodal reasoning that actually fits in a reasonable memory budget. The focus on enterprise benchmarks and tool calling also makes it particularly well-suited for real-world business applications rather than just benchmark chasing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;What ties these three models together isn't just that they're flying under the radar, it's what they represent about where AI development is heading. We're moving away from the "bigger is always better" mentality toward a more nuanced understanding of efficiency, architecture, and targeted optimization.&lt;/p&gt;

&lt;p&gt;Falcon H1R 7B shows that hybrid architectures can achieve remarkable results with fewer parameters. Nemotron Nano demonstrates that sparse activation through MoE can give us the best of both worlds, large model intelligence with small model efficiency. Apriel 1.6 proves that multimodal capabilities don't require massive models if you're thoughtful about training and optimization.&lt;/p&gt;

&lt;p&gt;All three of these models are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully open and available for local deployment&lt;/li&gt;
&lt;li&gt;Designed with efficiency as a first-class concern&lt;/li&gt;
&lt;li&gt;Backed by transparent research and training methodologies&lt;/li&gt;
&lt;li&gt;Focused on practical, real-world use cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those of us building AI-powered applications, especially in environments where we can't just throw unlimited compute resources at every problem, these models matter. They represent a future where advanced AI capabilities are accessible to anyone with modest hardware, not just those with access to massive GPU clusters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want to try these models yourself all three can be run locally using tools like llama.cpp, Ollama, or vLLM. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Falcon H1R 7B&lt;/strong&gt;: Available &lt;a href="https://huggingface.co/blog/tiiuae/falcon-h1r-7b" rel="noopener noreferrer"&gt;on Hugging Face&lt;/a&gt; and Ollama (&lt;code&gt;ollama pull falcon:7b&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NVIDIA Nemotron Nano 8B/30B&lt;/strong&gt;: Available &lt;a href="https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16" rel="noopener noreferrer"&gt;on Hugging Face&lt;/a&gt;, through NVIDIA's NIM platform, and Ollama (&lt;code&gt;ollama pull nemotron-3-nano:30b&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apriel 1.6 15B Thinker&lt;/strong&gt;: Available &lt;a href="https://huggingface.co/blog/ServiceNow-AI/apriel-1p6-15b-thinker" rel="noopener noreferrer"&gt;on Hugging Face&lt;/a&gt; and hosted on platforms like Together AI and Ollama (&lt;code&gt;ollama pull ServiceNow-AI/Apriel-1.6-15b-Thinker:Q4_K_M&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The AI landscape moves fast so it's easy to get caught up in the hype around the latest massive model releases. But some of the most interesting innovation is happening in the efficiency space, building models that are genuinely useful for practitioners who don't have access to unlimited compute resources. Falcon H1R 7B, NVIDIA Nemotron Nano, and Apriel 1.6 15B Thinker deserve more attention than they're getting. If you've been thinking about integrating AI into your projects but have been put off by the resource requirements of larger models, these three are worth a serious look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links and Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Technology Innovation Institute's &lt;a href="https://falconllm.tii.ae/falcon-h1r-7b.html" rel="noopener noreferrer"&gt;Falcon H1R 7B&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;NVIDEA's Nemotron Nano &lt;a href="https://huggingface.co/nvidia/Llama-3.1-Nemotron-Nano-8B-v1" rel="noopener noreferrer"&gt;8b&lt;/a&gt; and &lt;a href="https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16" rel="noopener noreferrer"&gt;30b&lt;/a&gt; and its &lt;a href="https://research.nvidia.com/labs/nemotron/files/NVIDIA-Nemotron-3-Nano-Technical-Report.pdf" rel="noopener noreferrer"&gt;technical paper&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ServiceNow's &lt;a href="https://huggingface.co/ServiceNow-AI/Apriel-1.6-15b-Thinker" rel="noopener noreferrer"&gt;Apriel 1.6 15b Thinker&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>Terraform Module MCP Server</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Fri, 31 Oct 2025 01:07:55 +0000</pubDate>
      <link>https://dev.to/zloeber/terraform-module-mcp-server-m1p</link>
      <guid>https://dev.to/zloeber/terraform-module-mcp-server-m1p</guid>
      <description>&lt;p&gt;I created an MCP server that streamlines access to custom Terraform modules that I'd like to share with the community. The project called &lt;a href="https://github.com/zloeber/terraform-ingest" rel="noopener noreferrer"&gt;terraform-ingest&lt;/a&gt; is a CLI, MCP, and API tool that can be used locally or with an AI agent to tap into your existing code base more effectively.&lt;/p&gt;




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

&lt;p&gt;I looked high and low for a model context protocol server I could use as an interface to the several dozen custom Terraform modules I've created. My search was futile so I spent a few cycles and just made my own. With &lt;a href="https://github.com/zloeber/terraform-ingest" rel="noopener noreferrer"&gt;this solution&lt;/a&gt; you can use a simple YAML file to define all your custom Terraform modules git sources. These are then ingested to extract and index all relevant information for embedding into a vector database. From here you can use this tool as a CLI, API, or MCP server to find  modules that best suit your needs and weave them together to create organizational compliant infrastructure as code!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/docs" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is the wildly popular interface for AI agentic workloads. I've written about how to use existing MCP Servers to augment AI for vast improvements in its ability to &lt;a href="https://blog.zacharyloeber.com/article/cagent-terraformer-rewrite/" rel="noopener noreferrer"&gt;clean up crappy&lt;/a&gt; and even &lt;a href="https://blog.zacharyloeber.com/article/create-terraform-with-ai-and-github-copilot/" rel="noopener noreferrer"&gt;author new&lt;/a&gt; Terraform. This works quite well for non-modular code. But it lacks the ability to interface with the many custom modules I've personally created for the teams I've worked with. And while I've created &lt;a href="https://github.com/metagit-ai/metagit-cli" rel="noopener noreferrer"&gt;tools&lt;/a&gt; to help manage multi-git project repositories as a single virtual monorepo it still is hard, even for me, to consistently know the right variables and outputs for the modules needed for any given solution. What we all need is an MCP server that better aggregates several dozen terraform modules (both as single projects or recursively as monorepo projects) into one managed source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" rel="noopener noreferrer"&gt;RAG&lt;/a&gt; ingestion pipeline for Terraform modules is how I envisioned a solution for these issues. It makes sense to pull in and ingest key information about each targeted module, interested versions, providers, and target paths. We can accomplish this with some simple git cloning, hcl parsing, and then finally vectordb embedding of the generated json that summarizes each module for similarity searching.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Was Built
&lt;/h2&gt;

&lt;p&gt;In this case the solution was built in a few rounds using AI for heavy lifting and scaffolding in this general order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a YAML definition and click cli interface with FastAPI server to allow for the ingestion of one or more git repos of terraform modules into a local json file, one per-target git branch or tag.&lt;/li&gt;
&lt;li&gt;Add a FastMCP interface for searching through the results.&lt;/li&gt;
&lt;li&gt;Add the embedding of this data into a local vector database using an embedding method of your choice (starting with chromadb and local but less precise embedding but including ollama or external embedding if so desired).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In between these steps at various points I added in the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-stage docker images&lt;/li&gt;
&lt;li&gt;A bunch of gitlab pipelines, publishing to Pypi, better unit tests, semantic releases, et cetera&lt;/li&gt;
&lt;li&gt;hatch-vcs for build and automatic versioning&lt;/li&gt;
&lt;li&gt;mkdocs based static website generation for docs&lt;/li&gt;
&lt;li&gt;Minor refactors from what got generated for me to be more cohesive to the way I like to have my Python apps&lt;/li&gt;
&lt;li&gt;Additional MCP dynamic resources and prompt template generation&lt;/li&gt;
&lt;li&gt;Lazy-loading of bulky dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/zloeber/terraform-ingest-example" rel="noopener noreferrer"&gt;This project&lt;/a&gt; includes a practical configuration example that encompasses all the modules for a team of Terraform professionals I happen to hold in the highest regards, &lt;a href="https://github.com/cloudposse/" rel="noopener noreferrer"&gt;Cloudposse&lt;/a&gt;. They have produced almost 200 high-quality open source AWS terraform modules. You can look at this example project to get more information on how to generate and then ingest such a configuration. The configuration file has hundreds of entries like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-access-analyzer&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/cloudposse-terraform-components/aws-access-analyzer.git&lt;/span&gt;
  &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
  &lt;span class="na"&gt;include_tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;max_tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src&lt;/span&gt;
  &lt;span class="na"&gt;exclude_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have ingested the modules to a local cache and embedded to a vector database it is easy to search without any AI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Retrieve the top 5 similar results using the vectordb search&lt;/span&gt;
terraform-ingest search &lt;span class="s2"&gt;"API Gateway Lambda integration"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; 5

&lt;span class="c"&gt;# Get all the details about the top search result for 'vpc module for aws' (requires jq)&lt;/span&gt;
terraform-ingest search &lt;span class="s2"&gt;"vpc module for aws"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; 1 &lt;span class="nt"&gt;-j&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; ./.vscode/cloudposse.yaml | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.results[0].id'&lt;/span&gt; | xargs &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; terraform-ingest index get &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cli also includes the ability to directly call exposed MCP tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform-ingest &lt;span class="k"&gt;function &lt;/span&gt;&lt;span class="nb"&gt;exec &lt;/span&gt;get_module_details &lt;span class="nt"&gt;--arg&lt;/span&gt; repository &lt;span class="s2"&gt;"https://github.com/cloudposse-terraform-components/aws-api-gateway-rest-api.git"&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; ref &lt;span class="s2"&gt;"v1.535.3"&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; path &lt;span class="s2"&gt;"src"&lt;/span&gt; &lt;span class="nt"&gt;--output-dir&lt;/span&gt; ./output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;p&gt;Some possible use cases include (but are not limited to):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On-demand module documentation and example generation&lt;/li&gt;
&lt;li&gt;Query your authored modules via any LLM&lt;/li&gt;
&lt;li&gt;Module upgrade planning and risk analysis&lt;/li&gt;
&lt;li&gt;Greenfield deployment using your own organization's modules&lt;/li&gt;
&lt;li&gt;Running a self-updating internal MCP server for inline validation of module use via any internal agent&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I learned a few things about developing an MCP server for RAG that are worth mentioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  STDIO Is Goofy
&lt;/h3&gt;

&lt;p&gt;This is the default mode many run local MCP servers in. The name says it all, it uses stdio output streams to communicate with the server. As such, you want to prevent superfluous output to the console when running in this mode otherwise MCP clients will get confused and start having JSON serialization errors.&lt;/p&gt;

&lt;p&gt;STDIO mode is just a local process that gets kicked off on behalf of the MCP client. If you want to speed things along you probably don't want to be running long running imports or embeddings when you start your app. Including a means of pre-processing long running workflows helps get around this. In my case I include a cli that can be used to do the needful for terraform-ingest before starting the MCP services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Embedding Is Heavy
&lt;/h3&gt;

&lt;p&gt;Using local vector databases and embeddings is really nice for development. But they are also quite large dependencies that can add multiple GB to your distribution. This makes for large Docker images and pypi downloads. More importantly, it really really drags out your CICD pipelines and I'm exceedingly impatient when it comes to long running pipelines.&lt;/p&gt;

&lt;p&gt;To get around this we lazy-load in models and dependencies if they are needed. My code architecture now looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│  User Interface Layer                               │
│  ┌──────────────┬──────────────┬─────────────────┐  │
│  │     CLI      │     API      │  Programmatic   │  │
│  └──────────────┴──────────────┴─────────────────┘  │
│         ↓           ↓              ↓                │
├─────────────────────────────────────────────────────┤
│  TerraformIngest                                    │
│  ├─ auto_install_deps parameter                     │
│  └─ calls ensure_embeddings_available()             │
├─────────────────────────────────────────────────────┤
│  dependency_installer.py                            │
│  ├─ DependencyInstaller class                       │
│  │  ├─ check_package_installed()                    │
│  │  ├─ get_missing_packages()                       │
│  │  ├─ install_packages()                           │
│  │  └─ ensure_embedding_packages()                  │
│  └─ ensure_embeddings_available() function          │
├─────────────────────────────────────────────────────┤
│  embeddings.py                                      │
│  └─ VectorDBManager (no changes, already works)     │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING&lt;/strong&gt; I usually do not recommend this approach for production use as it has security ramifications and defies best practices for artifact immutability. If you are repackaging this server up to use in production bake a new image with the appropriate embedding models in place. Then let me know as well cause that would definitely fill my bucket a bit!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I'm pretty happy with the results of this little project and plan on configuring it for any large set of modules I author. I've included some other nice features like automatic updating, automatic import of a github organization, and indexing. I could easily see updating the caching to be more efficient and adding more/better searching but even as it is currently the MCP server is quite functional and I encourage the community to put it through its paces so it can be further improved!&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>terraform</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Terraform with AI and Github Copilot</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Tue, 21 Oct 2025 02:32:56 +0000</pubDate>
      <link>https://dev.to/zloeber/terraform-with-ai-and-github-copilot-2n39</link>
      <guid>https://dev.to/zloeber/terraform-with-ai-and-github-copilot-2n39</guid>
      <description>&lt;p&gt;Creating terraform or other infrastructure as code for a new project can be daunting for some. This shows how you can easily crank out a new deployment to meet your requirements using Github copilot prompt files and a few free MCP servers. For the heck of it, we will also convert between two totally different cloud providers to deploy the same infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Github copilot is getting more powerful with each update and I've been enjoying using it quite a bit to write quick scripts and even initializing whole project repositories for me. But I've never been very impressed with it (or any other LLM's) ability to create solid terraform. I've been exploring model context protocol (MCP) servers quite a bit lately and figured perhaps they can augment an agent with enough additional capabilities to upset me less with their terraform tasks. Turns out that providing Copilot with the right tools can really amplify its results!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I'm using Github Copilot in VSCode along with several MCP servers for this exercise. You can setup a project locally with MCP servers easily enough by creating a file named &lt;code&gt;./.vscode/mcp.json&lt;/code&gt; in your project. Here is what mine looks like:&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;"servers"&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;"sequential-thinking"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@modelcontextprotocol/server-sequential-thinking"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&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;"server-filesystem"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"@modelcontextprotocol/server-filesystem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&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;"terraform"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"hashicorp/terraform-mcp-server"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&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;"mcp-feedback-enhanced"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp-feedback-enhanced@latest"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aws-knowledge"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://knowledge-mcp.global.api.aws"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&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;"azure-knowledge"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://learn.microsoft.com/api/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&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;"inputs"&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;These are the MCP servers used:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sequential-thinking&lt;/td&gt;
&lt;td&gt;Very popular MCP for helping an LLM organize its thoughts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;server-filesystem&lt;/td&gt;
&lt;td&gt;Reading/writing to the filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terraform&lt;/td&gt;
&lt;td&gt;Terraform best practices and provider documentation lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp-feedback-enhanced&lt;/td&gt;
&lt;td&gt;(Optional) User feedback forms for more interactive data gathering from the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws-knowledge&lt;/td&gt;
&lt;td&gt;Official AWS online knowledge datastore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;azure-knowledge&lt;/td&gt;
&lt;td&gt;Official Azure online knowledge datastore&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I don't let my MCP servers always run. If you want to start them you can open up the mcp.json file in the editor and above each of the definitions there is a little start button you can click on to get it going.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; mcp-feedback-enhanced is optional because I believe copilot will handle interfacing with you on questions just fine. But I recognize that I personally am not always going to be using Copilot for my solutions and wanted to use a less vendor locked solution. I'm also simply interested in human-in-the-loop MCP servers and this one was the best of three I tested out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Prompts
&lt;/h2&gt;

&lt;p&gt;To create a re-usable interface you can use &lt;a href="https://docs.github.com/en/copilot/tutorials/customization-library/prompt-files/your-first-prompt-file" rel="noopener noreferrer"&gt;Github Copilot prompt files&lt;/a&gt; in your project by creating them in the &lt;code&gt;./.github/prompts/&lt;/code&gt; folder with a name like &lt;code&gt;*.prompt.md&lt;/code&gt;. Once created you can kick them off at anytime in the Copilot agent chat window with a &lt;code&gt;/&amp;lt;prompt&amp;gt;&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Here is one I created to walk a user through creating an AWS terraform deployment from scratch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
mode: 'agent'
description: 'Create AWS terraform code for given requirements with interactive feedback.'
---

Create AWS Terraform code for the following requirements with step-by-step reasoning and interactive feedback:

Requirements: ${input:requirements:What infrastructure do you need? Please be as detailed as possible.}

Use the interactive_feedback tool to gather any additional necessary information from the user to refine their requirements.

Use the aws-knowledge MCP tool to ensure accuracy and best practices in AWS services and Terraform code.
Use the terraform-mcp-server tool to generate the Terraform code to meet the refined requirements for AWS infrastructure.
Output the final Terraform code only after confirming all requirements with the user, including any refinements made through interactive feedback.
Include a markdown file with all the requirements gathered along with any you have inferred along with the final Terraform code.
Refine all infrastructure requirements to be AWS-specific and aligned with best practices, security, and compliance standards. Be thorough and detailed in your analysis.
If you need to gather more information from the user to refine the requirements, use the interactive_feedback tool to ask clarifying questions before generating the code.

Rules:
    - Terraform should be written using HCL (HashiCorp Configuration Language) syntax.
    - Use the latest AWS provider version compatible with the required resources.
    - Follow best practices for Terraform code structure, including the use of variables, outputs, and modules.
    - Ensure that the generated code is well-documented with comments explaining the purpose of each resource and configuration.
    - Always try to use implicit dependencies over explicit dependencies where possible in Terraform.
    - When generating Terraform resource names, ensure they are unique and descriptive, lower-case, and snake_case.
    - Be sure to include any necessary provider configurations, backend settings, and required variables in the generated code.
    - Ensure the generated terraform code always includes a top level `tag` variable map that is used on all taggable resources, with at least the following tags: `Environment`, `Project`, and `Owner`.
    - Ensure that sensitive information such as passwords, API keys, and secrets are not hardcoded in the Terraform code. Use variables and secret management solutions instead.
    - Do not assume any prior knowledge about the user's AWS environment; always seek clarification when in doubt.
    - Do not ask for AWS specific information like instance types, instead focus on high level requirements and attempt to map them to AWS services for the user.
    - Before finalizing the Terraform code, always confirm with the user that all requirements have been accurately captured and addressed.
    - All output should be created in the `output/aws/` directory with appropriate filenames.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And one for Azure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
mode: 'agent'
description: 'Create azure terraform code for given requirements with interactive feedback.'
---

Create Azure Terraform code for the following requirements with step-by-step reasoning and interactive feedback:

Requirements: ${input:requirements:What infrastructure do you need? Please be as detailed as possible.}

Use the interactive_feedback tool to gather any additional necessary information from the user to refine their requirements.

Use the azure-knowledge MCP tool to ensure accuracy and best practices in Azure services.
Use the terraform-mcp-server tool to generate the Terraform code to meet the refined requirements for Azure infrastructure.
Output the final Terraform code only after confirming all requirements with the user, including any refinements made through interactive feedback.
Include a markdown file with all the requirements gathered along with any you have inferred along with the final Terraform code.
Refine all infrastructure requirements to be Azure-specific and aligned with best practices, security, and compliance standards. Be thorough and detailed in your analysis.
If you need to gather more information from the user to refine the requirements, use the interactive_feedback tool to ask clarifying questions before generating the code.

Rules:
    - Terraform should be written using HCL (HashiCorp Configuration Language) syntax.
    - Use the latest Azure provider version compatible with the required resources.
    - Follow best practices for Terraform code structure, including the use of variables, outputs, and modules.
    - Ensure that the generated code is well-documented with comments explaining the purpose of each resource and configuration.
    - Always try to use implicit dependencies over explicit dependencies where possible in Terraform.
    - When generating Terraform resource names, ensure they are unique and descriptive, lower-case, and snake_case.
    - Be sure to include any necessary provider configurations, backend settings, and required variables in the generated code.
    - Ensure the generated terraform code always includes a top level `tag` variable map that is used on all taggable resources, with at least the following tags: `Environment`, `Project`, and `Owner`.
    - Ensure that sensitive information such as passwords, API keys, and secrets are not hardcoded in the Terraform code. Use variables and secret management solutions instead.
    - Do not assume any prior knowledge about the user's Azure environment; always seek clarification when in doubt.
    - Do not ask for Azure specific information like instance types, instead focus on high level requirements and attempt to map them to Azure services for the user.
    - Before finalizing the Terraform code, always confirm with the user that all requirements have been accurately captured and addressed.
    - All output should be created in the `output/azure/` directory with appropriate filenames.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are ready to bootstrap either an AWS or Azure terraform project via Copilot go ahead and do so using the prompt. This starts the process for an Azure based terraform project &lt;code&gt;/terraform-azure-bootstrap&lt;/code&gt;. It will start by asking what you want then ask you refining questions to figure out what needs to be created. You do not need to close the feedback window that comes up, it will automatically be reused and refresh its contents when it needs further information or approval from you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Prompts
&lt;/h2&gt;

&lt;p&gt;For they heck of it I also created a few more prompts that can be used to convert a terraform project from Azure to AWS and vice versa. These use the same MCP servers but with different prompts. I'll let you look at the examples I constructed for each in the &lt;a href="https://github.com/zloeber/terraform-copilot-prompts" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt; for this exercise. I created two fictitious projects off the top of my head, one for AWS and another for Azure. I then used the conversion prompt for each to create the equivalent project for the other cloud provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pleasant Surprises
&lt;/h2&gt;

&lt;p&gt;When it works the way I want AI can be extremely satisfying to wield. This is even more so when it yields more than what you asked for. In this case I found that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the managed kubernetes deployment it generated functioning &lt;code&gt;Makefile&lt;/code&gt;s with a plethora of commands that are useful to the deployment.&lt;/li&gt;
&lt;li&gt;The terraform conversion between one provider and another included cost comparisons between the two deployments.&lt;/li&gt;
&lt;li&gt;The feedback tool used can remain open and be used for all prompts back and forth between the agent.&lt;/li&gt;
&lt;li&gt;The requirements.md generated is quite comprehensive and additive to the deployment for user comprehension.&lt;/li&gt;
&lt;li&gt;Both the AWS and Azure MCP servers were easily used by the Agent with very little extra prompting.&lt;/li&gt;
&lt;li&gt;For the virtual machines I put in some rather complex logic for how I wanted the disks done and was surprised to find that the appropriate &lt;code&gt;user-data.sh&lt;/code&gt; bash script for AWS and &lt;code&gt;cloud-init.yml&lt;/code&gt; file for AWS was created for me not only with the disks done as I had requested (LVM and mounted to &lt;code&gt;/opt&lt;/code&gt;) but much more. For instance, it also generated a pretty decent nginx deployment for wordpress, test scripts for cloud storage access (that I purposefully included as requirement to try to trip things up), and cloud specific agent installs for disk and memory monitoring. Pretty slick!&lt;/li&gt;
&lt;li&gt;There was a corpus of additional documentation included with both example deployments that included a good deal of extra info that I might personally include in a project were I delivering it to a team to manage. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Irksome Things
&lt;/h2&gt;

&lt;p&gt;The results are not all positive. I have a few minor gripes as well.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An abundance of emojis while visually pretty to see just screams LLM generated to the trained eye. Can probably reduce their use with minor prompt adjustments.&lt;/li&gt;
&lt;li&gt;The nondeterministic nature of LLMs means results for documentation were wildly different between each project. I specifically requested requirements.md be generated in the bootstrap process but forgot to say anything about it in the migration prompts. The first example I migrated from AWS to Azure left the file mostly in tact. The second example migration from Azure to AWS created a 500+ line operational guide out of it (which was cool and all, but still makes my point here).&lt;/li&gt;
&lt;li&gt;As mentioned before this can chew through your premium tokens pretty quickly depending on your requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So would I use any of this terraform without reviewing it first? Of course not. Heck, it probably wouldn't even run without some modifications. But I certainly would use it to get things started for a project. It produces quite clean and easy to read terraform with the correct naming conventions, variables, and documentation to get things off to a very nice start. I will not use it to scaffold out every project I do though. This is mainly because it does seem to burn through premium tokens which I'd rather use for more complex work. I'm on a standard plan and creating the 4 examples you can find in the project repository ate almost 10% of my premium tokens.&lt;/p&gt;

&lt;p&gt;This combo of MCP servers is quite good at overcoming some of AI's issues with building proper terraform as well. I'm quite happy that this is the case as repeated bizarre LLM results on terraform generation was starting to get upsetting. Next up, an MCP server that will allow you to use your own organizational modules. I'm hoping to have such a tool ready to test out next month sometime (if anyone already has one please reach out to me so I can collaborate with ya!).&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>ai</category>
      <category>githubcopilot</category>
      <category>agents</category>
    </item>
    <item>
      <title>AI Lessons Learned</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Wed, 01 Oct 2025 19:16:15 +0000</pubDate>
      <link>https://dev.to/zloeber/ai-lessons-learned-3k6c</link>
      <guid>https://dev.to/zloeber/ai-lessons-learned-3k6c</guid>
      <description>&lt;p&gt;I've got a tendency to take tools and frameworks in IT and immediately push them to their limits and beyond. Sadly, this often lands me into the trough of disillusionment quite quickly when exploring any new technology. On the flip side, it is through this process I often learn some great lessons. This article will cover lessons learned as it pertains to AI in an effort to help shortcut some of those that are starting to dive further into this incredible new world we are entering with AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context Management Is Key
&lt;/h2&gt;

&lt;p&gt;Context is the length of your prompt in its entirety. This includes any conversation history, custom instructions, additional rules, available tool instructions, RAG results, and verbalized reasoning output. This adds up quickly in smaller local models and needs to be factored into your overall context management strategy. One decent strategy is to look into multi-agent frameworks where each agent has its own unit of context. It is quite easy to cram all your needs into a single agent because you as a human could do a single workflow from end to end. But if you give it just a bit more thought and logically break things out into sub units of work for various sub-agents it will be less likely you run into context limit issues.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; Is your agent reading in several dozen files from the filesystem? This is one area where you can easily blow up your context if not thought out carefully!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Sometimes a Dumber AI is Better
&lt;/h2&gt;

&lt;p&gt;Many LLM models include reasoning or thinking modes of operation that you may reflexively want to use. Why wouldn't you want your LLM to be a bit more thoughtful in how it responds right? I can give you a few reasons you may want to dial back the deep thoughts on these things. Firstly, it can cause token bloat which directly equates to additional cost and latency. Secondly, not all LLMs separate out the thoughts from the output the same. Ollama will inline the thoughts with standard responses in tags like &lt;code&gt;&amp;lt;thought&amp;gt;&amp;lt;/thought&amp;gt;&lt;/code&gt;. This can be a bit of a bummer to deal with in some applications. While it can be fascinating to read how they are thinking through a process it can really pollute output if not handled properly. Third, I've experienced that including thinking in my requests sometimes led to worse results overall. This is only my anecdotal observations but I believe some models maybe overthink some simpler tasks or in the case of multi-agent interactions, simply confuse agents reading the reasoning output responses of other sub-agents.&lt;/p&gt;

&lt;p&gt;If you are employing a multi-agent workflow I'd consider only allowing for the orchestrator/master agents to process with additional thinking models. Or if that is not suitable, just enable thinking selectively and just make a bunch of purpose driven sub-agents that can be a bit dumber.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Is Sweet But Fickle
&lt;/h2&gt;

&lt;p&gt;I've run into several issues with MCP tools that were driving me crazy. There are some great MCP inspection tools but in a pinch you can also simply ask the LLM to give you a report of available tools that have been exposed to it. Here is a &lt;a href="https://github.com/docker/cagent" rel="noopener noreferrer"&gt;cagent&lt;/a&gt; definition I put together that does this for a local ollama model I was testing out with some tools I was tinkering with on my local workstation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env cagent run&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;

&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;thismodel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-oss&lt;/span&gt;
    &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:11434/v1&lt;/span&gt;
    &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16000&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;

&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;thismodel&lt;/span&gt;
    &lt;span class="na"&gt;add_date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Creates&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;documentation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;available&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agent"&lt;/span&gt;
    &lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;You are evaluating the functionality of various tools available to you as an AI agent.&lt;/span&gt;
      &lt;span class="s"&gt;Your goal is to generate a comprehensive report on the functionality of any tools that you can use to assist you in your tasks.&lt;/span&gt;
      &lt;span class="s"&gt;You will use the filesystem tool to read and write your final report in markdown format as a .md file with a name like tool-report-&amp;lt;date&amp;gt;.md.&lt;/span&gt;
      &lt;span class="s"&gt;No other tools are to be used by you directly but you can query the list of tools available to you.&lt;/span&gt;
      &lt;span class="s"&gt;You will instead generate a list of all the tools you can use and their functionality.&lt;/span&gt;
    &lt;span class="na"&gt;toolsets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filesystem&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;think&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;memory&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-mcp-server&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stdio"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-searxng"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SEARXNG_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Model Selection Is Hard
&lt;/h2&gt;

&lt;p&gt;There are just so many models out there to choose from. It would be easy to think that local models are good enough but honestly, no they are not. Aside from their smaller context length there is no standard way to really even look them up. This makes finding effective context length and max token counts a chore at best. Ollama has their own online catalog (api for it is forthcoming I've read) and there are some other minor lifelines such as &lt;a href="https://github.com/BerriAI/litellm/blob/main/litellm/model_prices_and_context_window_backup.json" rel="noopener noreferrer"&gt;this gem&lt;/a&gt; buried in the LiteLLM repo. This is just the hard details of the models, not their numerous scores, capabilities, and more. OpenRouter.ai has &lt;a href="https://openrouter.ai/docs/api-reference/list-available-models" rel="noopener noreferrer"&gt;an api endpoint&lt;/a&gt; that makes searching for some of this a bit easier for models it supports.&lt;/p&gt;

&lt;p&gt;This is all only for language models by the way. Additional servers and consideration for their use come to play for image, video, or audio generation. So if you are planning on doing something multi-model then the efforts begin to stack up rather quickly.&lt;/p&gt;

&lt;p&gt;This all said, often simply choosing a decent frontier model is the fastest and easiest way to go. Grok for more recent research is nice, Claude for coding is a good bet, OpenAI if you want to fit in with the broadest ecosystem of tools and community support. &lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Forget Embedding Models
&lt;/h2&gt;

&lt;p&gt;Let's not forget both RAG and (most) memory related tasks require &lt;a href="https://ollama.com/blog/embedding-models" rel="noopener noreferrer"&gt;embedding models&lt;/a&gt;. In (most) cases this will require some vector database which means you will need to encode your data into vectors via an embedding model. These are smaller and purpose driven to convert your language (or code AST blocks, or &lt;code&gt;&amp;lt;some other esoteric data&amp;gt;&lt;/code&gt;) into embedded similarity vectors. If you are doing local RAG for privacy then you will need a local embedding model and vector database to target. I've been using ollama and one of a few models it offers for embedding with qdrant as my local vector store as it has a nice little UI I can use to further explore vectorized data. Towards the end of this article I'll include a docker compose that will bring up this vector database quite easily.&lt;/p&gt;

&lt;p&gt;If you are embedding RAG data you will still often need to get it into an embedding model friendly format. I've taken a liking to &lt;a href="https://github.com/datalab-to/marker" rel="noopener noreferrer"&gt;marker&lt;/a&gt; for this task to process PDFs and other document formats. Once installed you can process a single document against a local ollama model to create a markdown file quite easily &lt;code&gt;marker_single --llm_service=marker.services.ollama.OllamaService --ollama_base_url=http://localhost:11434 --ollama_model=gpt-oss ./some.pdf&lt;/code&gt;. There are so many options for marker that I think the author must be partially insane (in a good way, I dig it) so check it out if you get a few free cycles. The project is impressive in its scope.&lt;/p&gt;

&lt;p&gt;Back to embedding models. There are several local ones you can choose from. Here are a few of the most popular open source ones as generated via AI.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model Name&lt;/th&gt;
&lt;th&gt;Dimensions&lt;/th&gt;
&lt;th&gt;Max Input Tokens&lt;/th&gt;
&lt;th&gt;Perf. (MTEB/Accuracy Score)&lt;/th&gt;
&lt;th&gt;Multilingual Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mistral-embed&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;8000&lt;/td&gt;
&lt;td&gt;77.8% (highest in benchmarks)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nomic-embed-text&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;8192&lt;/td&gt;
&lt;td&gt;High (state-of-the-art)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mxbai-embed-large&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;High (state-of-the-art)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EmbeddingGemma&lt;/td&gt;
&lt;td&gt;N/A (small model)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;High (best under 500M params)&lt;/td&gt;
&lt;td&gt;Yes (100+ languages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 8B Embedding&lt;/td&gt;
&lt;td&gt;N/A (8B params)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;70.58 (top in multilingual)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some additional notes on each model as well:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model Name&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mistral-embed&lt;/td&gt;
&lt;td&gt;Strong semantic understanding; open weights available on Hugging Face.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nomic-embed-text&lt;/td&gt;
&lt;td&gt;Offline-capable via Ollama; privacy-focused for local deployments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mxbai-embed-large&lt;/td&gt;
&lt;td&gt;Efficient open-source option; available via Ollama or Hugging Face.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EmbeddingGemma&lt;/td&gt;
&lt;td&gt;Mobile-ready; Matryoshka learning; ideal for edge devices or fine-tuning.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3 8B Embedding&lt;/td&gt;
&lt;td&gt;Excels in diverse topics; Apache 2.0 license for customization.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is a simple diagram of choices to make for selecting one of the free embedding models for your own projects. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wh0shekjza2nqc9wfww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9wh0shekjza2nqc9wfww.png" alt="Choosing a local embedding model" width="800" height="731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Matryoshka Support?&lt;/strong&gt; This was new to me when writing this article. A model that supports this might embed a chunk of data with 1024k dimensions to query for similarity against but be trained to surface the most important ones into the top 256 or 512 dimensions. This allows for the embeddings to capture most of the semantic meaning with a slight loss of precision if truncated compared to the full vector. Pretty nifty as it allows single models to generate multi-dimension embeddings. This is inspired by the concept of Matryoshka dolls, where smaller dolls nest within larger ones, and is formally known as Matryoshka Representation Learning (MRL).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Web Search Without Limits/Keys
&lt;/h2&gt;

&lt;p&gt;When you start to develop AI agents to do things one of the first activities will be to search the web for content then scrape it. This seems like a very innocuous task as it is something you might do every day without thought. But doing so automatically as an agent often requires some form of API key with an outside service (like Serper or any number of a dozen others) or through a free but highly rate limited target such as duckduckgo.&lt;/p&gt;

&lt;p&gt;With MCP and a local &lt;a href="https://github.com/searxng/searxng" rel="noopener noreferrer"&gt;SearXNG&lt;/a&gt; instance you can get around this snafu fairly easily. It is a local running search aggregator. Remember dogpile.com? SearXNG is kinda like that but locally hosted and more expansive in scope. You need only expose it to your agents using a local MCP server and they can search and scrape the web freely. I've included it in this docker compose file for your convenience (along with the valkey caching integration). This compose file is self-contained. All configuration can be done via the config blocks at the bottom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Exposes the following services:&lt;/span&gt;
&lt;span class="c1"&gt;# - http://localhost:6333/dashboard - qdrant (ui)&lt;/span&gt;
&lt;span class="c1"&gt;# - http://localhost:8080 - searxng (ui)&lt;/span&gt;
&lt;span class="c1"&gt;# - valkey (internal, for searxng)&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;valkey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;valkey&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/valkey/valkey:8-alpine&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;valkey-server --save 30 1 --loglevel warning&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;valkey-data2:/data&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json-file"&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1m"&lt;/span&gt;
        &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valkey-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;searxng&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;searxng&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/searxng/searxng:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;searxng-data:/var/cache/searxng:rw&lt;/span&gt;
    &lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;searxng_limiter_config&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/searxng/limiter.toml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;searxng_config&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/searxng/settings.yml&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SEARXNG_BASE_URL=https://${SEARXNG_HOSTNAME:-localhost}/&lt;/span&gt;
    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json-file"&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1m"&lt;/span&gt;
        &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;valkey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--no-verbose"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--tries=1"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--spider"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;

  &lt;span class="na"&gt;qdrant&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qdrant/qdrant:latest&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qdrant&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;6333:6333&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;6334:6334&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6333&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6334&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6335&lt;/span&gt;
    &lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qdrant_config&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/qdrant/config/production.yaml&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/qdrant:/qdrant/storage&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&amp;lt;&amp;gt;/dev/tcp/127.0.0.1/6333&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-e&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'GET&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/readyz&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;nHost:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;nConnection:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;close&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;n'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&amp;amp;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;grep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'HTTP/1.1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;200'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;amp;3"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;valkey-data2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;searxng-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;searxng_limiter_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;# This configuration file updates the default configuration file&lt;/span&gt;
      &lt;span class="s"&gt;# See https://github.com/searxng/searxng/blob/master/searx/limiter.toml&lt;/span&gt;

      &lt;span class="s"&gt;[botdetection.ip_limit]&lt;/span&gt;
      &lt;span class="s"&gt;# activate advanced bot protection&lt;/span&gt;
      &lt;span class="s"&gt;# enable this when running the instance for a public usage on the internet&lt;/span&gt;
      &lt;span class="s"&gt;link_token = false&lt;/span&gt;
  &lt;span class="na"&gt;searxng_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;# see https://docs.searxng.org/admin/settings/settings.html#settings-use-default-settings&lt;/span&gt;
      &lt;span class="s"&gt;use_default_settings: true&lt;/span&gt;
        &lt;span class="s"&gt;# engines:&lt;/span&gt;
        &lt;span class="s"&gt;#   keep_only:&lt;/span&gt;
        &lt;span class="s"&gt;#     - google&lt;/span&gt;
        &lt;span class="s"&gt;#     - duckduckgo&lt;/span&gt;
      &lt;span class="s"&gt;server:&lt;/span&gt;
        &lt;span class="s"&gt;# base_url is defined in the SEARXNG_BASE_URL environment variable, see .env and docker-compose.yml&lt;/span&gt;
        &lt;span class="s"&gt;secret_key: "some_secret_key123"  # change this!&lt;/span&gt;
        &lt;span class="s"&gt;limiter: false  # enable this when running the instance for a public usage on the internet&lt;/span&gt;
        &lt;span class="s"&gt;image_proxy: true&lt;/span&gt;
      &lt;span class="s"&gt;search:&lt;/span&gt;
        &lt;span class="s"&gt;formats:&lt;/span&gt;
          &lt;span class="s"&gt;- html&lt;/span&gt;
          &lt;span class="s"&gt;- csv&lt;/span&gt;
          &lt;span class="s"&gt;- rss&lt;/span&gt;
          &lt;span class="s"&gt;- json&lt;/span&gt;
      &lt;span class="s"&gt;valkey:&lt;/span&gt;
        &lt;span class="s"&gt;url: valkey://valkey:6379/0&lt;/span&gt;
  &lt;span class="na"&gt;qdrant_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;log_level: INFO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see my prior cagent yaml example to see an mcp server that can use this local instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-searxng"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SEARXNG_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080"&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI development is a rapidly evolving field, and the lessons learned along the way can save you time, frustration, and resources. By understanding the nuances of context management, model selection, embedding strategies, and practical tooling, you can build more robust and efficient AI workflows. Embrace experimentation, but also leverage the growing ecosystem of open-source tools and best practices. As the landscape continues to shift, staying curious and adaptable will be your greatest assets. Happy building!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>learning</category>
    </item>
    <item>
      <title>Terraforming With AI</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Wed, 24 Sep 2025 19:29:43 +0000</pubDate>
      <link>https://dev.to/zloeber/terraforming-with-ai-g0o</link>
      <guid>https://dev.to/zloeber/terraforming-with-ai-g0o</guid>
      <description>&lt;p&gt;This article will go over using a team of AI agents in conjunction with the &lt;a href="https://github.com/hashicorp/terraform-mcp-server" rel="noopener noreferrer"&gt;Terraform MCP server&lt;/a&gt; and Docker's &lt;a href="https://github.com/docker/cagent" rel="noopener noreferrer"&gt;cagent&lt;/a&gt; tool to clean up some rather gnarly autogenerated terraform without needing to write any code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've been digging quite deeply into the morass of AI related tools, models, and agent based frameworks as of late. It is hard to not be fascinated with the prospect of a human language driven declarative engine regardless of how non-deterministic LLMs output can be. I use tools like Cursor, Copilot, Dyad, or any number of agentic cli tools to code out solutions (or parts of them) daily. But I've not had the opportunity to create an agent based workflow. This is mainly because there have been few issues worthy of such attention that couldn't be resolved by using AI to create more deterministic solutions (aka. code/scripts). Using generated code is far less costly and resource intensive as having to push things through an LLM to get the results you are looking to achieve.&lt;/p&gt;

&lt;p&gt;Recently I found a good reason to use an AI workflow and instead of custom coding out something with CrewAI+Python or similar I opted to give Docker's &lt;a href="https://github.com/docker/cagent" rel="noopener noreferrer"&gt;cagent&lt;/a&gt; tool a spin.&lt;/p&gt;

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

&lt;p&gt;Being asked to turn an existing infrastructure deployment into code kinda stinks as a whole. But this kind of task is often a necessity if the environment was hastily constructed via click-ops or existed before you came on board. &lt;a href="https://github.com/GoogleCloudPlatform/terraformer" rel="noopener noreferrer"&gt;Terraformer&lt;/a&gt; was released by Google for this very purpose. It generates terraform from existing resources for a large number of terraform providers. As such it is a great place to start.&lt;/p&gt;

&lt;p&gt;The workflow is not so hard really:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fic103vj6viyw0ib2tl5p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fic103vj6viyw0ib2tl5p.png" alt="Basic terraformer workflow" width="223" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The terraformer tool can also import directly into remote state but I'm opting out of doing this as I wish to rewrite the generated manifests to not give me seizures when reading them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Using Terraformer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/GoogleCloudPlatform/terraformer" rel="noopener noreferrer"&gt;Terrafomer&lt;/a&gt; is a single binary tool that can create terraform for several provider types using some kind of demonic pact or wizardry that is beyond my mere mortal brain. The process for using it is pretty easy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create your &lt;code&gt;version.tf&lt;/code&gt; file in an empty folder with your provider requirements and backend state target.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# version.tf&lt;/span&gt;

terraform &lt;span class="o"&gt;{&lt;/span&gt;
  required_version &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.11"&lt;/span&gt;
  backend &lt;span class="s2"&gt;"local"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    path &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform.tfstate"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  required_providers &lt;span class="o"&gt;{&lt;/span&gt;
    aws &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;source&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      version &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 6.0"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Initialize the folder via terraform to pull down the provider(s)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;terraformer list&lt;/code&gt; to determine the provider resources you wish to import for the provider you are targeting. In my case this would be AWS.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraformer import aws list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Let 'er rip! This example targets a specific AWS profile I'm already authenticated with for 1 region and several network related resources.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraformer import aws &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--resources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;route_table,transit_gateway,vpc,vpc_endpoint,vpc_peering,igw,nat,subnet &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--regions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AWSAdministratorAccess-1111111111111 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--connect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--path-pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./generated &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result will be a &lt;code&gt;./generated&lt;/code&gt; folder in the current directory with a bunch of terraform manifests and tfstate file. Without the &lt;code&gt;--path-pattern=./generated&lt;/code&gt; each provider and resource type extracted would be created as a separate subfolder under a &lt;code&gt;./generated&lt;/code&gt; folder with its own state (in our case &lt;code&gt;generated/aws/route_table&lt;/code&gt;, &lt;code&gt;generated/aws/transit_gateway&lt;/code&gt;, et cetera). I also opted to export everything as JSON instead of HCL as it is far easier to parse and use in automation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE 1&lt;/strong&gt; If you supply multiple regions like &lt;code&gt;us-east-1,us-east-2&lt;/code&gt; then a folder for each region will be created instead. You can also use &lt;code&gt;--path-pattern=./&lt;/code&gt; to remove the &lt;code&gt;./generated&lt;/code&gt; folder from the mix to drop it all into the local path as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Export Issues
&lt;/h2&gt;

&lt;p&gt;Terraformer is pretty cool in what it does but the code generated is abysmal and it exports an unsustainable mess of terraform manifests. Some issues with the autogenerated terraform include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resource and other terraform block names with &lt;code&gt;--&lt;/code&gt; (ugly).&lt;/li&gt;
&lt;li&gt;Resource names that include upper-case and dashes instead of lower-case &lt;code&gt;snake_case&lt;/code&gt; names.&lt;/li&gt;
&lt;li&gt;A large number of attributes with superfluous default or constructed values being defined (ie. &lt;code&gt;all_tags&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;variables.tf&lt;/code&gt; file with no actual variables (includes a data source for the tfstate file instead).&lt;/li&gt;
&lt;li&gt;The use of remote state data that points to existing output as input to other terraform resources (???).&lt;/li&gt;
&lt;li&gt;Hard-coded attribute values for ids of resources being created elsewhere in the output.&lt;/li&gt;
&lt;li&gt;Only supporting terraform 0.13 and below for the state being generated.&lt;/li&gt;
&lt;li&gt;Generating exports for resources that are not able to be imported.&lt;/li&gt;
&lt;li&gt;No implicit dependencies at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of this can be individually fixed in a deterministic manner with scripts if you know the nuances of the provider. Other issues are rather nondeterministic in nature where your exported target resources, providers, and deployment can lead to a wide variety of possible results.&lt;/p&gt;

&lt;p&gt;There are just too many nuanced issues to deal with here for a single script to solve for. Normally I'd just spend hours hand crafting the generated output for use in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Approach
&lt;/h2&gt;

&lt;p&gt;Since most of the exported terraform is named in a way that includes essential ids that are behind the resources being created let us take the following approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the export process to create the initial terraform and state locally as JSON.&lt;/li&gt;
&lt;li&gt;Clean up the code base to address many of the issues as noted above (Use the MCP terraform server as needed here).&lt;/li&gt;
&lt;li&gt;Create modern terraform import blocks for all the resources.&lt;/li&gt;
&lt;li&gt;Delete the local state file.&lt;/li&gt;
&lt;li&gt;Add implicit dependencies where it makes sense to do so (Use the MCP terraform server as needed here as well).&lt;/li&gt;
&lt;li&gt;Optional: Convert final output to HCL.&lt;/li&gt;
&lt;li&gt;Optional: Create reports on what was done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically we will use AI agents to do all the work I might otherwise do manually (that word...'manual'...yuk, sorry for my filthy language)&lt;/p&gt;

&lt;p&gt;This should allow us to do this for any terraformer export moving forward with minor changes based on the provider.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; After going through this whole process I'm now considering just using JSON for all my terraform. Honestly, it is pretty easy to read and use compared to HCL and its many nuances.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Chosen Tool
&lt;/h2&gt;

&lt;p&gt;Agentic AI has several frameworks and tools to choose from. I'm proficient in multiple languages so there are many doors open to me. But I chose a largely no-code solution by docker called &lt;code&gt;cagent&lt;/code&gt; for a few reasons;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Code&lt;/strong&gt; - Mostly no code. This allowed me to scaffold out and test required prompts with little up front development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt; - With this tool I'm defining a single yaml file with multiple agents, their tools, and the models to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Support&lt;/strong&gt; - This supports MCP natively quite easily, we need this for the &lt;a href="https://github.com/hashicorp/terraform-mcp-server" rel="noopener noreferrer"&gt;terraform-mcp-server&lt;/a&gt; used for some of the more advanced tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scripts as Tools&lt;/strong&gt; - Some of the required tasks are able to be completed with simple scripting due to how deterministic they are with the correct information. For instance, if you are able to lookup the terraform resource provider documentation for attributes assigned in your manifest to see if those attributes are being set as default values then removing them from the manifest becomes a simple shell script with &lt;code&gt;jq&lt;/code&gt;. Cagent supports defining these scripts as custom tools with parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curiosity&lt;/strong&gt; - I just wanted to check this project out, so sue me.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;My whole solution can be found in &lt;a href="https://github.com/zloeber/terraformer-cleanup" rel="noopener noreferrer"&gt;this project repo&lt;/a&gt; to clone and use as you see fit. It includes some additional scripts for downloading the required binaries and setting up the environment. &lt;/p&gt;

&lt;p&gt;I use a multi-agent workflow that runs sequentially to break down the steps into manageable parts and reduce overall token context usage. It processes terraform export data you drop into the &lt;code&gt;./input&lt;/code&gt; directory to make clean and usable Terraform in the &lt;code&gt;./output&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1al2j69nbkuyjx6qsnmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1al2j69nbkuyjx6qsnmj.png" alt="Agent workflow" width="153" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;root&lt;/td&gt;
&lt;td&gt;orchestrate the workflow of subagent calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleaner&lt;/td&gt;
&lt;td&gt;Performs a number of terraform cleanup tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connecter&lt;/td&gt;
&lt;td&gt;Connects exported resources to create implicit dependencies where possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Importer&lt;/td&gt;
&lt;td&gt;Recreates state using terraform import blocks for any elements able to be imported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finalizer&lt;/td&gt;
&lt;td&gt;Performs final Terraform best practice scan of results, converts to hcl&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This single YAML file is the entire workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/usr/bin/env cagent run
version: "2"

models:
  # You can use ollama local models. These sort of worked for me
  gptoss:
    provider: openai
    model: gpt-oss
    base_url: http://localhost:11434/v1
  # Or OpenAPI compliant endpoints like OpenRouter.ai
  openrouter:
    provider: openai
    model: x-ai/grok-4-fast:free
    base_url: https://openrouter.ai/api/v1

agents:
  root:
    model: openrouter
    description: Beautifies and refactors Terraform code that was automatically generated by the terraformer tool
    sub_agents:
      - cleaner
      - connecter
      - importer
      - finalizer
    instruction: |
      You are an expert Terraform developer that specializes in writing clean, maintainable, and efficient Terraform code. 
      You manage a team of terraform experts that perform various tasks for your workflow.

      &amp;lt;AGENTS&amp;gt;
      - cleaner - A cleaner Agent that performs a series of cleanup tasks on the terraform codebase
      - connecter - A connecter Agent that connects resources together by updating static values to use implicit dependencies instead
      - importer - An Importer Agent that creates the state import blocks for the resources defined in the terraform codebase
      - finalizer - A Finalizer Agent that reviews the changes made by the other agents and ensures that everything is correct and complete
      &amp;lt;/AGENTS&amp;gt;

      You will start the workflow below to improve the quality and maintainability of a terraform codebase in the ./input path. 
      &amp;lt;WORKFLOW&amp;gt;
        1. call the cleaner agent to perform the cleanup tasks
        2. call the connecter agent to connect resources together by updating static values to use implicit dependencies instead
        3. call the importer agent to create the import blocks for all resources defined in the terraform codebase
        4. call the finalizer agent to review the changes made by the other agents and ensure that everything is correct and complete
      &amp;lt;/WORKFLOW&amp;gt;

      ** Rules
      - Use the transfer_to_agent tool to call the right agent at the right time to complete the workflow.
      - DO NOT transfer to multiple agents at once
      - ONLY CALL ONE AGENT AT A TIME
      - When using the `transfer_to_agent` tool, make exactly one call and wait for the result before making another. 
      - Do not batch or parallelize tool calls.
      - Do not skip any steps or change the order of the steps. 
      - Do not add any additional steps or modify the workflow in any way.
    toolsets:
      - type: think
      - type: todo

  cleaner:
    model: openrouter
    description: A cleaner Agent that performs a series of cleanup tasks on the terraform codebase
    instruction: |
      You are an expert Terraform developer that specializes in writing clean, maintainable, and efficient Terraform code.

      You will perform the following tasks in order to clean up the terraform codebase in the ./input path:
        1. Use the remove_all_attributes script on `./input` directory to remove the 'tags_all' and 'region' attributes
        2. Use the replace_double_dashes script on the `./input` directory
        3. Update the `./input/provider.tf.json` file to remove the terraform block if it exists
        4. Delete the `./input/terraform.tfstate` file if it exists
        5. Delete the `./input/terraform.tfstate.backup` file if it exists
        6. Delete any `./input/.terraform` directories if they exist
        7. Delete any `./input/.terraform.lock.hcl` files if they exist
        8. Update all references found that look like this: `"${data.terraform_remote_state.local.outputs.*}"` with the associated output value in `./input/outputs.tf.json` that is being referenced.
        9. Delete the `./input/outputs.tf.json` file
        10. Delete the `./input/variables.tf.json` file
        11. Use the terraform-mcp-server tool to find and remove all resource attributes defined with default attribute values using the `remove_default_attributes` script.

      ** Rules
      - Do not make any changes outside of the `./input` path.
      - Do not add any additional steps or modify the workflow in any way.
      - Do NOT make recommendations, instead just follow the instructions and make the changes directly to files using the provided scripts and tools.
      - Follow the instructions exactly and in order. 
      - Do not skip any steps or change the order of the steps.
      - If you are unsure about a step, just do your best to follow the instructions and move on to the next step.
      - Only use the provided scripts and tools to make changes to the codebase.
    toolsets:
      - type: filesystem
      - type: think
      - type: mcp
        command: terraform-mcp-server
        args: [ "stdio" ]
      - type: script
        shell:
          remove_default_attributes:
            cmd: "./scripts/remove-default-attributes.sh $filename $attribute $value"
            description: "Remove resource attributes that are set to default values"
            args:
              filename:
                description: "The Terraform file to modify"
                type: "string"
              attribute:
                description: "The resource attribute to remove"
                type: "string"
              value:
                description: "The default value to match"
                type: "string"
          remove_all_attributes:
            cmd: "./scripts/remove-all-attributes.sh $pathname $attribute"
            description: "Remove all resource attributes from the Terraform files regardless of value"
            args:
              pathname:
                description: "The path to run this script against"
                type: "string"
              attribute:
                description: "The resource attribute to remove"
                type: "string"
          replace_double_dashes:
            cmd: "./scripts/replace-double-dashes.sh $targetpath"
            description: "Replace double dashes with single dashes in resource names"
            args:
              targetpath:
                description: "The path to replace double dashes in"
                type: "string"

  connecter:
    model: openrouter
    description: A connecter Agent that connects resources together by updating static values to use implicit dependencies instead
    instruction: |
      You are an expert Terraform developer that specializes in writing clean, maintainable, and efficient Terraform code.
      You will perform the following tasks in order to connect resources together in the terraform codebase in the ./input path:
        1. Find all defined resource attributes with static values that are logically connected to the output attributes of other resources in the deployment 
        and update their assignments to be implicit dependencies of the generated resources instead.
        (For example: A vpc endpoint defined with `"vpc_id": "vpc-0b009e1ed52947d16"` when we create that vpc as `resource.aws_vpc.tfer_vpc-0b009e1ed52947d16` 
        should become `"vpc_id": "${aws_vpc.tfer_vpc-0b009e1ed52947d16.id}"`). Look for other common attributes that are often statically defined that could be converted 
        to implicit dependencies as well. These include but are not limited to:
          - subnet_id
          - security_group_id
          - vpc_id
          - iam_role_arn
          - cluster_id
          - instance_id
          - bucket_name
          - key_name
          - db_instance_identifier
          - db_subnet_group_name
          - route_table_id
          - network_interface_id
          - elastic_ip
          - nat_gateway_id
          - load_balancer_arn
          - target_group_arn
          - certificate_arn
          - log_group_name
          - topic_arn
          - queue_url
      ** Rules
      - Do not make any changes outside of the `./input` path.
      - Use the terraform-mcp-server tool to lookup resource output attributes when needed 
      - Do NOT make recommendations, instead just follow the instructions and make the changes directly to files 
    toolsets:
      - type: filesystem
      - type: think
      - type: mcp
        command: terraform-mcp-server
        args: [ "stdio" ]

  importer:
    model: openrouter
    description: Creates Terraform import blocks for resources that were automatically generated by the terraformer tool
    instruction: |
      You are an expert Terraform developer that specializes in writing clean, maintainable, and efficient Terraform code.

      You will create import blocks for all resources in the Terraform manifests found in `./input` by
      using the terraform-mcp-server tool to lookup each resource that is defined in ./input/*.tf.json files to
      generate import terraform code blocks within a new `./input/imports.tf.json` file. If the resource
      does not support import, remove it from the codebase.

      `./input/imports.tf.json` should be a valid terraform json file with the following structure:
      ```

json
      {
        "import": [
          {
            "to": "${resource_type.resource_name}",
            "id": "resource_id"
          },
          ...
        ]
      }
      ** Rules
      - Only make changes inside of the `./input` or `./output` paths.
      - When looking up resources using the terraform-mcp-server tool; 
          only lookup resources that were created in the `./input` path,
          if a resource is not able to be imported, remove it from the codebase and do not include it in the imports.tf.json file.
          only use the resource name as the identifier (do not use any other attributes or values).
          if you are unable to find a resource, just move on to the next step without making any changes.
          lookup the most recent version of the provider
      - When done processing do not display your final report to the screen, instead create a report in ./output/import_report.md in markdown format with your notes, suggestions, and changes made.
    toolsets:
      - type: filesystem
      - type: think
      - type: mcp
        command: terraform-mcp-server
        args: [ "stdio" ]
      - type: shell

  finalizer:
    model: openrouter
    description: Reviews the changes made by the other agents and ensures that everything is correct and complete.
    instruction: |
      You are an expert Terraform developer that specializes in writing clean, maintainable, and efficient Terraform code.
      Your job is to review the changes made by the cleaner and importer agents in the `./input` path and ensure that everything is correct and complete.

      You will make any final adjustments or corrections to the terraform codebase in the ./input path as needed to ensure that it is ready for production use.
      You can use the terraform-mcp-server tool to lookup the terraform style guidelines for any resources that you are unsure about.
      You will also ensure that the imports.tf.json file is correctly formatted and contains all necessary import blocks for the resources defined in the terraform codebase.

      When completed, you will create the `./output` directory and copy the cleaned and finalized terraform codebase from the `./input` directory to the `./output` directory
      converting the codebase from json into valid hcl format for use in production terraform pipelines as you go.

      ** Rules
      - Do not make any changes outside of the `./input` and `./output` paths.
      - If you find any issues or inconsistencies you are able to resolve, you will correct them directly in the codebase.
      - If there are any issues or inconsistencies you cannot resolve, add a comment to the top of the relevant file describing the issue and suggesting a possible solution.
      - Do not display your thought process or reasoning, just make the changes directly to the codebase.
      - Do not display your final report to the screen, instead create a report in ./output/final_report.md in markdown format with your notes and suggestions for next steps.
      - Return success when complete.
    toolsets:
      - type: filesystem
      - type: think
      - type: mcp
        command: terraform-mcp-server
        args: [ "stdio" ]
      - type: shell


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I ran this solution against a rather large network deployment in AWS and was immensely satisfied with the results. It was approximately 95% accurate after running the results through a terraform init and plan. It only missed a single AWS eid import for a NAT gateway.&lt;/p&gt;

&lt;p&gt;This is of course a warning to ALWAYS validate the output of a nondeterministic workflow like this. Had I just accepted it as-is a key resource would have been recreated thus causing an outage.&lt;/p&gt;

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

&lt;p&gt;I learned a few lessons in my endeavors that are worth noting. &lt;/p&gt;

&lt;h3&gt;
  
  
  1. Frontier Models are Just Better
&lt;/h3&gt;

&lt;p&gt;The models you choose really make a difference. No matter the billions of parameters or mixture of agents or any other tricks used by the model if it is not good at tool calling it will fumble about, freeze, and produce subpar results. If you aren't using Claude or OpenAI then just create an OpenRouter account and become part of the training data of some of the larger more mature models offered for free.&lt;/p&gt;

&lt;p&gt;I started with a capable local ollama host and tried a few decent models and had painfully random results. One out of ten runs would get me near my goals. It was maddening. The moment I jumped over to a frontier model things started working as designed consistently.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. New Tools == Inconsistent Documentation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/docker/cagent" rel="noopener noreferrer"&gt;cagent&lt;/a&gt; worked well but there is invalid documentation right in the first README.MD file in the project repo (it is &lt;code&gt;toolsets&lt;/code&gt; not &lt;code&gt;toolset&lt;/code&gt;). As the yaml schema doesn't seem to be validated you don't even know there is an issue either. Additionally, no where in the included &lt;a href="https://github.com/docker/cagent/blob/main/docs/USAGE.md" rel="noopener noreferrer"&gt;usage docs&lt;/a&gt; does it explain how to use custom scripts as tools. I only found out about them from some of the several dozen included &lt;a href="https://github.com/docker/cagent/tree/main/examples" rel="noopener noreferrer"&gt;examples&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also nowhere is it documented that you can simply use an openAI compliant API. But it does work, promise! Your only caveat is that regardless if there is an API key or not you must have &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; set in your environment.&lt;/p&gt;

&lt;p&gt;This is an open source app so if you are in doubt often it is best to just roll up your sleeves and dig into the code like I did to get the answers you are looking for.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. MCP is Nice
&lt;/h3&gt;

&lt;p&gt;I'm pretty happy with the results of my efforts but had to diverge from using the Docker MCP registry for the solution. As it supports calling the tools with arguments I opted to just install the binary locally using a mise http provider (also a nifty trick worth looking at in my &lt;code&gt;mise.toml&lt;/code&gt; file). This allows for the entire solution to run without docker or using their pre-approved MCP images.&lt;/p&gt;

&lt;p&gt;I'm convinced that using MCP is was what made this solution work well. Without it I've yet to see any LLM model create very good terraform, ever.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Mixed Solutions are Sweet
&lt;/h3&gt;

&lt;p&gt;I've used the term 'deterministic' and 'nondeterministic' like 20x in this article. Using LLMs in any solution is prone to give you different results, thus they are nondeterministic. Any other code that produces results the same way every time is deterministic. Using both kinds of tools in a solution like this is a powerhouse hit in my mind. Provide your team with the right tools to get the job done and it need not produce the same result every time, it just needs to accomplish the tasks you are giving them.&lt;/p&gt;

&lt;p&gt;The cagent tool is nice in that you are not required to create a whole MCP server for a few scripts to be exposed as tools to the agents. This allowed me to create a team of agents yet tell them to use some specific scripts as tools. This reduced the amount of effort being asked of the agents which, in turn, increased the quality of the produced results. As in real life, I don't really care if the results produced are always the same 100% of the time. I do care if they are technically infeasible or unusable though.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Pleasant Surprises!
&lt;/h3&gt;

&lt;p&gt;I got a few unexpected but pleasant surprises with the addition of my Finalizer sub-agent. It took the liberty to rename some of the resources to be more descriptive to what they actually were in the environment. It was able to discern some of the vpc's and subnets to be reflective of their purpose and in general did WAY more than I expected from it. I believe this is because it was instructed to use the terraform mcp server to look up style guidelines and create a production worthy release.&lt;/p&gt;

&lt;p&gt;On a whim I also had it do the HCL conversion and this worked way better than having to deal with a custom tool or script as well. I was expecting to just use the JSON results when finished but this ended up not being the case at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Automating the cleanup and refactoring of Terraformer output using agentic AI workflows has proven to be both practical and efficient. By leveraging tools like Docker's cagent and the Terraform MCP server, it's possible to transform messy, autogenerated Terraform code into production-ready infrastructure as code with minimal manual intervention. While some nondeterminism remains inherent to LLM-driven solutions, combining deterministic scripts with AI agents yields high-quality, maintainable results. As these tools and frameworks mature, expect even more streamlined and reliable workflows for infrastructure automation. Always remember to validate the final output, but with the right approach, AI-powered refactoring can save significant time and effort for DevOps teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/GoogleCloudPlatform/terraformer" rel="noopener noreferrer"&gt;terraformer&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/docker/cagent" rel="noopener noreferrer"&gt;cagent&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/hashicorp/terraform-mcp-server" rel="noopener noreferrer"&gt;terraform-mcp-server&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/zloeber/terraformer-cleanup" rel="noopener noreferrer"&gt;Code Repo&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>terraform</category>
      <category>nocode</category>
    </item>
    <item>
      <title>Free Tokens for AI Exploration</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Mon, 14 Jul 2025 21:19:26 +0000</pubDate>
      <link>https://dev.to/zloeber/free-tokens-for-ai-exploration-3ie4</link>
      <guid>https://dev.to/zloeber/free-tokens-for-ai-exploration-3ie4</guid>
      <description>&lt;p&gt;Using ChatGPT, Gemini, Grok, or any of the other chat based LLM service is a great way to start with AI. But in order to bring things to the next level you will either need some beefy hardware to run models locally or access to an online API with models you can use. This article will walk you through how to do the later of these two options for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is OpenRouter?
&lt;/h2&gt;

&lt;p&gt;OpenRouter.ai is an API service that acts like an umbrella to several dozen LLM providers with some of these models being free for training purposes. You can use these free models to develop AI at no cost if you don't mind being part of that training set. Sadly, OpenRouter does not include any form of embedding endpoints to use. Embedding is the conversion of knowledge/data for later retrieval. This is essentially how AI memory works so without this essential component your development efforts will be crippled.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Learning Point&lt;/strong&gt; Embedding models are used in AI to convert text or other data into numerical vectors that capture semantic meaning. These vectors enable efficient comparison, search, and retrieval of information, making them essential for tasks like semantic search, recommendation systems, and enabling memory in AI agents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/zloeber/crewai-openrouter-lab/" rel="noopener noreferrer"&gt;This project&lt;/a&gt; works around the lack of an embedding model endpoint by using the ollama endpoint locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;Create your own local &lt;code&gt;.env&lt;/code&gt; file from the &lt;code&gt;.env_example&lt;/code&gt; included and update the OpenRouter API key to be your own key. Other dependencies can be installed in macos/Linux using the included configuration script, &lt;code&gt;./configure.sh&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; I use &lt;a href="https://mise.jdx.dev/" rel="noopener noreferrer"&gt;mise&lt;/a&gt; for installing the required binaries here and recommend you install and use it if you do not already. Otherwise you can get away with just having the ollama binary, docker, and python 3.12+.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Starting The Embedder
&lt;/h2&gt;

&lt;p&gt;Start ollama locally and pull down and embedding model to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Configure ollama to run as a server and pull in the embedder used for storing 'memories'&lt;/span&gt;
ollama serve &amp;amp;
ollama pull nomic-embed-text

&lt;span class="c"&gt;# Optionally test it out (the final output should be a list of vectors representing the embedded data)&lt;/span&gt;
curl http://localhost:11434/api/embeddings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "nomic-embed-text",
    "prompt": "What is semantic search"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; &lt;a href="https://ollama.com/blog/embedding-models" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is a more detailed description with some example code of using an embedding model via ollama to store embeddings into a vector database (Chromadb) locally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With this running, we can then focus on finding an appropriate (free) LLM model from OpenRouter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding a Free LLM
&lt;/h2&gt;

&lt;p&gt;I've included a script you can use to query OpenRouter for LLMs of any sort, including the free ones. In our case we are looking for LLMs with zero cost but also include tools as a feature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ./.venv/bin/activate
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.select-openrouter-model &lt;span class="nt"&gt;--max-cost&lt;/span&gt; 0 &lt;span class="nt"&gt;--limit&lt;/span&gt; 10 &lt;span class="nt"&gt;--output&lt;/span&gt; brief &lt;span class="nt"&gt;--features&lt;/span&gt; &lt;span class="s1"&gt;'tools'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should provide a list of models you can use for free that support tools capabilities. This might be different in the future, thus the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Mistral: Devstral Small (free) (ID: mistralai/devstral-small:free)
2. Meta: Llama 3.3 8B Instruct (free) (ID: meta-llama/llama-3.3-8b-instruct:free)
3. Meta: Llama 4 Maverick (free) (ID: meta-llama/llama-4-maverick:free)
4. Meta: Llama 4 Scout (free) (ID: meta-llama/llama-4-scout:free)
5. Google: Gemini 2.5 Pro Experimental (ID: google/gemini-2.5-pro-exp-03-25)
6. Mistral: Mistral Small 3.1 24B (free) (ID: mistralai/mistral-small-3.1-24b-instruct:free)
7. Meta: Llama 3.3 70B Instruct (free) (ID: meta-llama/llama-3.3-70b-instruct:free)
8. Mistral: Mistral 7B Instruct (free) (ID: mistralai/mistral-7b-instruct:free)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Demo: Interactive Human-AI Chat with CrewAI
&lt;/h2&gt;

&lt;p&gt;This demonstrates using the ollama embedder and openrouter.ai LLM with chainlit and crewai to prompt a user for more information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo: Overview
&lt;/h3&gt;

&lt;p&gt;This script creates a conversational AI assistant that collects personal information through natural dialogue. Using CrewAI's agent framework and Chainlit's user interface, it demonstrates how to build interactive AI systems that gather specific information while maintaining a natural conversation flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo: How It Works
&lt;/h3&gt;

&lt;p&gt;When a user sends a message, two specialized AI agents work together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Information Collector&lt;/strong&gt;: Asks follow-up questions to gather name and location details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Information Summarizer&lt;/strong&gt;: Transforms collected data into a natural, friendly summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Natural back-and-forth conversation with AI&lt;/li&gt;
&lt;li&gt;Dynamic follow-up questions when more context is needed&lt;/li&gt;
&lt;li&gt;Friendly web interface using Chainlit&lt;/li&gt;
&lt;li&gt;Structured information collection in a conversational format&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Running
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ./.venv/bin/activate
chainlit run ./src/human_input/crewai_chainlit_human_input.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example demonstrates how AI systems can be made more interactive by combining structured task workflows with natural human conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good Take Away Knowledge
&lt;/h2&gt;

&lt;p&gt;Some things to understand about all of this.&lt;/p&gt;

&lt;h3&gt;
  
  
  CrewAI uses LiteLLM
&lt;/h3&gt;

&lt;p&gt;CrewAI uses LiteLLM to proxy most connection requests to various LLM providers. This can lead to some confusing results when you go to run your crew and find that your manually passed information for models, endpoints, and keys do not work. This is due to how LiteLLM will source in environment variables and use them instead. In our case, we load a &lt;code&gt;.env&lt;/code&gt; file into the current environment variables list of the current process which means they should align with the expected names of the LiteLLM provider. This means the variable names are not fungible. You must define them as the following for CrewAI to function properly for OpenRouter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
OPENROUTER_API_KEY="&amp;lt;your-key&amp;gt;"
OPENAI_MODEL_NAME="&amp;lt;model&amp;gt;"
OPENAI_API_BASE="https://openrouter.ai/api/v1"
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CrewAI Memory
&lt;/h3&gt;

&lt;p&gt;CrewAI &lt;a href="https://docs.crewai.com/concepts/memory" rel="noopener noreferrer"&gt;Memory&lt;/a&gt; is stored locally but still requires an embedding API endpoint to function. By default this is OpenAI's endpoint. Without modification you will be left with many errors about invalid credentials in your logs for OpenAI even when you aren't using it for your LLM calls.&lt;/p&gt;

&lt;p&gt;In my examples I overwrite the memory target from a default location in your home directory to the local project. See the CrewAI docs on how to review collections in this data and do other troubleshooting around this.&lt;/p&gt;

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

&lt;p&gt;This was an example using CrewAI with OpenRouter but should apply for most any Agent framework you decide to use. Enjoy coding up your AI app!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>api</category>
    </item>
    <item>
      <title>Semi Auto Importing Terraform State</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Mon, 14 Jul 2025 20:06:18 +0000</pubDate>
      <link>https://dev.to/zloeber/semi-auto-importing-terraform-state-374k</link>
      <guid>https://dev.to/zloeber/semi-auto-importing-terraform-state-374k</guid>
      <description>&lt;p&gt;On more than one occasion I've longed for the mean's to automatically import terraform state. But this is a feature I know will likely never be added for a number of very good reasons. In some cases it is possible to use only a plan file to automate the generation of terraform import blocks though. Here is how it can be done.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;p&gt;This blog, script, and example can be found &lt;a href="https://github.com/zloeber/terraform-semi-auto-import/" rel="noopener noreferrer"&gt;here&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Use Case
&lt;/h2&gt;

&lt;p&gt;You have created a slew of terraform modules or code for resources that already exist and would like to import them into your state via terraform &lt;code&gt;import&lt;/code&gt; blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why No Auto Import?
&lt;/h2&gt;

&lt;p&gt;For a long time I've wondered why terraform does not include a native ability to automatically import targeted state elements. It 'feels' like it would be such a killer feature to have when you need to refactor a bunch of infrastructure as code or add it where none existed before. But a deeper inspection on this yields several reasons that this feature will never be added.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ambiguity
&lt;/h3&gt;

&lt;p&gt;Terraform relies on explicit code definitions in &lt;code&gt;.tf&lt;/code&gt; files to know what resources to manage.&lt;/p&gt;

&lt;p&gt;Auto-import would require Terraform to guess the appropriate HCL configuration for each resource. This would be error-prone or incomplete, especially for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resources with complex dependencies&lt;/li&gt;
&lt;li&gt;Resources using computed values, modules, or for_each/count&lt;/li&gt;
&lt;li&gt;Custom logic or dynamic blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Lack of 1:1 Mapping from API → HCL
&lt;/h3&gt;

&lt;p&gt;Many cloud resources have non-obvious or lossy mappings between API responses and HCL syntax.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: AWS IAM policies, ECS task definitions, security group rules, etc., may include generated or optional data not present in Terraform configs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some fields are ignored by Terraform or only exist as computed outputs, meaning Terraform can't regenerate full HCL from state.&lt;/p&gt;

&lt;p&gt;Analogy: It's like trying to reverse-engineer source code from a compiled binary — possible in some cases, but often messy.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Tooling Complexity
&lt;/h3&gt;

&lt;p&gt;Implementing robust auto-import across providers would require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsing provider schemas for every resource type&lt;/li&gt;
&lt;li&gt;Generating idiomatic HCL, including nested blocks&lt;/li&gt;
&lt;li&gt;Ensuring generated code matches best practices&lt;/li&gt;
&lt;li&gt;Handling drift or manual configuration inconsistencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is difficult to maintain across hundreds of providers and thousands of resource types.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Risk of State Drift or Mismanagement
&lt;/h3&gt;

&lt;p&gt;Automatically importing resources could lead to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accidental overwrites of unmanaged resources&lt;/li&gt;
&lt;li&gt;Misalignment between actual infrastructure and expectations in code&lt;/li&gt;
&lt;li&gt;Users thinking resources are safely managed when they aren't&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Terraform's import is deliberately manual and opt-in to prevent such surprises.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Terraform's Design Philosophy: Explicit is Better
&lt;/h3&gt;

&lt;p&gt;HashiCorp prefers a conservative, explicit workflow where users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define resources in HCL&lt;/li&gt;
&lt;li&gt;Import them manually using terraform import&lt;/li&gt;
&lt;li&gt;Verify state and code alignment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes changes and intentions clear, especially in regulated or production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What? I Need This!
&lt;/h2&gt;

&lt;p&gt;The built in options are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use terraform cli to manually import each state element with the correct ID and target after running your plan.&lt;/li&gt;
&lt;li&gt;Use terraform's built in import blocks to perform one-time state import in your pipeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Either option is a manual slog. But in some cases we can use a code generation approach against an existing plan file (in json) to emit all the import statements for new resources you know already exist. This can be done with clever mapping for predictable provider resources and the process works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Author the initial terraform manifests&lt;/li&gt;
&lt;li&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;terraform plan -out=plan.tfplan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;terraform show -no-color -json plan.tfplan | jq &amp;gt; plan.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Look up the import schema to determine the import id format and, more importantly, if you already have all the data you need to construct the id within your existing plan file.&lt;/li&gt;
&lt;li&gt;Create an id map for your import data. This should target one or more providers and can include any known data you already have or that can be scraped from the existing plan file (see example further on).&lt;/li&gt;
&lt;li&gt;Run the &lt;a href="https://github.com/zloeber/terraform-semi-auto-import/" rel="noopener noreferrer"&gt;this script&lt;/a&gt; with your plan json data and the map file to create a new set of import commands. &lt;code&gt;uv run ./import-terraform.py ./plan.json new_imports.tf --id-map ./import_map.yaml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform plan -out=plan.tfplan&lt;/code&gt; --&amp;gt; If this shows only imports and additions then you likely are ready to apply. If not, then review what went wrong or how your mappings are defined to ensure they are accurate.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE 1&lt;/strong&gt; In step 4 I use jq to make the resulting output prettier for you to visually parse later when making your map file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE 2&lt;/strong&gt; Each map file is going to be highly dependant on your needs! I've yet to figure out how to import the appropriate schema to automate this process for a target provider. Import ids can be wildly different based on the provider and resource. See &lt;code&gt;./import_map.aws.example.yaml&lt;/code&gt; for one that merges several data points into a single id for instance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How The Script Works
&lt;/h2&gt;

&lt;p&gt;This script first parses out the &lt;code&gt;resource_changes&lt;/code&gt; of the plan file for anything with &lt;code&gt;change.action&lt;/code&gt; == &lt;code&gt;create&lt;/code&gt;. It then correlates each item found to a map file entry based on the provider and resource name. If one exists, it extrapolates the ID map for an import block based on the same created resource's &lt;code&gt;change.after&lt;/code&gt; data. Finally it uses all this to generate a valid import block for the target resource.&lt;/p&gt;

&lt;p&gt;This means we can only auto-import predictable terraform based on named elements. We do not auto pull data from any outside resources which limits the scope of what can be imported using this method. For example, a new ec2 instance would be impossible to auto-import using this method. This is because the instance id required for the import block would never be available in our plan file data. But one could still produce some template import blocks using this script, then post-process the results with another script that replaces the output with found aws instances! &lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;Install requirements via uv: &lt;code&gt;uv sync&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can also use mise to install terraform and python and uv if required &lt;code&gt;mise install -y&lt;/code&gt; (also included in &lt;code&gt;./configure.sh&lt;/code&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;A fairly poor but working example of how to do this can be found in the &lt;code&gt;./example&lt;/code&gt; path. Local resources do not lend themselves well to importing state so I used a local vault deployment with the hashicorp/vault terraform provider instead. Here is how you can run through this example locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;examples

&lt;span class="c"&gt;# Start local vault dev instance&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# export the dev root token&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev-token-12345

&lt;span class="c"&gt;# perform initial deployment&lt;/span&gt;

terraform init
terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt; plan.tfplan
terraform apply plan.tfplan

&lt;span class="c"&gt;# delete the local state then get your plan as json again.&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; ./terraform.tfstate ./terraform.tfstate.backup
terraform show &lt;span class="nt"&gt;-no-color&lt;/span&gt; &lt;span class="nt"&gt;-json&lt;/span&gt; plan.tfplan | jq &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plan.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point you will need to figure out the import id by visiting the terraform provider documentation (I know of no way to scrape import id schema automatically anywhere). So I dropped into the terraform provider &lt;a href="https://registry.terraform.io/providers/hashicorp/vault/latest" rel="noopener noreferrer"&gt;website&lt;/a&gt; for vault and looked up the &lt;code&gt;vault_mount&lt;/code&gt; and &lt;code&gt;vault_kv_secret_v2&lt;/code&gt; resources. I discovered that that they both require just a path to import. Sweet! Lets start with the &lt;code&gt;vault_mount&lt;/code&gt; resource. I inspect the plan json output for the &lt;code&gt;vault_mount&lt;/code&gt; create resource data and zero in on the &lt;code&gt;after&lt;/code&gt; section for the created resource:&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;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vault_mount.kv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"managed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vault_mount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"provider_name"&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/hashicorp/vault"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"change"&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;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"create"&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;"before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"after"&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;"allowed_managed_keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"allowed_response_headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"delegated_auth_accessors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KV Version 2 secret engine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"external_entropy_access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"identity_token_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"listing_visibility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"options"&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;"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;"2"&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;"passthrough_request_headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"plugin_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kv"&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like they give us &lt;code&gt;path&lt;/code&gt; straight away so the start of our map file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;registry.terraform.io/hashicorp/vault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vault_mount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{path}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if we look at the next resource, &lt;code&gt;vault_kv_secret_v2&lt;/code&gt;, we see something like this:&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;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vault_kv_secret_v2.secrets[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;user-credentials&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"managed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vault_kv_secret_v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secrets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"provider_name"&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/hashicorp/vault"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"change"&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;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"create"&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;"before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"after"&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;"cas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"data_json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;admin_password&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;secure_password_123&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;admin_username&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;last_backup&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;2024-01-15T10:30:00Z&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;user_count&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;150&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"data_json_wo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"data_json_wo_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"delete_all_versions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"disable_read"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"mount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No path! But we can make the correct path with &lt;code&gt;mount&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; so that becomes our mapping to complete our map file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;registry.terraform.io/hashicorp/vault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vault_mount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{path}"&lt;/span&gt;
  &lt;span class="na"&gt;vault_kv_secret_v2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{mount}/data/{name}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; An astute reader will notice that I included the 'data' section in that path. This is just a nuance of Vault kv version 2 that I happen to know already. It is also a good example of how you can manually tweak these mappings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that we have this we can create the import block file and proceed to replan with it in place to import all the existing paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run ../import-terraform.py ./plan.json kv_imports.tf &lt;span class="nt"&gt;--id-map&lt;/span&gt; ./import_map.yaml
terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt; plan.tfplan
terraform apply plan.tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will pull in the existing secrets as state and recreate your state file as it was before you deleted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improvements
&lt;/h2&gt;

&lt;p&gt;The manual part of this, creating map file entries, is pretty hard to automate. We could theoretically automate it somewhat using AI that scrapes the terraform registry documentation for each resource found and seeks out the import requirements to then generate the map entries. Or we could (somewhat dangerously) try to evoke terraform import commands with a bogus id and scrape the returned errors as some providers (such as AWS) will give useful errors for id format issues.&lt;/p&gt;

&lt;p&gt;I am also fascinated by the &lt;a href="https://github.com/GoogleCloudPlatform/terraformer" rel="noopener noreferrer"&gt;terraformer&lt;/a&gt; project's ability to create terraform from existing resources for several dozen providers. Perhaps there is some way to generate import commands instead of painfully large terraform manifests with some clever engineering of it's source code.&lt;/p&gt;

&lt;p&gt;If anyone has better options for this arduous process leave a comment, I'm keen to know what I'm missing here. Otherwise, maybe this script will help get you part of the way to clean terraform state. May all your terraform pipelines run green my friends!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>Pre-Cache Terraform Provider Plugins</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Wed, 19 Mar 2025 17:04:38 +0000</pubDate>
      <link>https://dev.to/zloeber/pre-cache-terraform-provider-plugins-42kn</link>
      <guid>https://dev.to/zloeber/pre-cache-terraform-provider-plugins-42kn</guid>
      <description>&lt;p&gt;Pre-caching terraform providers in your CICD pipeline images is awesome but hardly anyone does it. I've created a project that makes this task easier than ever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;In a very active platform as a service inside a larger organization you can see hundreds if not thousands of pipelines being run in a day for various terraform provisioning. This can add up to quite a bit of network activity and time wasted downloading the same terraform providers over and over again. But terraform is pretty smart and will not redownload provisioners that already exist in the local plugin cache. Pre-caching these can be beneficial for two main reasons&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reduce provisioner pipeline run time - Eliminates the near constant re-downloading of these external binary packages from the terraform registry.&lt;/li&gt;
&lt;li&gt;Reduce external dependencies - The terraform registry has gone down at least 1 time in the last few years. This causes an unresolvable provisioning outages.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  How
&lt;/h2&gt;

&lt;p&gt;I've created &lt;a href="https://github.com/zloeber/terraform-cicd-image" rel="noopener noreferrer"&gt;a project&lt;/a&gt; that includes a Dockerfile and some scripts that process a yaml file that contains target git repos (and any subpaths) that the image would be used within. When built, this image will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-cache providers for the defined target git projects/folders&lt;/li&gt;
&lt;li&gt;Install multiple versions of the terraform and other binaries via &lt;a href="https://mise.jdx.dev" rel="noopener noreferrer"&gt;mise&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Clone &lt;a href="https://github.com/zloeber/terraform-cicd-image" rel="noopener noreferrer"&gt;this repo&lt;/a&gt; into your organization then make updates as needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the &lt;code&gt;config/provisioners.yml&lt;/code&gt; file with all of your downstream terraform provisioning projects, their branches, and target folders that will be processed.&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;mise.toml&lt;/code&gt; file to include terraform and other binary versions you wish to have included.&lt;/li&gt;
&lt;li&gt;Add CICD pipeline code for your organization to build and push your image. &lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; The order of versions in &lt;code&gt;mise.toml&lt;/code&gt; matter. The first one in the list will be used by default. See the &lt;a href="https://mise.jdx.dev/configuration.html" rel="noopener noreferrer"&gt;configuration of mise&lt;/a&gt; for more details on this wonderful tool.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Manual Providers
&lt;/h2&gt;

&lt;p&gt;If you need to include latest versions of a provider or have a need to manually define one, you can easily do this as well. Edit the local &lt;code&gt;config/provisioners.yml&lt;/code&gt; file and add a local path that contains a terraform &lt;code&gt;version.tf&lt;/code&gt; file within the local &lt;code&gt;config&lt;/code&gt; directory. Examples are provided in this project (that can be removed if you do not need them)&lt;/p&gt;




&lt;h2&gt;
  
  
  Local Testing
&lt;/h2&gt;

&lt;p&gt;To see how this will work you can run everything locally using the included taskfile tasks within.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;task providers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This should produce a local &lt;code&gt;tempproviders&lt;/code&gt; folder with all of the plugins for your downstream terraform provisioners.&lt;/p&gt;

&lt;p&gt;Additionally, helper tasks for building and shelling into the container image are included.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;task docker:build docker:shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Shaving off 10 seconds per pipeline may seem like a fool's errand but the benefits from such an exercise are hard to ignore. Eliminating external dependencies while speeding up your frequently used pipelines should always be in you scopes when engineering your solutions.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>The Technical Interview</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Thu, 27 Feb 2025 20:57:21 +0000</pubDate>
      <link>https://dev.to/zloeber/the-technical-interview-3nfb</link>
      <guid>https://dev.to/zloeber/the-technical-interview-3nfb</guid>
      <description>&lt;p&gt;In the last several years I've had the great privilege of being the final technical interviewer for a large number of candidates. This article is an inside scoop on how I perform these interviews with tips on how you can shine as a candidate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why My Opinion Matters
&lt;/h2&gt;

&lt;p&gt;I come from a background of 25+ years of experience across a wide range of technical areas with a large number of certifications under my belt. I often joke that I got into this industry almost 30 years ago with the goal of knowing all there is yet here feeling like I know less than ever. &lt;/p&gt;

&lt;p&gt;Sadly, unless the interview is a direct hire for my own team there is a strong chance we will never actually work with one another. But that is OK, I still get the benefit of having deep technical discussions with a wide range of geeks across all walks of life and technical specialties. I just happen to be the one that has been chosen to vet candidates that are fibbing on their abilities from those that are not. This must be done in under an hour with little more than your resume in most cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Basics
&lt;/h2&gt;

&lt;p&gt;Obviously, not all technical positions are made the same. A Sr. DevOps role will demand entirely different skills than a Cloud Architect. But there are common elements of all technical roles that will surface in any high level technical interview with me. Regardless of your skill set I'd be prepared to talk on one or preferably all of these topics;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Software development life-cycle (SDLC)&lt;/strong&gt; - This includes varying degrees of knowledge about git and CICD and how to deliver artifacts to production. This may feel strange to list first but it is part of most roles currently in some form or another.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automation&lt;/strong&gt;  - Ansible, Terraform, or some declarative infrastructure as code is a huge plus in any technical role. If you have none of this then the next section is your friend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scripting&lt;/strong&gt; - Pick a language of your choice and know something about it. PowerShell, Bash, JavaScript, or Python are easy ones but any form of scripting knowledge is a sign you are not just an average mouse clicker. If you come from a development background then even better (unless that language is something like FoxPro or QuickBasic)!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI&lt;/strong&gt; - Yes, I expect you to be able to explain some ways you have used it to make your life easier and your job more productive. It shows you are sticking with the times as well.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your Specialty
&lt;/h2&gt;

&lt;p&gt;This is the reason why we are talking, your moment to shine as it were. Here are my general tips for how to handle me during this most essential portion of our discussion.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Know It&lt;/strong&gt; - Simple tip but worth putting out there first. I promise you cannot fake it with me very easily. I've done hundreds of interviews across many technical specialties and have a very high success rate for weeding out fakers. Your resume should reflect what we will be talking about on the very first page somewhere and be recent enough for you to go into depth on projects you have worked on that exhibit that knowledge. I will dig as deep as I can and then keep digging until you cannot answer my questions in most cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Teach Me&lt;/strong&gt; - Adding to the prior tip, I gain supreme satisfaction learning something new. If you can teach me about something in our industry I never knew before I'm going to be not only impressed but also more grateful for our time together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be an Expert in Something&lt;/strong&gt; - This is a bit more nuanced but I find that in most interviews I end up asking the candidates what they feel they are best at. This is intentional because then I know you should have answers in that topic and I should be able to dig and learn from you. This is one way I'll litmus test your brain on things. For example, if you are an expert in Ansible be ready to know what template language playbooks are written in (Jinja2 and YAML), how you can automate them, and the protocol it runs over.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be Curious&lt;/strong&gt; - No one likes a know it all. Curiosity is how we grow ourselves in this industry and push our knowledge boundaries to new levels. I'm going to want to know where you would like to expand on your current skills and grow yourself further in your journey.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be A Problem Solver&lt;/strong&gt; - Have at least one recent experience you can talk about in great detail that exhibits that you were in the trenches and getting things done in a self-directed manner. It should be something that you are proud of and can speak towards. This should easily be found on the first or second page of your resume. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Some Other Tips
&lt;/h2&gt;

&lt;p&gt;While each discussion I have is bound to be somewhat different there are some common elements that will come up. Often I'll even start my discussion with these tips because, believe it or not, I'm rooting for you to do well!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Focus on Yourself&lt;/strong&gt; - If you got this far in your career then I already know you must work somewhat well with others and so I don't really care about what you have done 'as a team'. I'm only interested in your personal accomplishments and solutions as that is how I can further assess your actual knowledge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be Honest&lt;/strong&gt; - If you don't know, just say so and we will pivot to something else. I will never hold honesty against you. But I will hold it against you if you waste our limited time spitting out totally wrong answers just because.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be Passionate&lt;/strong&gt; - No one wants to have a dull conversation. You will stick out if you are into what you do far more than if you drone on about technology and your accomplishments. Passion for this industry is why I am part of it, ideally you reflect that insatiable desire to learn and grow in our industry of endless possibilities as well.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be Visible&lt;/strong&gt; - If you don't show your face it severely lessens your chances of being selected among a list of candidates that do. Additionally I will rely on this to better read how flustered you get and how well you handle things when I start digging deep into your brain for what you know.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be patient&lt;/strong&gt; - Specifically with me. Obviously I don't know everything and sometimes I may ask questions because I selfishly want to walk away learning something new. I also might make hard pivots on purpose to keep things going and to see how well you handle it. Its not personal it is by design.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;This was a rather non-technical article but one that felt right to put out there given the climate of uncertainty within out industry. I genuinely hope you take this information and do well in your technical interviews. Maybe, if we are both lucky, you and I will do one of these together and you can shine brightly like the incredible geek that you are!&lt;/p&gt;

</description>
      <category>career</category>
      <category>careerdevelopment</category>
      <category>interview</category>
    </item>
    <item>
      <title>Mise - Jump between Per-Folder Dev Environments like a Wizard</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Mon, 03 Feb 2025 22:09:45 +0000</pubDate>
      <link>https://dev.to/zloeber/mise-jump-between-per-folder-dev-environments-like-a-wizard-19db</link>
      <guid>https://dev.to/zloeber/mise-jump-between-per-folder-dev-environments-like-a-wizard-19db</guid>
      <description>&lt;p&gt;Developing software across multiple programming languages and projects can feel like juggling flaming chainsaws while riding a unicycle. The additional cognitive load of getting things in place locally for your workstation for some other team's project can really slow a day down. Each language comes with its own ecosystem, version managers, and dependency hell. One project might require Python 3.11, another Node.js 18, and yet another Go 1.20. Keeping your local development environment in sync with these requirements is a nightmare. Containers like Docker can help, but they often feel heavy-handed for local development. And then there’s Nix—powerful, but let’s be honest, it’s like learning a new language just to manage your tools.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;asdf-vm&lt;/strong&gt;, the tool that promised simplicity. It was a breath of fresh air—a single tool to manage multiple runtime versions. But as I dug deeper, I stumbled upon &lt;strong&gt;aqua&lt;/strong&gt;, which added a layer of declarative configuration. Finally, I discovered &lt;strong&gt;mise&lt;/strong&gt; (formerly known as &lt;strong&gt;rtx&lt;/strong&gt;), and it felt like the missing piece of the puzzle. Mise combines the simplicity of asdf-vm with the declarative power of aqua, making it my go-to tool for managing local development environments.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk you through how I use &lt;a href="https://mise.jdx.dev" rel="noopener noreferrer"&gt;mise&lt;/a&gt; to tame the chaos of multi-language development, complete with a &lt;code&gt;mise.toml&lt;/code&gt; example to configure a project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Mise?
&lt;/h2&gt;

&lt;p&gt;Mise is a tool that allows you to define your project’s runtime requirements in a simple, declarative way. It’s like having a personal assistant who knows exactly which tools and versions you need for each project. Here’s why I love it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Declarative Configuration&lt;/strong&gt;: Define your tools and versions in a &lt;code&gt;mise.toml&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Language Support&lt;/strong&gt;: Works with Python, Node.js, Go, Ruby, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Package Manager Support:&lt;/strong&gt; Can install packages from asdf-vm, aqua, npm, pipx, ubi, go, gem, dotnet, cargo, spm, and vfox (vfox?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Setup&lt;/strong&gt;: No need to wrestle with containers or learn a new ecosystem like Nix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seamless Integration&lt;/strong&gt;: Works alongside your existing tools and workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment Support:&lt;/strong&gt; Can inject local &lt;code&gt;.env&lt;/code&gt; files into your environment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You get lots of wins using mise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: Using &lt;code&gt;mise.toml&lt;/code&gt; to Configure a Project
&lt;/h2&gt;

&lt;p&gt;Let’s say you’re working on a project developed by some maniac that requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11
&lt;/li&gt;
&lt;li&gt;Node.js 18&lt;/li&gt;
&lt;li&gt;Go 1.20&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, perhaps this project requires some environment variables as secrets in the git ignored &lt;code&gt;./.SECRETS.env&lt;/code&gt; as well as non-secret environment variables in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s how you can define these requirements in a &lt;code&gt;mise.toml&lt;/code&gt; file. You would drop this into the project folder you are working on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tools]&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.11"&lt;/span&gt;
&lt;span class="py"&gt;nodejs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"18"&lt;/span&gt;
&lt;span class="py"&gt;go&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.20"&lt;/span&gt;

&lt;span class="nn"&gt;[[env]]&lt;/span&gt;
&lt;span class="py"&gt;_.source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'./.env'&lt;/span&gt;
&lt;span class="py"&gt;PYTHONPATH&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"./src"&lt;/span&gt;
&lt;span class="py"&gt;NODE_ENV&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"development"&lt;/span&gt;

&lt;span class="nn"&gt;[[env]]&lt;/span&gt;
&lt;span class="py"&gt;_.source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'./.SECRETS.env'&lt;/span&gt;
&lt;span class="py"&gt;SOMETHING&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"nope"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this file in place, running &lt;code&gt;mise install -y&lt;/code&gt; will automatically install the specified versions of Python, Node.js, and Go. Mise also sets up the environment variables defined and sources in the secrets. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This is all predicated on mise being injected into your console session. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You may notice that the versions are at the minor semver level. This should net you the latest patch semver release for that software. You can, and probably should, pin these versions to the exact version for a project you maybe inheriting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Visualizing the Workflow with Mermaid
&lt;/h2&gt;

&lt;p&gt;Let’s break down the workflow with a Mermaid diagram:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eiqjcms0ltujnyjxmi1.png" alt="Mise workflow" width="798" height="661"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Managing development environments shouldn’t be a full-time job. With mise, you can spend less time configuring tools and more time writing code. It’s simple, declarative, and works seamlessly across multiple languages. Whether you’re a solo developer or part of a team, mise can help you standardize your development environment and reduce onboarding friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Give Mise a Try
&lt;/h2&gt;

&lt;p&gt;If you’re tired of juggling versions and wrestling with containers, give mise a shot. It’s a game-changer for multi-language development. Here’s how to get started:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mise.jdx.dev/getting-started.html" rel="noopener noreferrer"&gt;Install mise&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://mise.jdx.dev/install.sh | sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(~/.local/bin/mise activate zsh)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create a &lt;code&gt;mise.toml&lt;/code&gt; file in your project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;mise install&lt;/code&gt; and start coding!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Retrofitting An Existing Project
&lt;/h2&gt;

&lt;p&gt;Typically I'll just add the &lt;code&gt;mise.toml&lt;/code&gt; file at the project root including only the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used programming languages&lt;/li&gt;
&lt;li&gt;Build tools (jq, yq, task, et cetera)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I then add a local &lt;code&gt;./configure.sh&lt;/code&gt; file at the root like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# Check for GITHUB_TOKEN to not be rate limited&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"GITHUB_TOKEN is not set. Please set it and try again."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# check for mise&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; mise &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Please install mise first (run 'curl https://mise.run | sh')"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mise activate bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;## This is optional if you require any [env] sections be processed&lt;/span&gt;
&lt;span class="c"&gt;#mise settings set experimental true&lt;/span&gt;
&lt;span class="c"&gt;#mise trust&lt;/span&gt;

&lt;span class="c"&gt;# Install all dependencies&lt;/span&gt;
mise &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; I'd ensure you have the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; env var in place using your own created PAT. This will ensure rate limiting doesn't ruin your day.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have some unique tool that is not an asdf-vm plugin you can add software via any one of the &lt;a href=""&gt;back end package providers&lt;/a&gt; supported.  I've found that aqua may have some newer packages. But you can also try your luck with the &lt;a href="https://mise.jdx.dev/dev-tools/backends/ubi.html" rel="noopener noreferrer"&gt;&lt;code&gt;ubi&lt;/code&gt;&lt;/a&gt; backend. This one hits up GitHub releases and tries to guess the latest release for your architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I'm very happy with how well it integrates with my console sessions and shims the right application versions based on my location seamlessly. You can get rather fancy with the ability to source .env files (they are essentially bash scripts).&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus - Sops Integration
&lt;/h2&gt;

&lt;p&gt;You can have your secrets encrypted via sops and an age key and mise should handle the decryption entirely behind the scenes. I've created an example project that implements this feature &lt;a href="https://github.com/zloeber/mise-encryption-example" rel="noopener noreferrer"&gt;here&lt;/a&gt; for your convenience. This project uses a generated age key to encrypt a local &lt;code&gt;.secrets.env.json&lt;/code&gt; file for inclusion into the local environment via mise.&lt;/p&gt;




&lt;p&gt;Happy coding, and may your development environments always be easy to use!&lt;/p&gt;

</description>
      <category>development</category>
      <category>devops</category>
      <category>cli</category>
      <category>wizard</category>
    </item>
    <item>
      <title>Atmos - Wield Terraform Like a Boss</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Wed, 29 Jan 2025 19:21:56 +0000</pubDate>
      <link>https://dev.to/zloeber/atmos-wield-terraform-like-a-boss-3bfc</link>
      <guid>https://dev.to/zloeber/atmos-wield-terraform-like-a-boss-3bfc</guid>
      <description>&lt;p&gt;Terraform is great until you have to deal with state. As large state inherently will not scale you find that the more things grow the more state that needs to be managed and connected and otherwise understood. &lt;/p&gt;

&lt;p&gt;Atmos is a tool for this and so much more. This article will build on my prior terraform to opentofu encrypted local state example to introduce the use of atmos for deployment of my multi-state project. This time I'll change over to the &lt;a href="https://github.com/zloeber/tofu-exploration/tree/tofu-encrypted-atmos" rel="noopener noreferrer"&gt;&lt;code&gt;tofu-encrypted-atmos&lt;/code&gt; branch&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Atmos?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://atmos.tools/" rel="noopener noreferrer"&gt;Atmos&lt;/a&gt; is an opinionated infrastructure deployment tool from the great minds at &lt;a href="https://cloudposse.com/" rel="noopener noreferrer"&gt;CloudPosse&lt;/a&gt;. This team has a long history of releasing incredible Terraform modules and being deeply involved within the DevOps community. Erik has been running a weekly podcast/open office hours to talk all things DevOps for years now that I highly recommend people check out.&lt;/p&gt;

&lt;p&gt;Anyway, the point is that makers of this tool are really really good at flinging Terraform and have strong opinions on how to automate it. Atmos is the culmination of experienced gained by being in the trenches with infrastructure automation. It might be comparable to &lt;a href="https://terragrunt.gruntwork.io/" rel="noopener noreferrer"&gt;terragrunt&lt;/a&gt;, Terraform stacks (&lt;a href="https://developer.hashicorp.com/terraform/language/stacks" rel="noopener noreferrer"&gt;public beta&lt;/a&gt;), &lt;a href="https://github.com/cisco-open/stacks" rel="noopener noreferrer"&gt;Cisco's stacks&lt;/a&gt;, or even the &lt;a href="https://developer.hashicorp.com/terraform/cdktf" rel="noopener noreferrer"&gt;cdktf&lt;/a&gt; which also has a stack concept.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack Origins:&lt;/strong&gt; One may say that AWS CloudFormation is the origin story for treating infrastructure deployments as a 'stack'. It spawned AWS CDK which resulted in the libraries used to generate some other CDKs that build upon the stack concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding
&lt;/h2&gt;

&lt;p&gt;As always, there is a short learning curve to grok the atmos view of the world. I'll distill it down as best I can (please read more in their docs for a deeper dive). Let us start by level setting on vocabulary as it will help shortcut understanding of how to look at the atmos project structure layout. Here are some terraform terms and their Atmos equivalent.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Terraform&lt;/th&gt;
&lt;th&gt;Atmos&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;root module&lt;/td&gt;
&lt;td&gt;component&lt;/td&gt;
&lt;td&gt;State lives here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multiple root modules&lt;/td&gt;
&lt;td&gt;stack&lt;/td&gt;
&lt;td&gt;Multiple components grouped together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;group of available root modules&lt;/td&gt;
&lt;td&gt;component library&lt;/td&gt;
&lt;td&gt;Just a bunch of components in a logical group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;child module&lt;/td&gt;
&lt;td&gt;child module&lt;/td&gt;
&lt;td&gt;same as they ever were&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; A stack is effectively an environment.&lt;/p&gt;

&lt;p&gt;Atmos makes clever use of terraform workspaces for each component defined in a stack. This is pretty efficient and an entirely seamless use of terraform workspaces that looks a little like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F604mmp9koa5x2k7envh1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F604mmp9koa5x2k7envh1.png" alt="Atmos to terraform state workspace diagram" width="391" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this next diagram we'd have one state element for the localhost, cluster1, and cluster2 components in the dev workspace. We'd also have 1 state element in the baremetal  and cluster1 components for the prod workspace. This gives us 5 total state targets when completely deployed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8y8wa616urv2ncj2kjv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8y8wa616urv2ncj2kjv3.png" alt="Atmos to terraform state" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The component library concept allow for and additional vector to parameterize your deployments and allows for dependency mapping between disparate terraform states. &lt;/p&gt;

&lt;h2&gt;
  
  
  Adopting Atmos
&lt;/h2&gt;

&lt;p&gt;In order to accommodate for atmos I had to allow for some previously git ignored paths and trust that my openTofu encrypted state process was working properly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; I effectively relinquished the location of state files to atmos for local state file. I did end up adding a quick validation task unit test the state is encrypted for each deployment. &lt;code&gt;task test:state&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my root modules (aka. 'components') I had to remove all traces of the local backend configuration as atmos overwrites it otherwise (causing an endless loop of changed backend state migration approval prompts). The documentation for atmos goes over a slew of different state management schemas that allow for deep customized workflows tailored to an organization's structure.&lt;/p&gt;

&lt;p&gt;I also changed the base folders to comply with the atmos way of doing things and ended up with a basic project structure like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidja2vo9iu1vlfk281fy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidja2vo9iu1vlfk281fy.png" alt="My new folder structure" width="195" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I made almost no real changes to my modules or my base terraform but I did move them which required some validations. I also had to create the YAML scaffolding. This included the component library definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stacks/catalog/localhost.yaml&lt;/span&gt;
&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;localhost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost"&lt;/span&gt;
    &lt;span class="na"&gt;cluster1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster1&lt;/span&gt;
      &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost"&lt;/span&gt;
    &lt;span class="na"&gt;cluster2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster2&lt;/span&gt;
      &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stack definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# yaml-language-server: $schema=https://atmos.tools/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json&lt;/span&gt;
&lt;span class="c1"&gt;# stacks/deploy/dev.yaml&lt;/span&gt;

&lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;

&lt;span class="na"&gt;import&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;catalog/localhost&lt;/span&gt;

&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;localhost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev"&lt;/span&gt;
        &lt;span class="na"&gt;clusters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster1"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster2"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;secrets_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../../../secrets/dev"&lt;/span&gt;
    &lt;span class="na"&gt;cluster1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cluster_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster1"&lt;/span&gt;
        &lt;span class="na"&gt;key_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../../../secrets/dev/"&lt;/span&gt;
        &lt;span class="na"&gt;kubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../../../secrets/dev/cluster1_config"&lt;/span&gt;
    &lt;span class="na"&gt;cluster2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cluster_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster2"&lt;/span&gt;
        &lt;span class="na"&gt;key_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../../../secrets/dev/"&lt;/span&gt;
        &lt;span class="na"&gt;kubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../../../secrets/dev/cluster2_config"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And also the workflow to run them all as a set of additional atmos tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stacks/workflows/localhost.yaml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bring up and configure a few kind clusters&lt;/span&gt;
&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;up&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;Bring up the local environment&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply localhost -auto-approve&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply cluster1 -auto-approve&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply cluster2 -auto-approve&lt;/span&gt;

  &lt;span class="na"&gt;down&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;Tear it all down&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform destroy cluster2 -auto-approve&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform destroy cluster1 -auto-approve&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform destroy localhost -auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally the &lt;code&gt;atmos.yaml&lt;/code&gt; definition that points the binary to tofu (installed via mise)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;base_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;

&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;base_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;components/terraform"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tofu"&lt;/span&gt;
    &lt;span class="na"&gt;apply_auto_approve&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;deploy_run_init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;init_run_reconfigure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;auto_generate_backend_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;stacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;base_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stacks"&lt;/span&gt;
  &lt;span class="na"&gt;included_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deploy/*.yaml"&lt;/span&gt;
  &lt;span class="na"&gt;excluded_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/_defaults.yaml"&lt;/span&gt;
  &lt;span class="na"&gt;name_pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{stage}"&lt;/span&gt;

&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;base_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stacks/workflows&lt;/span&gt;

&lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dev/stderr"&lt;/span&gt;
  &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Info&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place and a few custom &lt;code&gt;Taskfile.yml&lt;/code&gt; tasks the entire infrastructure can be brought up or down with secured local encrypted state via 2 commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I use the taskfile for kicking things off but all workflows and tasks can simply be done via the atmos cli directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;task up
task down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Impressions
&lt;/h2&gt;

&lt;p&gt;Overall I'm appreciating this tool succinctly wrapping Terraform state operations into manageable and highly customizable deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is an interactive TUI app that will delight you to see when you get your first atmos configuration working properly (if you struggled to get it working that dopamine hit when the tui pops up is incredible...)&lt;/li&gt;
&lt;li&gt;There is OPA policy validation as well as jsonschema validation included. I love me some rego!&lt;/li&gt;
&lt;li&gt;Just about every aspect can be configured via declarative configuration.&lt;/li&gt;
&lt;li&gt;The tool's author's are heavily involved with the community and making regular improvements and significant feature additions.&lt;/li&gt;
&lt;li&gt;Can add custom commands and workflows to replace your tasker tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At first, the proposed file structure for Atmos might feel unfamiliar, but that’s only because so many projects lack a consistently enforced, programmatic structure. While any new system comes with a learning curve, these conventions ultimately reduce cognitive load and create efficiencies—especially in a team environment (Stick with it, and it’ll ‘click’ before you know it!).&lt;/li&gt;
&lt;li&gt;It felt quite difficult to get my existing deployment working with atmos. Yet I got it all working in an afternoon so 'difficult' may be relative here.&lt;/li&gt;
&lt;li&gt;Almost every aspect of a deployment is able to be customized but it often is not readily apparent just where to change things.&lt;/li&gt;
&lt;li&gt;Additional workflows will need to be created per-stack that you are deploying to automated the deployment of all the components within.&lt;/li&gt;
&lt;li&gt;Just about every aspect can be configured via a slew of YAML.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Interesting&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I'm only mildly uncomfortable with the fact that when atmos runs it generates local &lt;em&gt;.tfvar&lt;/em&gt; files in semi-deeply buried locations within the component folders. I added this to the &lt;code&gt;.gitignore&lt;/code&gt; list as I'm not certain they need to be there (and they do get recreated automatically).&lt;/li&gt;
&lt;li&gt;I dig that atmos supports vendoring (akin to caravel's &lt;a href="https://carvel.dev/vendir/" rel="noopener noreferrer"&gt;vendir&lt;/a&gt; app or the air-gap deployment tool &lt;a href="https://docs.zarf.dev/" rel="noopener noreferrer"&gt;zarf&lt;/a&gt;). But this is a manual affair of defining your vendored content.&lt;/li&gt;
&lt;li&gt;There are strong tie ins with one of my other favorite declarative manifest deployment tools, &lt;a href="https://helmfile.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;helmfile&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I really like this tool and see myself using it to bootstrap some other local infrastructure via Terraform. How about you? What tools are you using to manage your terraform deployments?&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>tofu</category>
      <category>devops</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>OpenTofu - Encrypted State + Git to Bootstrap Infrastructure</title>
      <dc:creator>Zachary Loeber</dc:creator>
      <pubDate>Sun, 26 Jan 2025 02:38:56 +0000</pubDate>
      <link>https://dev.to/zloeber/opentofu-encrypted-state-git-to-bootstrap-infrastructure-47lj</link>
      <guid>https://dev.to/zloeber/opentofu-encrypted-state-git-to-bootstrap-infrastructure-47lj</guid>
      <description>&lt;p&gt;In the evolving world of infrastructure-as-code (IaC), tools like OpenTofu are pushing boundaries, enabling developers to efficiently manage and deploy infrastructure. The OpenTofu team has been on a roll with new features to address some of the longest running complaints in the Terraform community.&lt;/p&gt;

&lt;p&gt;Two recent standout features are encrypted state and provider iteration. Both are intriguing and deserve a closer examination to understand their potential impact (and limitations) in real-world scenarios. &lt;/p&gt;

&lt;p&gt;In this article I'll show how to maintain infrastructure bootstrap code and its state in git without the need for a third party vault, cloud storage, or additional secret sprawl. I lay out fully working examples of how this might be done both with standard Terraform and also via OpenTofu's encrypted state feature.&lt;/p&gt;

&lt;p&gt;The example project I'll be covering deploys a couple of local kind clusters with ArgoCD installed. It then creates and pushes a ssh public/private key pair as Kubernetes local secrets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Working Example - Part 1 (Terraform)
&lt;/h2&gt;

&lt;p&gt;To explore this further I'll start with a deployment done entirely via Terraform. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; To follow along with things you should clone this repo locally and run in any local bash/zsh shell with docker running. Further configuration information can be found in the project readme.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;PROJECT&lt;/strong&gt;: &lt;a href="https://github.com/zloeber/tofu-exploration/" rel="noopener noreferrer"&gt;tofu-exploration&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;BRANCH&lt;/strong&gt;: main&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;main&lt;/code&gt; branch includes manifests for deploying 2 kind clusters side by side in the &lt;code&gt;infrastructure/environments/local&lt;/code&gt; folder. The state is stored for each component as separate Terraform state files in the &lt;code&gt;./secrets&lt;/code&gt; folder. This folder is then targeted with &lt;code&gt;sops&lt;/code&gt; to encrypt contents within.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bring cluster1 and cluster2 up&lt;/span&gt;
task deploy:all

&lt;span class="c"&gt;# Here you should review secrets and other state stuff in ./secrets.&lt;/span&gt;
&lt;span class="c"&gt;# Don't commit this to git yet!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this has completed you should have a handful of files in the local &lt;code&gt;./secrets&lt;/code&gt; folder including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes configuration files with full rights to your created clusters&lt;/li&gt;
&lt;li&gt;Additional per-cluster public and private keys&lt;/li&gt;
&lt;li&gt;Infrastructure and per-cluster state files with all applied Terraform (including the generated ssh private keys and other sensitive information)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Encrypting Local State
&lt;/h3&gt;

&lt;p&gt;Both plan and state files are inherently plain text. We can encrypt the state files easily enough though. To start you will need some private key that is kept locally. I've chosen age keys with &lt;a href="https://github.com/getsops/sops" rel="noopener noreferrer"&gt;sops&lt;/a&gt;. You could use PGP or anything that sops supports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;task | &lt;span class="nb"&gt;grep &lt;/span&gt;sops &lt;span class="c"&gt;# Show a list of our convenience tasks&lt;/span&gt;
task sops:show &lt;span class="c"&gt;# Show all the variables setup for the tasks&lt;/span&gt;

task sops:age:keygen &lt;span class="c"&gt;# Generate a local age key&lt;/span&gt;
task sops:init &lt;span class="c"&gt;# Initialize this project repo with your public age key&lt;/span&gt;
task encrypt:all &lt;span class="c"&gt;# Encrypt every file in the ./secrets folder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can now review the secrets files and see that they have all been encrypted. Binary looking files like ssh keys will be converted to JSON format with the information required to decrypt them baked into the metadata (obviously minus our private age key).&lt;/p&gt;

&lt;p&gt;With the age private key in &lt;code&gt;~/.config/sops/age/keys.txt&lt;/code&gt; and all secrets files are encrypted you can now safely commit your changes to git.&lt;/p&gt;

&lt;p&gt;When you need to decrypt and run terraform operations again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;task decrypt:all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; You can and should use pre-commit hooks to prevent accidentally committing your secrets!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Clean Up
&lt;/h3&gt;

&lt;p&gt;To remove the clusters and clean up your work in preparation for opentofu run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Tear it down&lt;/span&gt;
task destroy:all
task clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Working Example - Part 2 (OpenTofu)
&lt;/h2&gt;

&lt;p&gt;I created, then updated the &lt;code&gt;tofu-encryption&lt;/code&gt; branch from &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PROJECT&lt;/strong&gt;: &lt;a href="https://github.com/zloeber/tofu-exploration/" rel="noopener noreferrer"&gt;tofu-exploration&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;BRANCH&lt;/strong&gt;: tofu-encryption&lt;/p&gt;

&lt;p&gt;This is the same deployment is done using opentofu's encrypted state instead of sops. First big update is that we are changing the binary used in our main &lt;code&gt;Taskfile.yml&lt;/code&gt; definition to tofu.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt; I did try to use the VSCode plugin for OpenTofu but it was not very helpful for the more recent features (like the encryption block).&lt;/p&gt;
&lt;h3&gt;
  
  
  State/Plan Encryption
&lt;/h3&gt;

&lt;p&gt;As &lt;a href="https://opentofu.org/docs/language/state/encryption/" rel="noopener noreferrer"&gt;per the docs&lt;/a&gt; we can encrypt state and plan data with native opentofu.&lt;/p&gt;

&lt;p&gt;This can be enabled via the &lt;code&gt;TF_ENCRYPTION&lt;/code&gt; environment variable or in the terraform block. The way this works is that you define a &lt;code&gt;method&lt;/code&gt; which can optionally contain key providers or other configuration for encryption. The key providers and methods available are not so large currently but it is still enough to get along.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Vault Transit Support&lt;/strong&gt; is not available if vault is running beyond 1.14 (the license change). It is experimental for openbao otherwise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Anyway, the methods are assigned to the &lt;code&gt;state&lt;/code&gt; and/or &lt;code&gt;plan&lt;/code&gt; terraform definitions as either the primary or backup encryption types.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fssj89r1477m1488mxse5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fssj89r1477m1488mxse5.png" alt="Oversimple diagram of components making up OpenTofu's encryption configuration" width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can infer that your entry point for secret zero in a local file based state encryption will be that passphrase. We need to use something greater than 16 characters and private. The age private key can be used for this easily enough by setting the &lt;code&gt;TF_VAR_state_passphrase&lt;/code&gt; variable I created just for this purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important!&lt;/strong&gt; Ensure you have your local age key pair created with &lt;code&gt;task sops:age:keygen&lt;/code&gt; (existing key will always be preserved).&lt;/p&gt;

&lt;p&gt;With this in place I updated the local &lt;code&gt;Taskfile.yml&lt;/code&gt; manifest to automatically source the private key value into that environment variable so it could be used as the encryption passkey in the relevant terraform block. The result is something 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;variable&lt;/span&gt; &lt;span class="s2"&gt;"state_passphrase"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"value of the passphrase used to encrypt the state file"&lt;/span&gt;
  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&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;state_passphrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The passphrase must be at least 16 characters long."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.9.0"&lt;/span&gt;
  &lt;span class="nx"&gt;encryption&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;## Step 1: Add the desired key provider:&lt;/span&gt;
    &lt;span class="nx"&gt;key_provider&lt;/span&gt; &lt;span class="s2"&gt;"pbkdf2"&lt;/span&gt; &lt;span class="s2"&gt;"mykey"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;passphrase&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;state_passphrase&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;## Step 2: Set up your encryption method:&lt;/span&gt;
    &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="s2"&gt;"aes_gcm"&lt;/span&gt; &lt;span class="s2"&gt;"passphrase"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;key_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pbkdf2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mykey&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="s2"&gt;"unencrypted"&lt;/span&gt; &lt;span class="s2"&gt;"insecure"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;# enforced = true&lt;/span&gt;
      &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aes_gcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passphrase&lt;/span&gt;
      &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unencrypted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insecure&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;# enforced = true&lt;/span&gt;
      &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aes_gcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passphrase&lt;/span&gt;
      &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unencrypted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insecure&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;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;=&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;"tehcyx/kind"&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;"0.7.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;backend&lt;/span&gt; &lt;span class="s2"&gt;"local"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../../secrets/local/infrastructure_tfstate.json"&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;If we run the deployment with no further changes then it automatically encrypts the terraform state files when we deploy via &lt;code&gt;task deploy:all&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The SSH keys I was generating and encrypting via sops before are not covered in this case. But that data is sourced from our state so we simply start ignoring them via &lt;code&gt;.gitignore&lt;/code&gt; knowing we can always recreate them later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interesting:&lt;/strong&gt; Because the kind provider I used doesn't track the local config file resource when it gets created, I needed to make changes to isolate the kubeconfig files to their own generated file resources instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With this in place we should be able to push state up to your git repo directly after any kind of state altering task has been done, clone it later to another machine with the same age private key, and run through the deployment lifecycle again seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impressions
&lt;/h2&gt;

&lt;p&gt;I'm really happy with how fluid encrypted state works and will definitely be using it for some personal projects. Remember to keep all your secrets in state when doing this, be extra careful of what you commit, and of course protect/backup that private age key.&lt;/p&gt;

</description>
      <category>tofu</category>
      <category>terraform</category>
      <category>infrastructureascode</category>
      <category>security</category>
    </item>
  </channel>
</rss>
