<?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: Innovation Process Technology AG (ipt)</title>
    <description>The latest articles on DEV Community by Innovation Process Technology AG (ipt) (@ipt).</description>
    <link>https://dev.to/ipt</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%2Forganization%2Fprofile_image%2F9906%2F5d6310d9-6d88-4bf1-a012-dd8f8b7f8ac7.png</url>
      <title>DEV Community: Innovation Process Technology AG (ipt)</title>
      <link>https://dev.to/ipt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ipt"/>
    <language>en</language>
    <item>
      <title>Generative UI - The Future of Human-AI Interaction</title>
      <dc:creator>Weili Gao</dc:creator>
      <pubDate>Thu, 28 May 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/ipt/generative-ui-the-future-of-human-ai-interaction-5eol</link>
      <guid>https://dev.to/ipt/generative-ui-the-future-of-human-ai-interaction-5eol</guid>
      <description>&lt;p&gt;This article was co-authored with my colleague&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__3956086"&gt;
    &lt;a href="/benjamin_buergisser" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3956086%2F9608b1cf-6ac5-40ae-9f91-1b6b6aac5329.jpg" alt="benjamin_buergisser image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/benjamin_buergisser"&gt;Benjamin Bürgisser&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/benjamin_buergisser"&gt;Hi, I'm Benjamin, a software engineer and IT architect based near Zurich, Switzerland. 
https://b-dimension.com/&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Generative UI is the next evolution of AI interaction, moving beyond text to create dynamic, agent-driven user interfaces. We explored three key patterns - Static (AG-UI), Declarative (A2UI), and Open-ended UI - and used the CopilotKit framework to build two demos: a Tic-Tac-Toe assistant, and a 2D RPG with LangGraph NPC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Generative UI?
&lt;/h2&gt;

&lt;p&gt;Today’s AI agents primarily communicate through text. While revolutionary, this often forces humans to interact with a conversational interface, even for tasks that would be more efficient or intuitive within a traditional graphical user interface.&lt;/p&gt;

&lt;p&gt;Generative UI is the paradigm shift that enables AI agents to dynamically generate the UI elements required to complete a task. Instead of the user describing what they see, the agent reads the UI state directly and acts through it - clicking buttons, moving characters, proposing decisions.&lt;/p&gt;

&lt;p&gt;This shift blends the power of LLMs with familiar interactive components and is critical for implementing &lt;strong&gt;human-in-the-loop&lt;/strong&gt; workflows, where the agent can propose an action but requires human confirmation via an interactive UI before execution. This ensures control and a better user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Core Patterns of Generative UI
&lt;/h2&gt;

&lt;p&gt;Three core patterns exist for building Generative UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static Generative UI&lt;/strong&gt; (&lt;a href="https://docs.ag-ui.com/introduction" rel="noopener noreferrer"&gt;AG-UI&lt;/a&gt;): the agent generates a command (like a function call) for a predefined frontend tool. The UI for that tool is already built and the agent simply triggers it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative Generative UI&lt;/strong&gt; (&lt;a href="https://a2ui.org/" rel="noopener noreferrer"&gt;A2UI&lt;/a&gt;, &lt;a href="https://docs.copilotkit.ai/learn/generative-ui/specs/open-json-ui" rel="noopener noreferrer"&gt;Open-JSON-UI&lt;/a&gt;): the agent generates a structured data format (like JSON) that describes the UI it wants to render (e.g., a form, a chart, or a list). The frontend then interprets this data and renders the elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-ended Generative UI&lt;/strong&gt; (&lt;a href="https://modelcontextprotocol.io/extensions/apps/overview" rel="noopener noreferrer"&gt;MCP Apps&lt;/a&gt;): the agent generates raw, open-ended content, such as a block of custom HTML/CSS, which is then rendered directly in the UI. This is the most flexible approach but requires careful security measures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The different patterns are not mutually exclusive and can be complementary, e.g. both AG-UI and A2UI are used by the CopilotKit framework for implementing Generative UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Generative UI Frameworks
&lt;/h2&gt;

&lt;p&gt;The Generative UI approach is seeing rapid development from several frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.copilotkit.ai/" rel="noopener noreferrer"&gt;CopilotKit:&lt;/a&gt; a comprehensive framework for building agent-driven interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://trypillar.com/" rel="noopener noreferrer"&gt;Pillar:&lt;/a&gt; a simpler, headless framework focused on the core logic of agent tool routing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vercel.com/docs/ai-sdk" rel="noopener noreferrer"&gt;Vercel AI SDK:&lt;/a&gt; provides foundational support for streaming and model integrations, and is used as the runtime for CopilotKit's built-in agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introducing CopilotKit
&lt;/h2&gt;

&lt;p&gt;We picked CopilotKit for building our two demos. It is an open-source (MIT) framework with 30k+ stars on GitHub that connects AI agents to your frontend. It is the company behind the AG-UI protocol.&lt;/p&gt;

&lt;p&gt;Its core concepts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Readables:&lt;/strong&gt; expose app state to the agent (board positions, inventories, distances).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions:&lt;/strong&gt; let the agent do things in your UI (make a move, take an item, spawn a reward).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat UI:&lt;/strong&gt; a themed sidebar or popup, with streaming and tool-call rendering, out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It supports React/Next.js and Angular, and works with agent backends like LangGraph or Microsoft Agent Framework. CopilotKit is partnered with Microsoft and Google for development on the AG-UI and A2UI protocols.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Demos
&lt;/h2&gt;

&lt;p&gt;To gain hands-on experience with Generative UI, we have developed two demos with CopilotKit: a Tic-Tac-Toe Assistant and a 2D RPG. They demonstrate how a front-end application can expose its state and functionality (tools) to an agent, allowing the agent to perform actions that are visible and interactive for the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo 1: Tic-Tac-Toe Assistant
&lt;/h3&gt;

&lt;p&gt;In our first demo we built a tic-tac-toe assistant that coaches the player at the immensely sophisticated game of tic-tac-toe. The agent can suggest moves (with an accept/reject dialog for human-in-the-loop), annotate the board with threats and opportunities, and render a custom "Coach Card" via generated HTML. The demo illustrates key Generative UI features by applying the patterns discussed above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Move Suggestion (AG-UI + Human-in-the-Loop):&lt;/strong&gt; The agent calls a predefined &lt;code&gt;proposeNextMove&lt;/code&gt; tool, and the result is rendered as an "Accept / Reject" dialog (&lt;code&gt;renderAndWaitForResponse&lt;/code&gt;), requiring user confirmation before the move is made.&lt;/li&gt;
&lt;/ul&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%2Ffv2xit7hzlgz66clpejq.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%2Ffv2xit7hzlgz66clpejq.png" alt="AI move suggestions" width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Board Annotations (AG-UI):&lt;/strong&gt; The agent calls an &lt;code&gt;analyzeBoard&lt;/code&gt; tool, which returns data used to render overlay symbols (threats, opportunities) directly onto the existing UI components (&lt;code&gt;Board.tsx&lt;/code&gt;/&lt;code&gt;Square.tsx&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&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%2F08en7ql7nrug5oa1ma1s.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%2F08en7ql7nrug5oa1ma1s.png" alt="Board annotations" width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coach Card (Open-ended Generative UI):&lt;/strong&gt; The agent calls a &lt;code&gt;renderCoachCard&lt;/code&gt; tool, which generates fully custom HTML for the "Coach Card" component, rendered using a secure mechanism (&lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; with DOMPurify).&lt;/li&gt;
&lt;/ul&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%2Fc3re6uoj0pntou7cohp7.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%2Fc3re6uoj0pntou7cohp7.png" alt="Coach card" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared Game State (Agent Context / Readable State):&lt;/strong&gt; The &lt;code&gt;useAgentContext&lt;/code&gt; hook is used to publish the current board state, player, and winner status, giving the agent a "readable state" of the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application is structured as a Next.js app, with all frontend logic (tools, UI) in the browser, communicating with the backend API route (&lt;code&gt;/api/copilotkit&lt;/code&gt;) which hosts the CopilotKit’s built-in agent and handles the model interaction (Azure OpenAI &lt;code&gt;gpt-5-mini&lt;/code&gt; via &lt;code&gt;@ai-sdk/azure&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This demo validated that exposing React state as readables and defining actions as tools is a clean, working pattern. The built-in agent made setup fast - readables and the instructions prop on the chat sidebar just worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo 2: 2D RPG with LangGraph NPC
&lt;/h3&gt;

&lt;p&gt;For the second demo we wanted to go deeper: custom system prompts, richer agent logic, and a more complex scenario. We built a 2D RPG where the player walks around a medieval world collecting bananas, berries, and crystals. An NPC - powered by a LangGraph agent - acts as a quest giver. The agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;See the game state: positions, inventories, objects on the map, player–NPC distance.&lt;/li&gt;
&lt;li&gt;Move the NPC through the world using A* pathfinding around obstacles.&lt;/li&gt;
&lt;li&gt;Give quests dynamically based on what's available on the map.
Take items from the player and reward coins for completed quests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this runs through CopilotKit's readables and actions - the agent never touches the DOM directly; it just reads structured state and calls named functions.&lt;/p&gt;

&lt;h4&gt;
  
  
  A Quest in Three Acts
&lt;/h4&gt;

&lt;p&gt;Nothing is scripted. The NPC reads the world state, decides what to ask for, and reacts to whatever the player actually does.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Act 1 - The Quest:&lt;/strong&gt; We ask the NPC for a quest. It inspects the map and decides: "Bring me 3 crystals." No quest table - just the agent reasoning over game state.&lt;/li&gt;
&lt;/ul&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%2Fddknwapnldfw6x7azphb.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%2Fddknwapnldfw6x7azphb.png" alt="We ask the NPC for a quest. It inspects the map and decides: " width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Act 2 - The Pivot:&lt;/strong&gt; Instead of crystals, we grab a banana and call the NPC over. It pathfinds around the brick walls to reach us. In a scripted game, offering the wrong item would be a dead end. Here, the agent goes off-script and reluctantly accepts the banana anyway.&lt;/li&gt;
&lt;/ul&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%2F12ws1f32oneivhg44tm9.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%2F12ws1f32oneivhg44tm9.png" alt="Instead of crystals, we grab a banana and call the NPC over. It pathfinds around the brick walls to reach us." width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Act 3 - The Reward:&lt;/strong&gt; We ask for a reward. The agent decides a single coin is fair, spawns it next to the NPC, and we pick it up.&lt;/li&gt;
&lt;/ul&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%2Fzn3i7nsblm3b9ff4lf0q.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%2Fzn3i7nsblm3b9ff4lf0q.png" alt="We ask for a reward. The agent decides a single coin is fair, spawns it next to the NPC, and we pick it up." width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The NPC feels more real because we can freely talk to it and it makes decisions on the fly - exactly the kind of dynamic interaction that's hard to achieve with traditional game scripting.&lt;/p&gt;

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

&lt;p&gt;Summary of what we've learned from the demos.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Worked Well
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The readable/action model is clean.&lt;/strong&gt; You declare what the agent can see (&lt;code&gt;useCopilotReadable&lt;/code&gt;) and do (&lt;code&gt;useCopilotAction&lt;/code&gt;) right next to the React components that own the state. CopilotKit handles serialization, transport, and tool binding. It feels like a natural extension of React's component model - state flows down, actions flow up, and the agent plugs into that loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The chat UI saved real time.&lt;/strong&gt; A themed sidebar with message history, streaming, and tool-call rendering - for free. We styled it to match our medieval parchment theme with a few CSS custom properties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangGraph integration is straightforward.&lt;/strong&gt; Define a graph, export it, point the Next.js route at the deployment URL. CopilotKit's state annotation passes readables and actions through cleanly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Tripped Us Up
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Readables don't auto-inject with custom agents.&lt;/strong&gt; This was the biggest surprise. With the built-in agent (as in the Tic-Tac-Toe demo), &lt;code&gt;useCopilotReadable&lt;/code&gt; values appear in the model's context automatically. With a custom LangGraph agent, they arrive in &lt;code&gt;state.copilotkit.context&lt;/code&gt; - but you have to manually inject them into your system message. Actions worked immediately; readables silently did nothing until we traced the issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;instructions&lt;/code&gt; prop is ignored with custom agents.&lt;/strong&gt; We set system instructions via CopilotSidebar's instructions field and they were silently dropped. This is a &lt;a href="https://github.com/CopilotKit/CopilotKit/issues/3037" rel="noopener noreferrer"&gt;known open CopilotKit issue&lt;/a&gt;. The workaround: define instructions in the agent's system message directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React's async batching vs. rapid tool calls&lt;/strong&gt; - when the agent fired two rapid calls (e.g. "take 1 berry" then "take 3 crystals"), the second read stale state because React hasn't re-rendered yet. We had to move from useEffect-based ref syncing to immediate ref updates - a useful pattern when bridging async agent actions with React state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Is Using CopilotKit Worth It?
&lt;/h3&gt;

&lt;p&gt;Couldn't we just append state to the user's message and define function-calling tools on our own? Yes - but CopilotKit gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A structured protocol&lt;/strong&gt; - AG-UI standardizes the agent–frontend connection so you don't reinvent the wheel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full chat framework&lt;/strong&gt; - sidebar, streaming, theming. Non-trivial to build and maintain yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A component-level API&lt;/strong&gt; - readables and actions co-located with the components that own the state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a quick prototype, rolling your own is fine. For anything that will grow - multiple agents, generative UI patterns, shared state - a framework pays off.&lt;/p&gt;

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

&lt;p&gt;Generative UI represents an exciting step forward in AI-powered applications, moving beyond simple text generation to dynamic, context-aware user interaction. Our experience with CopilotKit in developing the two demos validated the effectiveness of the AG-UI and open-ended patterns for creating engaging and powerful Human-in-the-Loop experiences. As frameworks mature, Generative UI is poised to become a core part of how developers integrate AI into modern web applications.&lt;/p&gt;

</description>
      <category>ui</category>
      <category>frontend</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>From Rot to Rhythm: How We Rebuilt Our Documentation Culture with Docusaurus</title>
      <dc:creator>Vincent von Büren</dc:creator>
      <pubDate>Tue, 19 May 2026 06:21:49 +0000</pubDate>
      <link>https://dev.to/ipt/from-rot-to-rhythm-how-we-rebuilt-our-documentation-culture-with-docusaurus-2d16</link>
      <guid>https://dev.to/ipt/from-rot-to-rhythm-how-we-rebuilt-our-documentation-culture-with-docusaurus-2d16</guid>
      <description>&lt;h1&gt;
  
  
  The Forgotten Layer
&lt;/h1&gt;

&lt;p&gt;Every infrastructure has a critical layer that doesn't show up on dashboards, but without it, everything slows down: &lt;strong&gt;documentation&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;We learned this the hard way.&lt;/p&gt;

&lt;p&gt;A little background for you. My team and I provide cloud services on a private cloud. This means that our customers are developers. We deliver platform features, portal functionality, maintenance, and infrastructure services. Over the last years, we invested heavily in automation and feature delivery — the things every PO dreams of. That focus allowed us to onboard more and more platforms onto our cloud service offering.&lt;/p&gt;

&lt;p&gt;But there was one thing we consistently deprioritized: documentation.&lt;br&gt;
And our customer-facing service docs paid the price.&lt;/p&gt;

&lt;p&gt;For years, it lived in Confluence spaces — large, tangled forests of pages where knowledge went to die. And of course, search functionality is also particularly bad using Confluence. So whenever a client reached out and asked if this part of the service information was still valid, the advice was always the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I am not sure, let me double check."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that worked — until it didn't.&lt;/p&gt;

&lt;p&gt;This post is not just a reflection. At the end, I will also share a Docusaurus scaffold we built internally — including linting, theming, and contribution workflows — so you can replicate this approach without reinventing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Slow Decay
&lt;/h2&gt;

&lt;p&gt;Documentation debt doesn't scream. It whispers.&lt;/p&gt;

&lt;p&gt;At first, it was a broken link here, an outdated diagram there. Nothing dramatic. But as our platform grew, the gap between &lt;strong&gt;what we shipped&lt;/strong&gt; and &lt;strong&gt;what we documented&lt;/strong&gt; widened.&lt;/p&gt;

&lt;p&gt;Onboarding stretched from a week to months.&lt;br&gt;
Customers filed support tickets for information that &lt;em&gt;should have been one search away&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When we looked closer, the symptoms all pointed to the same root cause:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docs lived outside the development flow&lt;/li&gt;
&lt;li&gt;Ownership was unclear&lt;/li&gt;
&lt;li&gt;Updating them felt like overhead&lt;/li&gt;
&lt;li&gt;And sorry for the dinosaurs out there: Confluence! &lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The Moment Everything Snapped
&lt;/h2&gt;

&lt;p&gt;The real tipping point came in the form of a short email from a customer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This page hasn't been updated since 2022.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We checked. They were right.&lt;/p&gt;

&lt;p&gt;Behind that single outdated page lay dozens more in similar shape. We could have patched the page and moved on. But deep down, we knew:&lt;br&gt;
&lt;strong&gt;this wasn't a page problem — it was a structural problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the day we stopped treating documentation as a side activity. We started treating it as part of the product.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Why Confluence Couldn't Keep Up
&lt;/h2&gt;

&lt;p&gt;Confluence had served us well in the early years. But we had outgrown it.&lt;/p&gt;

&lt;p&gt;Here's why it broke down at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disconnected from development:&lt;/strong&gt; Docs lived in a separate place, updated weeks after features shipped (if at all).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevEx:&lt;/strong&gt; Adding interactive code snippets, or embedding multi-media content is all possible in Confluence, but the writing is cumbersome. Markdown or AsciDocs for the win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No versioning:&lt;/strong&gt; We couldn't align docs with specific releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search pain:&lt;/strong&gt; Finding anything meant traversing endless page trees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No customer contribution:&lt;/strong&gt; Feedback loops were slow, often going nowhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No guardrails:&lt;/strong&gt; No linting, no validation, no CI — which meant quality drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And perhaps most importantly:&lt;br&gt;
&lt;strong&gt;Confluence didn't create shared ownership.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was "someone else's job."&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Before the Tool Came the Story
&lt;/h2&gt;

&lt;p&gt;This might sound counterintuitive for an engineering team, but &lt;strong&gt;we didn't start with a tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We started with a &lt;strong&gt;story&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If documentation isn't close to the code, it will always be outdated."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This became our North Star. We repeated it in every retro, every cross-team architecture session, and every leadership conversation.&lt;/p&gt;

&lt;p&gt;We didn't want just better docs. We wanted documentation to become part of the way we build — fast, iterative, owned by everyone, and open to customers.&lt;/p&gt;

&lt;p&gt;Once that vision was clear, the tooling decision became much easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Choosing the Right Documentation Platform
&lt;/h2&gt;

&lt;p&gt;We ran a structured evaluation of several tools. We needed something that could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git-native workflows&lt;/li&gt;
&lt;li&gt;Versioning&lt;/li&gt;
&lt;li&gt;Fast performance for customers&lt;/li&gt;
&lt;li&gt;Low barrier to contribution&lt;/li&gt;
&lt;li&gt;Enough flexibility without becoming a framework project&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Confluence&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Familiar, structured&lt;/td&gt;
&lt;td&gt;Poor dev integration, outdated model&lt;/td&gt;
&lt;td&gt;❌ Outgrown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read the Docs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple, Sphinx-based, versioning support&lt;/td&gt;
&lt;td&gt;Python-focused, limited customization&lt;/td&gt;
&lt;td&gt;⚠ Too rigid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MkDocs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lightweight, good search, fast&lt;/td&gt;
&lt;td&gt;Less UI flexibility&lt;/td&gt;
&lt;td&gt;⚠ Great but limited for branding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hugo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very fast, flexible&lt;/td&gt;
&lt;td&gt;Requires more setup, not docs-first&lt;/td&gt;
&lt;td&gt;⚠ Too generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docusaurus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MDX, versioning, React components, fast CI/CD integration&lt;/td&gt;
&lt;td&gt;Initial setup heavier&lt;/td&gt;
&lt;td&gt;✅ Perfect balance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We chose &lt;strong&gt;Docusaurus&lt;/strong&gt; because it aligned with how we already worked: Git, Markdown, pull requests, CI/CD, and reusable components.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Understanding Docusaurus (for those new to it)
&lt;/h2&gt;

&lt;p&gt;At its core, &lt;strong&gt;Docusaurus&lt;/strong&gt; is a static site generator designed specifically for documentation.&lt;/p&gt;

&lt;p&gt;It combines Markdown with React — giving you the simplicity of writing content in &lt;code&gt;.md&lt;/code&gt; files and the flexibility of building custom UI when needed.&lt;/p&gt;

&lt;p&gt;Key features that mattered for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versioning:&lt;/strong&gt; Tie docs to releases — a feature Confluence never had.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MDX support:&lt;/strong&gt; Mix Markdown with React components for rich documentation (feels funky, but kind of cool).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search:&lt;/strong&gt; Fast, extensible, works out of the box (Algolia or Typesense).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom plugins:&lt;/strong&gt; Integrate linters, search, CI checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; Our site loads fast — customers notice that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Familiar workflow:&lt;/strong&gt; Pull requests, code review, previews — like any other repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For our engineers, writing docs started feeling less like "extra work" and more like writing code. Some even said that documentation started to be &lt;strong&gt;fun&lt;/strong&gt;. &lt;/p&gt;




&lt;h2&gt;
  
  
  8. Our Docusaurus Documentation Platform
&lt;/h2&gt;

&lt;p&gt;Instead of treating documentation as a collection of pages, we built a documentation platform on top of Docusaurus.&lt;/p&gt;

&lt;p&gt;The goal was simple: any team should be able to start migrating from Confluence to Docs-as-Code without needing hand-holding.&lt;/p&gt;

&lt;p&gt;Our Docusaurus came with structure, guidance, and guardrails that made contribution predictable and safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  What our Docusaurus setup provides
&lt;/h3&gt;

&lt;p&gt;Our documentation platform includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clear structure and navigation&lt;/strong&gt;: A predefined information architecture so teams don’t invent their own page trees.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Built-in tutorials&lt;/strong&gt;: Step-by-step guides that explain how to migrate content from Confluence, how to structure pages, editing guidelines, and how to use MDX components correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Guardrails by default&lt;/strong&gt;: ESLint &amp;amp; Prettier for consistent formatting&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vale&lt;/strong&gt; for terminology, style, and writing quality&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI checks&lt;/strong&gt; that prevent broken builds or low-quality content from being merged&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reusable MDX components&lt;/strong&gt;: Components like , , and warnings for deprecations helped teams present information consistently without custom markup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Search and discoverability&lt;/strong&gt;: Fast, global search made content immediately usable for customers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CI/CD with preview environments&lt;/strong&gt;: Every pull request gets a preview deployment so contributors can see exactly what they are about to publish.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Theming and branding&lt;/strong&gt;: Documentation looks and feels like part of the platform, not an afterthought.&lt;/p&gt;

&lt;p&gt;The result: teams could migrate their content incrementally, at their own pace, without breaking anything — and without needing a central documentation team to mediate every change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing is Caring!
&lt;/h3&gt;

&lt;p&gt;Later in this post, we'll share a generic version of this setup — a reusable Docusaurus scaffold based on what we built internally. It includes the same structure, guardrails, and conventions, so your team can adopt Docs-as-Code without starting from scratch&lt;/p&gt;




&lt;h2&gt;
  
  
  9. The First Migration
&lt;/h2&gt;

&lt;p&gt;We started small: one service, one team.&lt;/p&gt;

&lt;p&gt;We rewrote only the most accessed and most broken pages. Minimal navigation. Strict linting. Then we shipped.&lt;/p&gt;

&lt;p&gt;Then we shipped it.&lt;/p&gt;

&lt;p&gt;The response surprised us.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customers noticed the speed&lt;/li&gt;
&lt;li&gt;Engineers fixed docs in the same PR as code&lt;/li&gt;
&lt;li&gt;Reviews made documentation visible again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Momentum follows. What used to live across multiple Confluence spaces consolidated into a single, structured documentation system.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Documentation as an Enabled Ecosystem
&lt;/h2&gt;

&lt;p&gt;The biggest shift wasn't technical — it was systemic.&lt;/p&gt;

&lt;p&gt;Documentation became a shared interface between platform teams, SREs, portal developers, and customers. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It enabled self-service instead of support load&lt;/li&gt;
&lt;li&gt;It reduced onboarding friction &lt;/li&gt;
&lt;li&gt;It made platform capabilities — and gaps — visible and discoverable&lt;/li&gt;
&lt;li&gt;It allowed feedback to flow directly into improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: documentation stopped being static knowledge and became part of the platform ecosystem itself.&lt;/p&gt;

&lt;p&gt;In hindsight, this aligns closely with what platform engineering maturity models describe as an enabled ecosystem: teams don't just consume the platform — they actively shape it.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. What Surprised Us Most
&lt;/h2&gt;

&lt;p&gt;When we moved to Docs as Code, we expected better structure.&lt;br&gt;
What we didn't expect was how it would &lt;strong&gt;bring teams together&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SRE engineers and portal developers now speak the same documentation language. Platform teams see their work reflected directly in docs. Customers trust what they read again.&lt;/p&gt;

&lt;p&gt;The act of &lt;strong&gt;writing and reviewing together&lt;/strong&gt; created something Confluence never could: &lt;strong&gt;a shared documentation identity&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Lessons Learned Along the Way
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with a narrative, not a tool.&lt;/strong&gt;&lt;br&gt;
Align teams on &lt;em&gt;why&lt;/em&gt; you're doing this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't migrate everything.&lt;/strong&gt;&lt;br&gt;
Migrate the docs that matter most first. Cut the old and ugly and don't be afraid to rewrite stuff&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make contribution effortless.&lt;/strong&gt;&lt;br&gt;
A good scaffold lowers the barrier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automate quality.&lt;/strong&gt;&lt;br&gt;
Vale, CI, formatting — make good docs the default, not the exception.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Documentation is never done.&lt;/strong&gt;&lt;br&gt;
Build habits, not one-time migrations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customers are collaborators.&lt;/strong&gt;&lt;br&gt;
Give them a way to contribute — it builds trust.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  13. Why We're Sharing Our Scaffold
&lt;/h2&gt;

&lt;p&gt;This isn't just a success story. It's also a &lt;strong&gt;pattern&lt;/strong&gt; other teams can use.&lt;/p&gt;

&lt;p&gt;We’re making our internal scaffold available as a &lt;strong&gt;private starter repo&lt;/strong&gt;: &lt;a href="https://github.com/VincentvonBueren/erfa-docusaurus-demo/" rel="noopener noreferrer"&gt;https://github.com/VincentvonBueren/erfa-docusaurus-demo/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can fork it, clone it, or just learn from how we structured it.&lt;/p&gt;

&lt;p&gt;We don't believe great documentation should be an afterthought.&lt;br&gt;
We believe it should be a &lt;strong&gt;first-class citizen&lt;/strong&gt; of every cloud platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. The Moral of the Story
&lt;/h2&gt;

&lt;p&gt;Our journey began with a customer pointing out an outdated page.&lt;br&gt;
It ended with documentation that moves at the speed of our platform.&lt;/p&gt;

&lt;p&gt;Docusaurus wasn't the hero of this story.&lt;br&gt;
It was the &lt;strong&gt;vehicle&lt;/strong&gt; that helped us tell a new one.&lt;/p&gt;

&lt;p&gt;If your team is still fighting Confluence sprawl, don't start with a tool. &lt;/p&gt;

&lt;p&gt;Start with a narrative. Then give it the right structure. And watch &lt;strong&gt;your docs come alive&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>productivity</category>
      <category>writing</category>
    </item>
    <item>
      <title>Beyond the Pod: Why wasmCloud and WebAssembly Might Be the Next Evolution of the Platform</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Tue, 21 Oct 2025 05:08:44 +0000</pubDate>
      <link>https://dev.to/ipt/beyond-the-pod-why-wasmcloud-and-webassembly-might-be-the-next-evolution-of-the-platform-1i3e</link>
      <guid>https://dev.to/ipt/beyond-the-pod-why-wasmcloud-and-webassembly-might-be-the-next-evolution-of-the-platform-1i3e</guid>
      <description>&lt;p&gt;Over the past few months I have invested some time to contribute to an open source project I find fascinating: &lt;a href="https://wasmcloud.com/" rel="noopener noreferrer"&gt;wasmCloud&lt;/a&gt;. As a platform engineer and architect, I am very familiar with how software platforms are typically built in practice. However, with the ubiquity of Kubernetes, you run the risk to being stuck in the "doing it the Kubernetes way" line of thinking. But then again, are there any better ways? This is where wasmCloud caught my attention. A modern platform building on proven concepts from Kubernetes, but with some significant differences. In this article I want to introduce wasmCloud, how it compares to Kubernetes, what its internal architecture looks like, and what ideas are, in my humble opinion, a step up from "the Kubernetes way of things".&lt;/p&gt;

&lt;p&gt;Before getting started, I need to get some things out of the way. This article will make quite a few comparisons to Kubernetes and bytecode interpreters like the JVM. If you are unfamiliar with these technologies, it might make sense to have a short look at what these are. Considering you clicked on this article, I am however guessing that you are familiar with them and have some experience in platform engineering practices, either as a poweruser of a platform, or as a designer and developer of one.&lt;/p&gt;

&lt;p&gt;Moreover, I want to thank the company I work for, &lt;a href="https://ipt.ch/en/" rel="noopener noreferrer"&gt;ipt&lt;/a&gt;, for allowing me to invest time to learn about new technologies such as wasmCloud. Not only is contributing to open source a great way to pay back a community powering the modern world, it is also a huge passion of mine. Being able to help the development of such projects during paid worktime enables me to learn so much on emerging technologies, and maybe help build the revolutionary tools of tomorrow.&lt;/p&gt;

&lt;p&gt;So... wasmCloud!? I have been interested in WebAssembly ever since it promised to replace JavaScript, a language I personally consider as extremely poorly designed (someone once told me it was designed in three days, so no wonder there). While WebAssembly is very far from doing anything close to replacing JavaScript in the browser, it has evolved into something else: an application runtime and a potential replacement for containers.&lt;/p&gt;

&lt;h1&gt;
  
  
  WebAssembly as a Platform Foundation
&lt;/h1&gt;

&lt;p&gt;Modern platforms nearly all build on top of containers as their foundational element to run executable code. This is a logical evolution from Docker's meteoric growth, and the ecosystem that grew around its open standards (such as the &lt;a href="https://opencontainers.org/" rel="noopener noreferrer"&gt;OCI - Open Container Initiative&lt;/a&gt;). While containers provide a huge step in terms of ease of use, standardization, and security compared to shipping raw artefacts to virtual machines, as was the case before them, they do have some shortcomings.&lt;/p&gt;

&lt;p&gt;First and foremost, containers are not composable. In part due to their flexibility, they do not offer standard ways of expressing how the world should interact with them at runtime, or what they rely on to perform their functionality. This means that containers are typically deployed as REST-based microservices, where containers communicate with one another over a network using APIs agreed upon outside of the container standards. This lack of standardization makes building reusable components more challenging than it has to be. Moreover, each container essentially needs a server, authentication, authorization, and more to run. This results in quite some waste in the compute density of the platform, with lots of compute wasted on boilerplate.&lt;/p&gt;

&lt;p&gt;Moreover, while containers are a huge step in the right direction in terms of security, they are not quite as secure as most people are led to believe. Containers are "allow by default" constructs, which take quite some work to properly harden.&lt;/p&gt;

&lt;p&gt;Finally, due to how containers are typically built, their startup times are not that great. It is not abnormal to see container start times in the dozens of seconds. This does not bother people very much because containers are mostly used to run long running processes (since we need these REST APIs everywhere). However, a large part of containers are mostly idle, waiting for some API request to come in. If one considers that workloads could be called (and thus the process started) only when needed, startup times over 100ms is considered slow.&lt;/p&gt;

&lt;p&gt;This is where WebAssembly comes it. WebAssembly addresses these challenges. Composability is addressed by the component model.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly: The Component Model
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://component-model.bytecodealliance.org/" rel="noopener noreferrer"&gt;component model&lt;/a&gt; is a way that WebAssembly modules can be built with metadata attached to them which describe their imports and exports based on a rich type system. Moreover, they are composable such that a new component can be built from existing components as long as the imports of one are satisfied by the exports of another. This means that components can interact with one another via direct method/function calls, whose specification is fully standardized. This interface specification is declared in a language known as the WebAssembly Interface Types (WIT) language. An example of a WIT specification of a component relying on a system clock can be seen below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package wasi-example:clocks;

world mycomponent {
    import wall-clock;
}

interface wall-clock {
    record datetime {
        seconds: u64,
        nanoseconds: u32,
    }

    now: func() -&amp;gt; datetime;

    resolution: func() -&amp;gt; datetime;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;WIT can be compared to the &lt;a href="https://en.wikipedia.org/wiki/Interface_description_language" rel="noopener noreferrer"&gt;Interface Definition Language (IDL)&lt;/a&gt; from gRPC but for wasm components.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This declaration says that the component relies on an interface &lt;code&gt;wall-clock&lt;/code&gt; (it &lt;code&gt;import&lt;/code&gt;s the interface) which defines two functions: &lt;code&gt;now&lt;/code&gt; and &lt;code&gt;resolution&lt;/code&gt;. Both take no arguments and return a &lt;code&gt;datetime&lt;/code&gt; object consisting of a &lt;code&gt;seconds&lt;/code&gt; and &lt;code&gt;nanoseconds&lt;/code&gt; field. This component could then be composed with any other component which exports this &lt;code&gt;wall-clock&lt;/code&gt; interface.&lt;/p&gt;

&lt;p&gt;If this were a container which would rely on accessing some API, we would need to read a non-standardized documentation of the container image, and then read up on other containers to ensure they provide APIs that match the ones called by the first container.&lt;/p&gt;

&lt;p&gt;The WebAssembly component model can essentially be seen as a form of contract-based programming to formalize interfaces between WebAssembly core modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly: Secure by Default
&lt;/h2&gt;

&lt;p&gt;Whereas containers provide some form of security by namespacing processes and filesystems, WebAssembly actually sandboxes modules such that they cannot affect one another, or the host they run on. By default a WebAssembly module cannot perform any privileged action and needs to be granted explicit permission. I will not dive deeper into the details of this or I might loose myself in a rant on how software security in the modern day and age is abysmal.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebAssembly: Performance
&lt;/h2&gt;

&lt;p&gt;WebAssembly's main goal is performance. This means that WebAssembly modules run fast, but also that loading modules and starting them is much faster than containers. This has proven to be very useful already, for instance in use cases such as serverless computing, where hyperscalers heavily rely on WebAssembly as a runtime to reduce cold start times, and reduce the delay in function calls.&lt;/p&gt;

&lt;p&gt;Considering the idea to avoid having long running servers providing REST APIs and move to raw function calls on short running modules, having extremely short start times is imperative.&lt;/p&gt;

&lt;p&gt;Alright, so we can see that WebAssembly can be a great choice for the foundation runtime of a platform. So where are platforms leveraging this? Well, actually, quite some "platforms" leverage this idea already. For instance, &lt;a href="https://www.spinkube.dev/" rel="noopener noreferrer"&gt;SpinKube&lt;/a&gt; does exactly this, enabling to run WebAssembly functions on Kubernetes. However, you still interact with these functions via a REST call. Another example is &lt;a href="https://www.kubewarden.io/" rel="noopener noreferrer"&gt;Kubewarden&lt;/a&gt;, leveraging WebAssembly modules to evaluate policies. While some might argue that this is not a platform, Kubewarden provides a runtime for arbitrary programs, including their scheduling and deployment. Sounds like a platform to me.&lt;/p&gt;

&lt;p&gt;Finally: wasmCloud! wasmCloud is probably what people would consider the closest to a full blown platform to run WebAssembly modules. In other words, what Kubernetes is to containers, wasmCloud is to WebAssembly components. It provides a way to deploy, schedule, link, and lifecycle WebAssembly components on a distributed platform.&lt;/p&gt;

&lt;h1&gt;
  
  
  wasmCloud Architecture
&lt;/h1&gt;

&lt;p&gt;Let us look at the wasmCloud architecture a little.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This section will contain quite a few comparisons to Kubernetes concepts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Generally, the wasmCloud architecture can be seen as quite similar to the Kubernetes architecture, with the difference being that wasmCloud does not provide as much flexibility in swapping out building blocks as Kubernetes does. This makes sense as it is a more nascent technology and is currently more opinionated.&lt;/p&gt;

&lt;p&gt;As a reference, here is the diagram wasmCloud uses to provide an overview of the platform:&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%2Fn21migl5ljd0uj9zvg5z.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%2Fn21migl5ljd0uj9zvg5z.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As one can see, the architecture is essentially a set of hosts connected via a so called "lattice". Thus, the architecture distributes the runtime over a set of compute instances in order to achieve resilience against hardware/compute failures. The principle is identical to the one from Kubernetes, providing a cluster in order to be able to quickly shift payloads on the platform to different nodes in case of node failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosts
&lt;/h2&gt;

&lt;p&gt;wasmCloud hosts are the foundation of the compute platform that is provided. They are the equivalent of Kubernetes nodes and provide a WebAssembly runtime for components to run on. Just as with Kubernetes nodes, application developers will rarely need to worry about the hosts other than for deployment affinities and the like.&lt;/p&gt;

&lt;p&gt;In practice, hosts can be anything from a virtual machine, an IoT device, or even a pod running on Kubernetes. In fact, hosting wasmCloud on Kubernetes is a relatively straight forward way to get started with the technology, providing wasmCloud as an application runtime, while providing services via Kubernetes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lattice
&lt;/h2&gt;

&lt;p&gt;The wasmCloud lattice is its networking layer. This can seem a bit strange when considering that this a &lt;a href="https://nats.io/" rel="noopener noreferrer"&gt;NATS&lt;/a&gt; instance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For those unfamiliar with NATS: it is an event streaming component similar to Kafka, but provides additional features such as a key values store, an object store, and publish-subscribe capabilities.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Having a NATS instance as the "networking layer" confused me quite a lot at first. However, one has to remember that thanks to the component model, we no longer require HTTP/TCP network calls for our components to interact with one another. Thus we don't necessarily need an IP to address a component we want to reach. Of course NATS itself will require a physical network to run on in order to distribute events to its different instances, but wasmCloud then only needs to use NATS.&lt;/p&gt;

&lt;p&gt;Essentially, every component exposing a function becomes a subscriber to a queue for this function on NATS. Other components can then call this function via wRPC (gRPC for WebAssembly) by publishing a call to some subject. This is quite different from Kubernetes networking, where calls need to know the location of the callee in the network. Using a subject-based addressing model simplifies deployment and improves scaling and resilience.&lt;/p&gt;

&lt;p&gt;As a user of wasmCloud, you do not need to worry about this though. How function calls are preformed under the hood is abstracted away from the user.&lt;/p&gt;

&lt;p&gt;This distributed networking aspect is one of the superpowers of wasmCloud, as one does not need to worry about how to address a component on the platform. However, it can also introduce strange behaviour in some cases. For instance, on Kubernetes, it's common sense that a HTTP call to a different pod running on the cluster can fail. On wasmCloud however, if the interface we are calling from a different component returns some type, we use the component like a raw function call in our components code. What if that call fails, not because of the called component but due to a networking issue? In the current implementation of wasmCloud this will lead to a panic in the caller. As this is typically not the desired outcome, efforts are underway to design an adapted way how the interfaces need to be designed to handle failures in the transport layer. On top of that, function calls might change such that might avoid using NATS as a transport layer if the component being called in on the same host and the caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capabilities
&lt;/h2&gt;

&lt;p&gt;This is where Kubernetes and wasmCloud start differing in their philosophy. Thanks to the standardized way interfaces can be declared in the component model, one can describe an abstract interface which provides some functionality, without providing an implementation. This is what capabilities are. They are abstract interfaces that describe some useful functionality, such as reading and writing to a key value store, or retrieving some sensitive information from a secured environment. These capabilities are published on wasmCloud for applications to use.&lt;/p&gt;

&lt;p&gt;An application developer can then write a component that makes use of that interface if he/she needs that functionality. He/she does not need to worry about how this capability is implemented. He relies on the "contract" provided by the capability.&lt;/p&gt;

&lt;p&gt;In my opinion, while this is quite challenging to grasp initially, this is what makes wasmCloud so promising. Having worked on many platforms in the past, the main challenge is always how additional services can be provided on top of raw platforms such as Kubernetes in a way that makes then highly standardized while easily consumable. In the current state of platform engineering, this quickly becomes a question of good product management. Unfortunately, doing this correctly is surprisingly difficult. Capabilities provide a technical solution to this, with the only limitation being complete incompatibility with existing software.&lt;/p&gt;

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

&lt;p&gt;A provider is a specific implementation of a capability. For instance, taking the example of the capability enabling the reading and writing to a key value store, a provider might implement this by having a &lt;a href="https://valkey.io/" rel="noopener noreferrer"&gt;ValKey&lt;/a&gt; instance backing the capability. Another provider might implement the very same capability using NATS, Redis, or even an in-memory key-value store.&lt;/p&gt;

&lt;p&gt;Abstracting the provider away from the consumer via a capability enables the platform to swap providers based on needs. Of course performing such a swap might be quite complex, for instance involving a data migration from NATS to ValKey. However, the beauty is that the applications do not require any changes as would be the case in traditional platforms.&lt;/p&gt;

&lt;p&gt;It should be noted that the provider might run completely outside of wasmCloud itself. However, wasmCloud also provides internal providers that are backed into the hosts themselves, providing functionality such as logging or randomness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components
&lt;/h2&gt;

&lt;p&gt;Components refer to the WebAssembly payload that contain your business logic. In the traditional sense, this is your application. However, in wasmCloud lingo, an application is a set of interlinked components including all information about what capabilities they require.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applications
&lt;/h2&gt;

&lt;p&gt;Applications are an abstraction enabling to declaratively define a combination of components, capabilities, and providers together into a deployable unit. Applications are based on the &lt;a href="https://oam.dev/" rel="noopener noreferrer"&gt;open application model (OAM)&lt;/a&gt; and should thus look quite familiar to people working with Kubernetes. In terms of definition, they are similar to a Kubernetes Deployment, describing not only the deployment unit (component or pod in the Kubernetes context), but also its replication, affinities, links to capabilities, etc.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It should be noted that in wasmCloud v2, applications are re-worked to be much more closely modelled after Kubernetes Deployments and ReplicaSets. Version 2 drops the idea of Applications alltogether and uses &lt;code&gt;Workload&lt;/code&gt;, &lt;code&gt;WorkloadReplicaSets&lt;/code&gt;, and &lt;code&gt;WorkloadDeployments&lt;/code&gt; objects. These are also no longer linked to the OAM. In all likelihood we will write another blog post showcasing the capabilities of composition provided by version 2 in the future.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  wadm
&lt;/h2&gt;

&lt;p&gt;The wasmCloud Application Deployment Manager (wadm) manages Applications. It can be seen as the deployment controller from Kubernetes for wasmCloud Applications. It essentially orchestrates the deployment of components, capabilities, their links, etc. on the platform. This construct will also be dropped with wasmCloud version 2.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verdict
&lt;/h1&gt;

&lt;p&gt;With a decent understanding of the architecture we can now get an idea of the uses of wasmCloud in the real world. While I have not yet run anything productive on wasmCloud, I have played with the platform a lot over the past few months, and have come to really appreciate some of its innovative ideas.&lt;/p&gt;

&lt;p&gt;Thus, to summarise my experience: wasmCloud is a relatively new platform and provides interesting new approaches to how inter-component communication can be modeled. On top of that, it does it while building on open standards such as WebAssembly and the component model, such that the business logic of your application remains portable. While these new concepts are very promising, wasmCloud still suffers from a couple drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For people unfamiliar with WebAssembly, it has a quite steep learning curve. This is highly accentuated for people unfamiliar with existing platforms such as Kubernetes.&lt;/li&gt;
&lt;li&gt;The set of supported providers and capabilities is extremely small to date. This will of course grow as adoption increases, but currently early adopters will have to write their own providers most of the time and will not be able to rely on third-party components.&lt;/li&gt;
&lt;li&gt;As wasmCloud shifts more responsibility to the platform level, it will require a strong platform team to operate this with low developer friction. This can be an issue as finding highly skilled platform engineers is quite difficult at the moment. However, the team behind wasmCloud is focused on making application delivery as frictionless as possible.&lt;/li&gt;
&lt;li&gt;Finally, I am not sure I currently understand the security model wasmCloud uses to authenticate and authorize calls between components. While I am not sure this is a drawback, it does not yet feel as intuitive as Kubernetes simple yet relatively powerful RBAC. I will have to dive deeper into this to have a final opinion on it though (another blog post might follow on this).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devex</category>
      <category>webassembly</category>
      <category>cloudnative</category>
      <category>serverless</category>
    </item>
    <item>
      <title>That Time We Found a Service Account Token in my Log Files</title>
      <dc:creator>Vincent von Büren</dc:creator>
      <pubDate>Thu, 04 Sep 2025 07:59:04 +0000</pubDate>
      <link>https://dev.to/ipt/that-time-i-found-a-service-account-token-in-my-log-files-4d00</link>
      <guid>https://dev.to/ipt/that-time-i-found-a-service-account-token-in-my-log-files-4d00</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Disclaimer&lt;/strong&gt;&lt;br&gt;
This article assumes you're already somewhat familiar with Kubernetes concepts (Pods, ServiceAccounts) and the basics of JSON Web Tokens (JWTs).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was a &lt;strong&gt;Tuesday&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nothing special - just your average day as a platform engineer. My team's notifications were mercifully quiet, and I thought, "Perfect, I can finally clean up that old Helm chart that's been bothering me."&lt;/p&gt;

&lt;p&gt;I opened the repo of the underlying image written in Go to double-check the config before merging.&lt;/p&gt;

&lt;p&gt;Before I even got far, a colleague — Martin Odermatt — pinged me:&lt;/p&gt;

&lt;p&gt;“You might want to take a look at this…”&lt;/p&gt;

&lt;p&gt;He had spotted something concerning in the code:&lt;/p&gt;

&lt;p&gt;log.Println("SA Token:", token)&lt;/p&gt;

&lt;p&gt;Wait. What?&lt;/p&gt;

&lt;p&gt;A debug statement. Still in production code. Logging an actual Kubernetes ServiceAccount token. Not cool...&lt;/p&gt;

&lt;p&gt;I paused. My heart rate didn’t. Curious but mostly horrified, I took the token Martin had found and decoded the payload in my shell:&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;"iss"&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://kubernetes.default.svc.cluster.local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"kubernetes.io/serviceaccount/namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"kubernetes.io/serviceaccount/secret.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;"payments-token-6gh49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"kubernetes.io/serviceaccount/service-account.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;"payments-sa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"kubernetes.io/serviceaccount/service-account.uid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f9a2c144-11b3-4eb0-9f30-3c2a5063e2e7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"aud"&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://kubernetes.default.svc.cluster.local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:serviceaccount:payments:payments-sa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1788201600&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;span class="err"&gt;Sat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Aug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;GMT&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1756665600&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;span class="err"&gt;Fri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Aug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;GMT&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;Default audience claim. A 1-year expiry.&lt;/p&gt;

&lt;p&gt;As we dug deeper, Martin Odermatt pointed out the underlying issue: this was a legacy ServiceAccount token, and we should be using projected tokens instead.&lt;/p&gt;

&lt;p&gt;This "bad boy" wasn't just a dev leftover - it was a high-privilege token with zero constraints floating around in plaintext logs!&lt;/p&gt;




&lt;h3&gt;
  
  
  What This Article Covers
&lt;/h3&gt;

&lt;p&gt;In this post, I'll guide you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The inner workings of Vault authentication with JWT and Kubernetes methods&lt;/li&gt;
&lt;li&gt;What Kubernetes ServiceAccounts and their tokens are, and how they’re (mis)used&lt;/li&gt;
&lt;li&gt;How projected ServiceAccount tokens fix many of the hidden dangers of older token behavior&lt;/li&gt;
&lt;li&gt;Why you should start adopting token projection and Vault integration today&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll cover real-world use cases, implementation tips, and common pitfalls - so you don't end up like I did, staring at a:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SA token:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and wondering how close you just came to a security incident.&lt;/p&gt;




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

&lt;p&gt;To really understand why that log statement gave me chills, we need to unpack a few core concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is a JWT?&lt;/li&gt;
&lt;li&gt;How do Kubernetes ServiceAccounts and their tokens work?&lt;/li&gt;
&lt;li&gt;And what role do these tokens play in authenticating to systems like Vault?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's start with the fundamentals.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Is a JWT?
&lt;/h3&gt;

&lt;p&gt;If you've been around authentication systems long enough, you've probably seen one of these beasts:&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="err"&gt;eyJhbGciOiJSUzI&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;NiIsInR&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;cCI&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;IkpXVCJ&lt;/span&gt;&lt;span class="mi"&gt;9&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;&lt;strong&gt;This is a JSON Web Token (short: JWT).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a compact, URL-safe format for representing claims between two parties. They're used everywhere: web apps, APIs, and yes — inside your Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;A JWT consists of three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header&lt;/strong&gt; – declares the algorithm used to sign the token (e.g. RS256)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt; – contains the claims (who you are, what you're allowed to do, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt; – a cryptographic seal that verifies the payload hasn't been tampered with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claims are the heart of a JWT — key-value pairs that describe who the token refers to and what it can be used for. &lt;/p&gt;

&lt;p&gt;They can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard claims defined by the spec (e.g., &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Custom claims added by the issuer for domain-specific needs&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Closer Look at &lt;code&gt;aud&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;audience&lt;/strong&gt; (&lt;code&gt;aud&lt;/code&gt;) claim tells &lt;strong&gt;who the token is meant for&lt;/strong&gt;. Think of it as the intended recipient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Imagine a Coldplay concert ticket. It says &lt;em&gt;valid for Stadium X on 01-09-2025&lt;/em&gt;. You can't take the same ticket and use it at Stadium Y — they'll reject it (...trust me, I tried).&lt;/p&gt;

&lt;p&gt;A JWT works the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the token has &lt;code&gt;"aud": "https://kubernetes.default.svc"&lt;/code&gt;, then only the Kubernetes API server should accept it.&lt;/li&gt;
&lt;li&gt;If some other service receives that token, the &lt;code&gt;aud&lt;/code&gt; won't match and the token must be rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this check, a token could be misused anywhere that trusts the signing key. With &lt;code&gt;aud&lt;/code&gt;, it's scoped to the right system.&lt;/p&gt;




&lt;h3&gt;
  
  
  Kubernetes and ServiceAccounts
&lt;/h3&gt;

&lt;p&gt;Kubernetes is an open-source platform that orchestrates containers at scale. At its heart is the &lt;strong&gt;Pod&lt;/strong&gt; — the smallest deployable unit.&lt;/p&gt;

&lt;p&gt;But every pod needs an identity. That's where &lt;strong&gt;ServiceAccounts&lt;/strong&gt; come in.&lt;/p&gt;

&lt;h4&gt;
  
  
  ServiceAccounts 101
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Every Pod references a ServiceAccount (default if none is set), but a token is only mounted if enabled&lt;/li&gt;
&lt;li&gt;Kubernetes mounts the identity at:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;That token is a &lt;strong&gt;JWT&lt;/strong&gt;, signed by the Kubernetes control plane&lt;/li&gt;
&lt;li&gt;It lets the pod authenticate with the API server — and sometimes even external systems like Vault&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Catch
&lt;/h4&gt;

&lt;p&gt;Until recently, these tokens came with dangerous defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long-lived (often valid for a year)&lt;/li&gt;
&lt;li&gt;Previous to Kubernetes v1.24, there was no default audience set (&lt;a href="https://kubernetes.default.svc" rel="noopener noreferrer"&gt;https://kubernetes.default.svc&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Automatically mounted into every pod, even if unused&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Enter Vault: The Gatekeeper of Secrets
&lt;/h3&gt;

&lt;p&gt;HashiCorp Vault is your cluster’s paranoid librarian:&lt;br&gt;
it stores API keys, certs, passwords — and only hands them out when it's sure you should have them.&lt;/p&gt;

&lt;p&gt;How? &lt;strong&gt;Authentication methods.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Vault Authentication Methods
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Username &amp;amp; password&lt;/li&gt;
&lt;li&gt;AppRole&lt;/li&gt;
&lt;li&gt;LDAP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's zoom into the last two.&lt;/p&gt;


&lt;h4&gt;
  
  
  Kubernetes Auth Method
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Pod sends its mounted ServiceAccount token to Vault&lt;/li&gt;
&lt;li&gt;Vault validates it against the Kubernetes API&lt;/li&gt;
&lt;li&gt;If valid, Vault maps it to a policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is simple and works well when Vault runs inside the cluster.&lt;/p&gt;


&lt;h4&gt;
  
  
  JWT Auth Method
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Vault verifies the JWT itself (signature, claims, expiration)&lt;/li&gt;
&lt;li&gt;No need for Kubernetes API access&lt;/li&gt;
&lt;li&gt;More portable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Kubernetes&lt;/strong&gt; if Vault runs inside your cluster and simplicity matters&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;JWT&lt;/strong&gt; if you want portability, stronger boundaries, and flexibility&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  Projected Tokens: Because It's 2025
&lt;/h3&gt;

&lt;p&gt;Old tokens were static and long-lived — exactly what we were looking at here. As Martin pointed out during the investigation, projected tokens are designed to fix this entire class of problems.&lt;/p&gt;

&lt;p&gt;Instead of mounting a one-year token into every pod, Kubernetes can now generate &lt;strong&gt;short-lived, audience-bound tokens on demand.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  What You Get
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Short TTL (e.g. 10 minutes)&lt;/li&gt;
&lt;li&gt;Audience restrictions (&lt;code&gt;aud: vault&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Automatic rotation by &lt;code&gt;kubelet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No automatic mounting into pods&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Example Pod with Projected Token
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projected-token-test-pod&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projected-auth-sa&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projected-auth-test&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;demo/vault-curl:latest&lt;/span&gt;
      &lt;span class="na"&gt;command&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;sleep"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;token&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/run/secrets/projected&lt;/span&gt;
          &lt;span class="na"&gt;readOnly&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;token&lt;/span&gt;
      &lt;span class="na"&gt;projected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;serviceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&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;token&lt;/span&gt;
              &lt;span class="na"&gt;expirationSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
              &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Vault Loves This
&lt;/h3&gt;

&lt;p&gt;Vault's JWT auth method is tailor-made for projected tokens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It parses and verifies the JWT signature (via a configured PEM key or JWKS endpoint)&lt;/li&gt;
&lt;li&gt;Validates all claims (&lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;iss&lt;/code&gt;) locally&lt;/li&gt;
&lt;li&gt;Issues secrets only if every check passes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Minimal dependencies. Strong claim validation. Secure, verifiable checks.&lt;/p&gt;


&lt;h3&gt;
  
  
  Back to the Log
&lt;/h3&gt;

&lt;p&gt;Imagine you stumble upon this in a Go app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Auth Token:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old world:&lt;/strong&gt; a one-year, cluster-wide token with no audience. A time bomb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New world:&lt;/strong&gt; a 10-minute token, scoped to Vault, rotating automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's still bad to log tokens — but at least it's not catastrophic.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try It Yourself: Vault + K8s AuthN Lab
&lt;/h3&gt;

&lt;p&gt;I've built a hands-on demo repo where you can test this locally with KIND (Kubernetes in Docker) and Vault Helm charts:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/VincentvonBueren/erfa-projected-sa-token" rel="noopener noreferrer"&gt;GitHub: VincentvonBueren/erfa-projected-sa-token&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  What's Inside
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;KIND cluster with Vault&lt;/li&gt;
&lt;li&gt;Both Kubernetes and JWT auth methods enabled&lt;/li&gt;
&lt;li&gt;Vault policies and roles&lt;/li&gt;
&lt;li&gt;Four demo pods:

&lt;ul&gt;
&lt;li&gt;Kubernetes auth method&lt;/li&gt;
&lt;li&gt;JWT with static token&lt;/li&gt;
&lt;li&gt;JWT with projected token&lt;/li&gt;
&lt;li&gt;JWT with wrong audience (failure demo)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Final Drop 🎤
&lt;/h3&gt;

&lt;p&gt;If your pods still run with default, long-lived tokens:&lt;br&gt;
you’re one debug log away from giving away the keys to your cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Projected tokens aren't optional. They're essential.&lt;/strong&gt;&lt;br&gt;
Adopt them today — and stop shipping security disasters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Acknowledgment
&lt;/h3&gt;

&lt;p&gt;The discovery of the exposed ServiceAccount token — and the push towards using projected tokens — came from my dear fellow engineer Martin Odermatt, whose input significantly shaped this investigation and motivated me to tell this story.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>serviceaccount</category>
      <category>security</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Dissecting Kubewarden: Internals, How It's Built, and Its Place Among Policy Engines</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Mon, 07 Jul 2025 05:31:30 +0000</pubDate>
      <link>https://dev.to/ipt/dissecting-kubewarden-internals-how-its-built-and-its-place-among-policy-engines-57gf</link>
      <guid>https://dev.to/ipt/dissecting-kubewarden-internals-how-its-built-and-its-place-among-policy-engines-57gf</guid>
      <description>&lt;p&gt;Kubernetes offers amazing capabilities to improve compute density compared to older runtimes such as virtual machines. However, in oder to leverage the capabilities of the platform, these tend to host applications from various tenants. This introduces a strong need for properly crafted controls and well-defined compliance to ensure the tenants use the platform correctly and do not affect one another. The RBAC capabilities provided out of the box by Kubernetes are quickly insufficient to address this need. This is where policy engines such as &lt;a href="https://www.kubewarden.io/" rel="noopener noreferrer"&gt;Kubewarden&lt;/a&gt; come into play. In this post we will look at how Kubewarden can be leveraged to ensure correct usage of a platform, how it compares to other policy engines, and how to best adopt it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Policy Engines
&lt;/h1&gt;

&lt;p&gt;Kubernetes provides role-based access control (RBAC) out of the box to control what actions can be performed against the Kubernetes API. Generally, RBAC works by assigning sets of roles to users or groups of users. Capabilities are attached to these roles, and users having a role obtain these capabilities. This simple mechanism is very powerful, mostly because it is quite flexible while allowing a simple overview of a user's capabilities. However, in the case of Kubernetes, the definition of capabilities is very restricted. Roles only allow or deny access to Kuberenetes API endpoints, but to not allow control based on payload content. This means that these capabilities are mostly restricted to CRUD operations on Kubernetes primitives (e.g. &lt;code&gt;Deployments&lt;/code&gt;, &lt;code&gt;Ingresses&lt;/code&gt;, or custom resources). Unfortunately, this is often not enough.&lt;/p&gt;

&lt;p&gt;For instance, it is quite common to allow users to perform actions on some primitives under specific conditions. An example would be that creating &lt;code&gt;Deployments&lt;/code&gt;s is only allowed as long as its name follows some convention and the pods its creates are not privileged and set proper resource requests/limits. The naming convention cannot be enforced by standard RBAC controls as these have no possibility to represent more complex logic. Controlling the configuration of the pods created by a &lt;code&gt;Deployment&lt;/code&gt; is a validation of the payload pushed to the API, and is thus not supported either.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Security contexts and resources on pods can be controlled via methods such as Security Context Constraints or Pod Security Policies and ResourceQuotas. However, these do not reject the creation of the deployment, but will only block the creation of the pods themselves. It is therefore possible to apply a Deployment that is known to not allow the creation of pods. In my personal opinion this is not ideal, as it does not fail early.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These scenarios is where policy engines come into play. They utilise Kubernetes' Dynamic Access Control mechanisms to enable cluster administrators to manage permissions using more complex logic. The exact capabilities of policy engines can vary greatly as these are essentially arbitrary software that validates or mutates Kubernetes requests. However, the majority of major policy engines work similarly. They tend to implement the operator pattern, enabling the configuration of policies using Kubernetes custom resources. In this blog post we will have a look at Kubewarden in more detail, and how it compares to other engines.&lt;/p&gt;

&lt;h1&gt;
  
  
  Kubewarden Architecture
&lt;/h1&gt;

&lt;p&gt;Kubewarden leverages &lt;a href="https://webassembly.org/" rel="noopener noreferrer"&gt;WebAssembly (WASM)&lt;/a&gt; to enable extremely flexible policy evaluation. Essentially, Kubewarden can be seen as a WASM module orchestrator where policies are deployed as serverless functions that get called when necessary. The result of these WASM functions then determines whether an API request against Kubernetes is allowed, denied, or altered (mutated).&lt;/p&gt;

&lt;p&gt;This similee can also help explain Kubewarden's architecture. Essentially, the Kubewarden controller (operator) manages policy servers and admission policies. Policy servers can be seen as hosts for the serverless execution of functions, whereas admissions policies are the functions themselves. Therefore, in order to perform policy validation, one needs at least one policy server running to host the policies one wants to enforce. The controller then takes care of configuring the runtime (policy server) to properly run the adequate policy executable with the appropriate inputs when a policy needs to be evaluated. The diagram below illustrates 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%2F7x0xmdqg4ql6gmp3te9h.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%2F7x0xmdqg4ql6gmp3te9h.png" alt="A policy server's internal architecture" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As policies are WASM modules, they can themselves support configuration. This makes policy reuse a major feature of Kubewarden. Complex logic can be contained in the WASM module while exposing some tuning as configuration, allowing a policy to perform a relatively generic task. To understand this better, let us have a look at such a policy:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policies.kubewarden.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterAdmissionPolicy&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;annotations&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cel-policy-replica-example"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry://ghcr.io/kubewarden/policies/cel-policy:v1.0.0&lt;/span&gt;
  &lt;span class="na"&gt;backgroundAudit&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;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;protect&lt;/span&gt;
  &lt;span class="na"&gt;mutating&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;policyServer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&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;apps"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;apiVersions&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;v1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;operations&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;CREATE"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deployments"&lt;/span&gt;&lt;span class="pi"&gt;]&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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replicas"&lt;/span&gt;
        &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object.spec.replicas"&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;maxreplicas&lt;/span&gt;
        &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int(5)&lt;/span&gt;
    &lt;span class="na"&gt;validations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variables.replicas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;variables.maxreplicas"&lt;/span&gt;
        &lt;span class="na"&gt;messageExpression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;replicas&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;less&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;than&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;equal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;string(variables.maxreplicas)"&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, we are using a WASM module which evaluates a &lt;a href="https://cel.dev/" rel="noopener noreferrer"&gt;Common Expression Language (CEL)&lt;/a&gt; expression to define our policy. Evaluating a CEL expression is not something we want to implement every time ourselves. Thankfully, Kubewarden provides this as a WASM module on their &lt;a href="https://artifacthub.io/packages/search?kind=13&amp;amp;sort=relevance&amp;amp;page=1" rel="noopener noreferrer"&gt;ArtefactHub&lt;/a&gt;. Thus we do not need to implement anything and can reuse that module. It is referenced on the &lt;code&gt;module&lt;/code&gt; line above. Of course we also need to actually define the CEL expression that should be the heart of the policy rule. This is done within the &lt;code&gt;settings&lt;/code&gt; block. Note how we can use object internals (such as replicas defined in a &lt;code&gt;Deployment&lt;/code&gt;) in the validation expression. Finally, we need to define on what objects this policy should be evaluated. In order to do this, we provide &lt;code&gt;rules&lt;/code&gt; that tell Kubewarden on what Kubernetes API endpoints to trigger the policy, and additionally provide information about which namespaces should be affected by the policy with a &lt;code&gt;namespaceSelector&lt;/code&gt;. The remaining options configure the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;backgroundAudit&lt;/code&gt;: informs Kubewarden to report on this policy for objects that are already deployed. In this case, we validate the replicas on created or updated &lt;code&gt;Deployment&lt;/code&gt; objects. However, there might already be &lt;code&gt;Deployments&lt;/code&gt; on the cluster that violate the policy before we start enforcing it. This option will tell Kubewarden to provide reports on such violations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode&lt;/code&gt;: Kubewarden supports enforcing policies (in &lt;code&gt;protect&lt;/code&gt; mode), or monitoring the cluster (in &lt;code&gt;monitor&lt;/code&gt; mode). Using the &lt;code&gt;monitor&lt;/code&gt; mode can be interesting when investigating how people use the Kubernetes cluster or providing them with warnings before enforcing policies.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mutating&lt;/code&gt;: policies can also mutate (change) requests. In this case we are only performing validation to potentially reject requests. Thus we set &lt;code&gt;mutating&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;policyServer&lt;/code&gt;: as explained above, Kubewarden can manage many policy servers. This simply informs
the controller on which policy server this specific policy should be deployed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As one can see based on the sample policy above, while Kubewarden technically uses programs as policies, it is usually not necessary to write any code to use Kubewarden. This is thanks to its strong focus on module configuration and re-usability. The above CEL module alone already enables the configuration of a very wide range of policies. On top of that, other modules shared on ArtefactHub provide more specific validations or mutations that might incorporate more complex logic. If this is not enough, policy groups (a feature we will not cover in this post) can be utilised to combine other policies and express more complex logic as well. Finally, if one has very specific needs that cannot be addressed by any of the publicly shared modules, one can still fall back to writing code and building ones own module with fully arbitrary logic. How such policies can be written, in actual code, might follow in a separate blog post.&lt;/p&gt;

&lt;p&gt;The above architecture of Kubewarden is what makes it stand apart from most other policy engines. Generally policy engines contain the logic fully in the controller, only exposing configuration via the custom resource. Since Kubewarden can essentially execute arbitrary WASM bytecode, it is not bound by the expressiveness of the custom resource declaration.&lt;/p&gt;

&lt;p&gt;All this considered, is Kubewarden the best choice for a policy engine and should be used in all scenarios?&lt;/p&gt;

&lt;h1&gt;
  
  
  Comparison
&lt;/h1&gt;

&lt;p&gt;There are many other policy engines out there, such as &lt;a href="https://kyverno.io/" rel="noopener noreferrer"&gt;Kyverno&lt;/a&gt;, &lt;a href="https://open-policy-agent.github.io/gatekeeper/website/" rel="noopener noreferrer"&gt;Gatekeeper&lt;/a&gt;, or &lt;a href="https://www.fairwinds.com/polaris" rel="noopener noreferrer"&gt;Polaris&lt;/a&gt;. So why would you choose Kubewarden over any other?&lt;/p&gt;

&lt;p&gt;As explained above, Kubewarden provides unprecedented flexibility, thanks to the way it evaluates its policies. This has the massive advantage that you will never reach a point that you have a policy that you would like to enforce but are restricted by the policy engine itself. However, it also has some drawbacks. The primary one being complexity. Writing WASM modules is not for the fainthearted, as WebAssembly is not yet incredibly mature, and most developers will not be familiar with it. The complexity issue can however be sidestepped as the vast majority of policies can be expressed using off-the-shelf WASM modules provided by Kubewarden.&lt;/p&gt;

&lt;p&gt;Another aspect that often needs to be considered in enterprise contexts, is support. Kubewarden is an open source project that is loosely backed by SUSE (as it was originally developer for its Rancher offering). Thus enterprise support is only available via a SUSE Rancher Prime. Other tools such as Kyverno are not only more mature, but offer more flexible enterprise support (via Isovalent).&lt;/p&gt;

&lt;p&gt;Finally, another aspect to consider is the featureset of a policy engines. Not all policy engines support mutating requests, and are thus much more restricted in their use. However, in this category Kubewarden offers all the features typically desired from policy engines. Some engines such as Kyverno support more features such as synchronizing &lt;code&gt;Secret&lt;/code&gt; objects. While this can be useful, it is, in my humble opinion, not a feature for a policy engine.&lt;/p&gt;

&lt;p&gt;Of course, there are also personal preference aspects to consider. As an example, Kubewarden and Kyverno handle policy exceptions very differently. Kubewarden has matchers that can be defined as part of the policy itself, which allow to exclude some resources from being validated. Kyverno on the other hand uses a separate CRD called &lt;a href="https://kyverno.io/docs/exceptions/" rel="noopener noreferrer"&gt;&lt;code&gt;PolicyException&lt;/code&gt;&lt;/a&gt;. Both have advantages and disadvantages.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verdict
&lt;/h1&gt;

&lt;p&gt;Kubewarden is a very interesting piece of software. Its internal architecture enables it to be incredibly flexible, at the cost of complexity. However, due to a smart concept of WebAssembly module re-use, that complexity is mostly under the hood, unless one wants or needs to dive deep. In my opinion, Kubewarden can be an absolutely great consideration when ones operates very large Kubernetes clusters what might have quite exceptional requirements. However, even in these cases, I would recommend starting very slow, and slowly building up to the complexity Kubewarden can hold in store.&lt;/p&gt;

&lt;p&gt;If you do not operate a large Kubernetes fleet, or expect to have rather standard requirements in terms of how you want to restrict access to you cluster(s), you might be better off with more mature and simpler tools like Kyverno. Getting support for these tools is likely to also be much simpler.&lt;/p&gt;

&lt;p&gt;A large part of the complexity of Kubewarden also comes from all that is required to even run this in an enterprise context. Unless you allow pulling WASM modules directly from the internet, you will also need a registry to host OCI packaged modules. On top of that, should you decide to write your own modules, you will need a process to do this, and build knowhow in that area. These are some of the aspects I hope to cover in a follow up post.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>kubernetes</category>
      <category>security</category>
    </item>
    <item>
      <title>Your Cluster Deserves Better Traffic Management. Enter: Gateway API.</title>
      <dc:creator>Ignacio de los Rios</dc:creator>
      <pubDate>Thu, 05 Jun 2025 09:07:17 +0000</pubDate>
      <link>https://dev.to/ipt/your-cluster-deserves-better-traffic-management-enter-gateway-api-3ago</link>
      <guid>https://dev.to/ipt/your-cluster-deserves-better-traffic-management-enter-gateway-api-3ago</guid>
      <description>&lt;p&gt;A Kubernetes cluster is like a beautifully designed city—with excellent plumbing, stable buildings, and all essential services—but no roads leading in or out. No visitors, no deliveries, no exits. If you want your application or services to interact with the outside world (say, users), you’ll need to pave some highways. Kubernetes has traditionally offered a few ways to do this—some better than others. Now, there’s a new infrastructure project in town: the Gateway API—and it's here to stay.&lt;br&gt;
In this post, we’ll explore Kubernetes’ latest external access mechanism and show how it integrates with Cert-Manager to handle certificates. But before diving into this new world, let’s briefly review the most common ways to expose services in Kubernetes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Current Alternatives
&lt;/h2&gt;
&lt;h3&gt;
  
  
  NodePort: Exposing a Service on a Node’s Port
&lt;/h3&gt;

&lt;p&gt;A NodePort service is the most basic way to expose a service externally. Kubernetes opens a specific port on each node, and any traffic to a node’s IP on that port is forwarded to the service inside the cluster. Simple, but with notable limitations.&lt;/p&gt;
&lt;h3&gt;
  
  
  LoadBalancer: External Load Balancers for Services
&lt;/h3&gt;

&lt;p&gt;A LoadBalancer service goes a step further by integrating with external load balancing infrastructure. When you create a Service of type LoadBalancer, Kubernetes asks the cloud provider to provision an external load balancer (e.g., AWS ELB, GCP Load Balancer, Azure Load Balancer). The service receives an external IP or hostname that forwards traffic to the backing Pods.&lt;br&gt;
This works well in managed cloud environments but scales poorly and doesn’t work out of the box for bare-metal or on-prem clusters—unless you add a software load balancer like MetalLB.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ingress/Routes: Layer-7 Routing for HTTP/S
&lt;/h3&gt;

&lt;p&gt;Ingress (or Route in OpenShift) is a separate API object used for external access at Layer 7 (HTTP/S). With Ingress, you define routing rules—for example, "send requests for api.example.com to Service A, and &lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt; to Service B."&lt;br&gt;
An Ingress Controller implements these rules, typically using a proxy or cloud load balancer. However, Ingress only supports HTTP(S) and lacks native support for TCP or other protocols.&lt;/p&gt;
&lt;h2&gt;
  
  
  From Ingress to Gateway API: Why a New API?
&lt;/h2&gt;

&lt;p&gt;The Gateway API is an evolving standard—now an official Kubernetes SIG project—designed to address the limitations of Ingress and support more advanced traffic management. If you're unfamiliar with the term “SIG” it stands for Special Interest Group: volunteer teams that manage and maintain specific areas of the Kubernetes ecosystem (see the project home &lt;a href="https://github.com/kubernetes-sigs" rel="noopener noreferrer"&gt;here&lt;/a&gt;). Think of the Gateway API as a modern urban plan for our Kubernetes city — with zoning laws, dedicated roads, and clearly defined roles.&lt;/p&gt;

&lt;p&gt;Unlike Ingress, Gateway API is more than a single traffic cop signaling HTTP cars through an intersection. It is an expressway system that speaks multiple protocols—HTTP, HTTPS, TCP, TLS, even UDP—while offering built-in traffic tricks such as path and header rewrites, query-parameter matching, and weighted routing for canary releases - all without using vendor-specific annotations. Crucially, it separates concerns: platform teams lay down GatewayClasses and Gateways (the asphalt), while application teams own their Routes (the traffic signs). Because the spec is vendor-neutral, the very same YAML can drive Istio, NGINX or Cilium, meaning your road network is portable as your underlying architecture evolves.&lt;/p&gt;

&lt;p&gt;Three of the most important Resources introduced by GatewayAPI are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GatewayClass – urban planners&lt;/li&gt;
&lt;li&gt;Gateways – the roads and entry points&lt;/li&gt;
&lt;li&gt;Routes – detailed traffic control rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are implemented as Kubernetes Custom Resource Definitions (CRDs), providing flexibility, scalability, and separation of concerns. Let's dive into each of them.&lt;/p&gt;
&lt;h3&gt;
  
  
  GatewayClass
&lt;/h3&gt;

&lt;p&gt;A GatewayClass defines a category of Gateways and is managed cluster-wide. It encapsulates the controller responsible for implementing the associated gateways.&lt;br&gt;
This is similar in concept to StorageClass in Kubernetes: administrators define them, and users reference them.&lt;br&gt;
Example:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GatewayClass&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;controllerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio.io/gateway-controller&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, any Gateway with gatewayClassName: istio will be handled by the Istio controller. It applies similar for other controllers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gateway
&lt;/h3&gt;

&lt;p&gt;A Gateway is an instance of ingress infrastructure—an actual network entry point. It references a GatewayClass and defines one or more listeners, each specifying a protocol (HTTP, HTTPS, TCP), port, and optional hostname.&lt;br&gt;
Example:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gateway&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-gateway&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio-ingress&lt;/span&gt;  &lt;span class="c1"&gt;# Gateways are namespaced&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gatewayClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio&lt;/span&gt;   &lt;span class="c1"&gt;# must match a GatewayClass&lt;/span&gt;
  &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;            &lt;span class="c1"&gt;# an arbitrary name for the listener&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;allowedRoutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;All&lt;/span&gt;         &lt;span class="c1"&gt;# allow Routes from any namespace to bind (could be restricted in production)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Routes: The Core of Gateway API’s Flexibility
&lt;/h3&gt;

&lt;p&gt;Here’s where the Gateway API truly shines—dedicated, protocol-specific route types like HTTPRoute, TCPRoute, and TLSRoute. These give you granular control over traffic, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Matching by host, path, headers, or query parameters&lt;/li&gt;
&lt;li&gt;Routing to multiple services with weighted distribution (for canary or A/B testing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of Routes as traffic signs guiding vehicles through the city. They keep everything flowing smoothly.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPRoute&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-route&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hostnames&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;myapp.example.com"&lt;/span&gt;
  &lt;span class="na"&gt;parentRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-gateway&lt;/span&gt;
      &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio-ingress&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&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;PathPrefix&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
      &lt;span class="na"&gt;backendRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-service&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example highlights from an HTTPRoute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parentRefs: Attach the route to a specific Gateway and listener&lt;/li&gt;
&lt;li&gt;hostnames: Match specific domains (e.g., myapp.example.com)&lt;/li&gt;
&lt;li&gt;rules: Define matching logic and forwarding behavior&lt;/li&gt;
&lt;li&gt;matches: Conditions like path prefix or headers&lt;/li&gt;
&lt;li&gt;backendRefs: Destination services, possibly weighted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Earlier, we talked about one of the key advantages of the Gateway API over Ingress: native support for advanced traffic management—or what we called “traffic tricks.” A concrete example of this comes in the form of on-object filters. For instance, if you need to strip /v1 from all incoming URLs, here’s how you can do it using Gateway’s built-in URL rewrite filter:&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;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&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;PathPrefix&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/v1&lt;/span&gt;
    &lt;span class="na"&gt;filters&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;URLRewrite&lt;/span&gt;
      &lt;span class="na"&gt;urlRewrite&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;replacePrefixMatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;backendRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-service&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No annotations, no sidecars—just one CRD doing the job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gateway Controllers and Implementations
&lt;/h3&gt;

&lt;p&gt;Gateway API resources (like Gateway, HTTPRoute, and GatewayClass) are declarative. To actually route traffic, you need a Gateway Controller—a component that reads these resources and configures the data plane (Envoy, NGINX, etc.).&lt;br&gt;
Supported controllers include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service meshes like Istio and Linkerd (Istio is moving to Gateway API by default)&lt;/li&gt;
&lt;li&gt;Ingress controllers such as Contour, NGINX, Traefik, Kong (adding Gateway support)&lt;/li&gt;
&lt;li&gt;Cloud providers (GKE, AWS, Azure) that map Gateway resources to native load balancers&lt;/li&gt;
&lt;li&gt;Other projects like Cilium and Gloo offering enhanced networking&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Managing TLS Certificates with Gateway API
&lt;/h2&gt;

&lt;p&gt;Let’s be honest: rotating and renewing TLS certificates remains one of the most thankless tasks in day-to-day ops. Exposing HTTP apps to the internet requires HTTPS, and thus, TLS certificates. In the Ingress world, cert-manager automates this via annotations and certificate blocks. Fortunately, Gateway API is supported too.&lt;br&gt;
To automate TLS certificate issuance with cert-manager and Gateway API:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure cert-manager and Gateway API CRDs are installed&lt;/li&gt;
&lt;li&gt;Configure a ClusterIssuer or Issuer (e.g., Let’s Encrypt)&lt;/li&gt;
&lt;li&gt;Annotate your Gateway to trigger cert-manager
Example:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gateway&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-gateway&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cert-manager.io/issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letsencrypt-prod"&lt;/span&gt;  &lt;span class="c1"&gt;# Reference to a cert-manager Issuer or ClusterIssuer&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gatewayClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio&lt;/span&gt;
  &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPS&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;443&lt;/span&gt;
      &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;myapp.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terminate&lt;/span&gt;
        &lt;span class="na"&gt;certificateRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&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;myapp-example-com-tls&lt;/span&gt;  &lt;span class="c1"&gt;# Secret that will hold the TLS cert and key&lt;/span&gt;
      &lt;span class="na"&gt;allowedRoutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;All&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cert-manager will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect the need for a certificate&lt;/li&gt;
&lt;li&gt;Create a Certificate resource behind the scenes&lt;/li&gt;
&lt;li&gt;Complete the ACME challenge (if using Let’s Encrypt)&lt;/li&gt;
&lt;li&gt;Store the key and cert in the referenced Secret&lt;/li&gt;
&lt;li&gt;Keep the certificate renewed automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works much like how cert-manager integrates with Ingress—only now, it's applied to Gateway. This seamless integration with cert-manager is key to automating secrets management for external access and eliminates the need for hacky workarounds.&lt;/p&gt;

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

&lt;p&gt;Kubernetes offers several ways to expose services—from the basic NodePort to cloud-managed LoadBalancer and flexible Ingress. The Gateway API builds on those lessons and provides a modern, scalable way to manage external access with better separation of infrastructure and application concerns.&lt;br&gt;
We explored how GatewayClass, Gateway, and Routes work together, and how Gateway controllers like Istio implement them. On the secrets side, tools like cert-manager integrate seamlessly to automate HTTPS. For broader secrets management, tools like HashiCorp Vault are an excellent addition — perhaps a topic for another blog. 😊&lt;br&gt;
Kubernetes has evolved from a small village into a thriving metropolis. And just like any modern city, it needs robust, scalable infrastructure to manage how traffic flows and how identities are verified at its gates. The Gateway API is that next-generation road system—built with urban planning in mind. The pavement is poured; now it’s time to open the on-ramps and let your applications cruise.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Tortoise and the Hare: do AI Agents Really Help for Software Development?</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Wed, 23 Apr 2025 05:37:56 +0000</pubDate>
      <link>https://dev.to/ipt/the-tortoise-and-the-hare-do-ai-agents-really-help-for-software-development-3fo4</link>
      <guid>https://dev.to/ipt/the-tortoise-and-the-hare-do-ai-agents-really-help-for-software-development-3fo4</guid>
      <description>&lt;p&gt;Making my development workflow as fast as possible is a big passion of mine. From customizing my development setup to get the last inkling of efficiency out of it, to thinking how to manage notes and knowledge resources to access them as quickly as possible. With the sudden ubiquity of AI in development tools, I came to wonder how AI could help me write code faster. Being quite the skeptic when it comes to AI actually generating code for me (using tools such as Cursor or GitHub Copilot), I came to investigate AI agents which specialise in code reviews. In this blog post I will share my experience using such an agent on a real world case. I will explore where such agents shine and where they are severely lacking.&lt;/p&gt;

&lt;h1&gt;
  
  
  I am an AI Skeptic
&lt;/h1&gt;

&lt;p&gt;Generally I am not fond of using AI to develop software. My background is mostly in systems software, where correctness of the software can be critical. This means that using tooling that is non-deterministic and might not produce adequate results makes me uneasy. Furthermore, even if AI were to produce amazing results, a developer relying on it could quickly lose understanding of the code. This results in skill atrophy and large risks if the AI reaches the limits of its capabilities. In other words, I am not keen on having any AI generating code for me on a large scale for anything more than a proof of concept or low risk project.&lt;/p&gt;

&lt;p&gt;Nonetheless, one would be foolish to ignore AI's capabilities when it comes to developer tooling.&lt;/p&gt;

&lt;h1&gt;
  
  
  AI Support Agents
&lt;/h1&gt;

&lt;p&gt;Thus starts my journey investigating AI agents that can support me in the software development lifecycle, but whose main use is &lt;em&gt;not&lt;/em&gt; to generate code. Many such agents exist, mostly focusing on reviewing code. I am quite the fan of such a use case, as the AI essentially plays the role of another developer I might work with. It reviews my code, provides feedback, suggestions, and potentially even improvements. It however does this immediately after I have opened a pull request, rather than having to wait for days or weeks on a human review.&lt;/p&gt;

&lt;p&gt;How is this different from using an AI that generates code you might ask? The main difference lies in the fact that I still have to think on how to solve the problem I am working on, and provide a base solution. This forces me to understand the issue at hand. Thus, I am much better prepared to accept or reject any suggestions from an AI than if the AI just generated a first solution for me. Moreover, people (myself included) tend to be slightly defensive about the code they write. Thus I will, in all likelihood, only accept AI generated code improvements if it offers a real improvement, rather than blindly incorporating them into the codebase.&lt;/p&gt;

&lt;p&gt;All in all, it is extremely unlikely that I will lose understanding of the codebase or have my problem solving skills atrophy, but I can iterate on reviews much faster.&lt;/p&gt;

&lt;h1&gt;
  
  
  CodeRabbitAI
&lt;/h1&gt;

&lt;p&gt;In order to gain first experiences with such an AI agent, I chose to try out &lt;a href="https://www.coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbitAI&lt;/a&gt;. This was not a thoroughly researched decision. The main reason I chose CodeRabbitAI is that I could try it out for free during 14 days and that it integrates well with GitHub. I am aware that performance between AI models varies greatly. However, CodeRabbitAI uses Claude under the hood, a model typically known to perform surprising well on programming tasks. I thus expect it to not perform significantly worse than any other state of the art model out there.&lt;/p&gt;

&lt;h1&gt;
  
  
  Starting Small
&lt;/h1&gt;

&lt;p&gt;In my opinion, such agents need to be tested on real world examples. One can see demos using AI to generate a dummy web app all over the place. However, common software projects are significantly larger, contain more complex logic, and are less standardized than these demos. Unfortunately, most software I work on professionally is not publicly available, so I cannot use CodeRabbitAI on these. I therefore picked two (still very small) personal projects of mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://github.com/f4z3r/gruvbox-material.nvim" rel="noopener noreferrer"&gt;NeoVim plugin&lt;/a&gt; providing a colour scheme.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://github.com/f4z3r/sofa" rel="noopener noreferrer"&gt;command execution engine&lt;/a&gt; to run templated commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both projects are extremely small, with under two thousand lines of code. Both projects are written in Lua, a quite uncommon language. I wanted to see how the AI fares against something it is unlikely to have seen too much during its training.&lt;/p&gt;

&lt;p&gt;With that in mind, I wrote a &lt;a href="https://github.com/f4z3r/gruvbox-material.nvim/pull/40" rel="noopener noreferrer"&gt;first pull request&lt;/a&gt; implementing a fix in highlight groups for pop-up menus in NeoVim. I enabled CodeRabbitAI to summarize the PR for me.&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%2F53fewpgxrfwwxvqln031.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%2F53fewpgxrfwwxvqln031.png" alt="Summary provided by CodeRabbitAI on my first PR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The summary looks good, even though it somehow marks some fixes as features. This is especially intriguing as I use &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" rel="noopener noreferrer"&gt;conventional commits&lt;/a&gt; and explicitly marked these changes as fixes. Additionally, CodeRabbitAI offers a "walkthrough" of the changes made in the PR. In the case of such a simple PR, I found the walkthrough to be mostly confusing. In the case of larger PRs I can however see how this may be appealing.&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%2F1i5blq1cnvnuqus2w729.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%2F1i5blq1cnvnuqus2w729.png" alt="A walkthrough of the changes in the first PR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In reality, I initially opened the PR with only the fixes the pop-up menus. I then pushed commits introducing the support for additional plugins later on. I would have expected CodeRabbitAI to complain that the new commits introduce changes unrelated to the PR, which is not seen as best practice. It did nothing of the sort.&lt;/p&gt;

&lt;p&gt;While the summary, walkthrough, and disregard for best practices were unsatisfying, one unexpected benefit emerged: the integration of linting feedback directly within the pull request comments. It provided nitpicks from linting tools (in this case &lt;a href="https://github.com/DavidAnson/markdownlint" rel="noopener noreferrer"&gt;&lt;code&gt;markdownlint&lt;/code&gt;&lt;/a&gt;. On one side, it is very disappointing to see that the AI agent did nothing more than lint the code and generate a nice comment out of the output. On the other hand it is quite nice that it introduces "quality gates" such as linting without me having to write a pipeline for it. Moreover, producing easily digestible output from a linter is nothing to be underestimated. The quality of life of having this directly as a comment rather than having to go through pipeline logs to read the raw linter output is quite nice. Is it worth two dozen USD per month? No, definitely not!&lt;/p&gt;

&lt;p&gt;On the upside, it did update the summary of the PR to reflect the other changes:&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%2F7jmypo9fwel98n9x97qp.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%2F7jmypo9fwel98n9x97qp.png" alt="Updated summary provided by CodeRabbitAI on my first PR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first PR was extremely trivial. It did not introduce any code containing logic. Other than not pointing out that it should probably have been two separate PRs, CodeRabbitAI fared as I would have expected another developer to have reviewed the PR. With two small differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CodeRabbitAI review was close to &lt;strong&gt;immediate&lt;/strong&gt; (took around 30-60 seconds to run). This is amazing to iterate quickly.&lt;/li&gt;
&lt;li&gt;Where I would have expected a human reviewer to point our the nitpick or simply approve, CodeRabbitAI is extremely &lt;strong&gt;verbose&lt;/strong&gt; with explanations, walkthroughs, and so on. This in turn wastes time for the author, as he/she would need to read through this. The verbosity could be nicer on larger PRs, but for small concise PRs this is massive overkill and borderline annoying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To further evaluate CodeRabbitAI's capabilities, I decided to test it on a pull request with more substantial changes...&lt;/p&gt;

&lt;h1&gt;
  
  
  A More Complex PR
&lt;/h1&gt;

&lt;p&gt;Armed with dampened expectations from my first PR, I opened &lt;a href="https://github.com/f4z3r/sofa/pull/3" rel="noopener noreferrer"&gt;another PR&lt;/a&gt; in the command execution repository implementing a feature affecting multiple files. These changes also update existing logic.&lt;/p&gt;

&lt;p&gt;In this second PR, CodeRabbitAI went above and beyond, and generated a walkthrough containing two sequence diagrams showcasing the control flow of the code that was modified! I was actually quite impressed by this. While probably not necessary for the author of a PR, this is great even only for documentation purposes. New team members with less experience may benefit from such visual aids to understand complex logic within the code. Unfortunately the diagrams didn't highlight the &lt;em&gt;specific modifications&lt;/em&gt; introduced by the pull request.&lt;/p&gt;

&lt;p&gt;However, the supporting text suddenly becomes more relevant when considering such PRs.&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%2Fvx0eypb4e1v5syzg0gre.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%2Fvx0eypb4e1v5syzg0gre.png" alt="One of the sequence diagrams generated by CodeRabbitAI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On top of that, CodeRabbitAI actually posted interesting comments. It found the odd nitpick here and there, but also found more meaningful potential issues. For instance, I modified a test configuration to use a different shell. CodeRabbitAI identified that this shell is not listed as a dependency anywhere in the repository, and that it would thus not work off-the-shelf. In this case this was only a test file used to parse the configuration and the configured shell did not affect anything, but this is a great finding generally.&lt;/p&gt;

&lt;p&gt;I also started conversing with CodeRabbitAI about some changes. Requesting it to give me a suggestion on some configurations. It managed just fine, but did not actually provide these as code suggestions that can be applied, but rather as code blocks in comments, which was a bit disappointing.&lt;/p&gt;

&lt;p&gt;Additionally, I decided to try to use CodeRabbitAI's commands feature. This enables ChatOps to control actions taken by CodeRabbitAI. I generated the PR title using one such command. The title turned out generic and not very informative. In CodeRabbitAI's defense, I am quite unsure how I would have named that PR.&lt;/p&gt;

&lt;p&gt;I then tried to get it to write docstrings for new functions that were introduced in the PR. It massively misunderstood the request, and created &lt;a href="https://github.com/f4z3r/sofa/pull/4" rel="noopener noreferrer"&gt;a PR adding docstrings to all functions&lt;/a&gt; in the affected files, even ones that already had docstrings... This goes to show that in some cases, it cannot even do what the most junior of all engineers would be capable of doing thanks to a even so tiny dose of common sense. Moreover, it started adding commits with emojis in the title. This goes to show that these AIs are probably not trained much on professional 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%2F0kcqsm6xjkflyk5xpunq.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%2F0kcqsm6xjkflyk5xpunq.png" alt="CodeRabbitAI not only breaking conventional commits but introducing emojis..."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that first disaster, with significantly less ambition, I requested it creates a PR to change a small typo. CodeRabbitAI informed me that it created a branch with the changes included, but that it was not capable of creating pull requests. This shocked me, considering it had created its first disaster PR no 10 minutes before.&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%2Fjwbeqdfikxqu0c5xndu3.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%2Fjwbeqdfikxqu0c5xndu3.png" alt="Fighting with CodeRabbitAI to fix my typo."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After another nudge, CodeRabbitAI however did &lt;a href="https://github.com/f4z3r/sofa/pull/5" rel="noopener noreferrer"&gt;create a PR&lt;/a&gt;. It targeted &lt;code&gt;main&lt;/code&gt; instead of the branch I was initially using. I guess this is my own fault though for not being specific enough.&lt;/p&gt;

&lt;p&gt;Finally, I also tried to get it to update the wording on a commit it did to use conventional commits. Unfortunately it seems that it only has access to the GitHub API and cannot execute any local &lt;code&gt;git&lt;/code&gt; commands. It is therefore not able to perform some relatively common operations in the SDLC that are not part of the GitHub API. However, I am guessing this is subject to change relatively soon with the emergence of technologies such as the &lt;a href="https://modelcontextprotocol.io/introduction" rel="noopener noreferrer"&gt;model context protocol&lt;/a&gt;, which would enable it to control external tools such as &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All in all, I would say CodeRabbitAI did as I would have expected after the first PR. It corrected nitpicks and allowed me to perform some simple actions. Did it deliver a review of the same quality like a senior engineer familiar with the project would have? No. In fact, in order to test this I intentionally implemented a feature that was already present in the repository, while making a couple design decisions that go against most of what the rest of the repository does. CodeRabbitAI neither detected that the logic I was introducing was already present in the codebase, nor did it complain about the sub-optimal design decisions. This goes to show that such agents are still not capable replacing humans with nuanced understanding of the project's history and architectural principles, potentially leading to the introduction of redundant or suboptimal solutions.&lt;/p&gt;

&lt;h1&gt;
  
  
  Dashboards!
&lt;/h1&gt;

&lt;p&gt;Another feature of AI agents next to the reviews is the analytics capabilities that come with them. In my personal opinion, analytics are important to measure the impact the introduction of such tooling has on the software delivery. CodeRabbitAI provides a couple nice dashboards on how much it is being used, and what kind of errors it helped uncover.&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%2F855vwq57tx742mawk65e.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%2F855vwq57tx742mawk65e.png" alt="Activity dashboard showing engagment with CodeRabbitAI"&gt;&lt;/a&gt;&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%2Flx3jz0kgur7y6iksl0yq.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%2Flx3jz0kgur7y6iksl0yq.png" alt="Dashboard showing overall adoption of CodeRabbitAI on the projects"&gt;&lt;/a&gt;&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%2F4lay5x98wr9gyulpf91g.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%2F4lay5x98wr9gyulpf91g.png" alt="Findings dashboard showing errors and suggestions by type"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did not try out CodeRabbitAI for long enough to have any meaningful metrics, but I am confident that the capabilities provided are enough to get a decent understanding of the quality of adoption.&lt;/p&gt;

&lt;p&gt;Moreover, CodeRabbitAI supports reporting. This allows to generate reports based on natural language prompts that could be useful for product owners to get insights of changes made to the software over the course of a sprint.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verdict
&lt;/h1&gt;

&lt;p&gt;While this whole article might seem like a slight rant against such tools, I would in fact wish I could use such tools at work. Not as a replacement for human reviewers, but as an addition to them. For instance, the quite verbose walkthroughs CodeRabbitAI provides can be a very helpful entrypoint to a human reviewer on larger PRs. Moreover, while the quality of the review is insufficient for projects where quality matters, having near instant feedback is amazing.&lt;/p&gt;

&lt;p&gt;Finally, as mentioned above, I believe one major selling point of such agents is in the way we humans interact with them. Even if the agent might do little more than execute linters or similar in the background, having the output of these tools in natural language directly as comments in the PRs is not to be underestimated. This is especially true in the age where more and more responsibility is being shifted to developers. With DevSecOps, developers have to understand and act upon the output of all kinds of tools. Presenting this output in a more understandable format, potentially enriched with explanations, can have a significant impact.&lt;/p&gt;

&lt;p&gt;Therefore, as a final word, I would actually encourage people to explore such agents to augment their workflow &lt;strong&gt;safely&lt;/strong&gt;, albeit with caution and a clear understanding of their &lt;strong&gt;limitations&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/f4z3r" rel="noopener noreferrer"&gt;
        f4z3r
      &lt;/a&gt; / &lt;a href="https://github.com/f4z3r/gruvbox-material.nvim" rel="noopener noreferrer"&gt;
        gruvbox-material.nvim
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Material Gruvbox colorscheme for Neovim written in Lua
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim/archive/master.zip" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flogo.png" alt="Gruvbox Material" width="25%"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Gruvbox Material&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/01033da302ce50a77b87423bdc412176b379a892529b9971db88153719c23fd4/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732d616e6f6e2f66347a33722f67727576626f782d6d6174657269616c2e6e76696d"&gt;&lt;img src="https://camo.githubusercontent.com/01033da302ce50a77b87423bdc412176b379a892529b9971db88153719c23fd4/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f636f6e7472696275746f72732d616e6f6e2f66347a33722f67727576626f782d6d6174657269616c2e6e76696d" alt="GitHub contributors"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/a19974e4ca72dabfea4e7782cf9b91efc495c71ad9b4d00890f76e2b28cd6cfe/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f67727576626f782d6d6174657269616c2e6e76696d"&gt;&lt;img src="https://camo.githubusercontent.com/a19974e4ca72dabfea4e7782cf9b91efc495c71ad9b4d00890f76e2b28cd6cfe/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f67727576626f782d6d6174657269616c2e6e76696d" alt="GitHub last commit"&gt;&lt;/a&gt;
&lt;a href="https://repology.org/project/vim%3Agruvbox-material.nvim/versions" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e3fe60bcdb4a06eff8f6d203c67e4bd018cc7834c2e73bc75f6f1326c5434e05/68747470733a2f2f7265706f6c6f67792e6f72672f62616467652f76657273696f6e2d666f722d7265706f2f6e69785f737461626c655f32355f30352f76696d25334167727576626f782d6d6174657269616c2e6e76696d2e737667" alt="nixpkgs stable 25.05 package"&gt;&lt;/a&gt;
&lt;a href="https://repology.org/project/vim%3Agruvbox-material.nvim/versions" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6ba0ae57d5825b36c63ac82e6c84b146e3b921f3004db6bdc7e14882b1a106ca/68747470733a2f2f7265706f6c6f67792e6f72672f62616467652f76657273696f6e2d666f722d7265706f2f6e69785f756e737461626c652f76696d25334167727576626f782d6d6174657269616c2e6e76696d2e737667" alt="nixpkgs unstable package"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;A NeoVim colour scheme in pure Lua allowing for highly flexible configuration and customization.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim#features" rel="noopener noreferrer"&gt;Features&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim#usage-and-configuration" rel="noopener noreferrer"&gt;Usage and Configuration&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/gruvbox-material.nvim/./docs/api.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-alert markdown-alert-note"&gt;
&lt;p class="markdown-alert-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This is a continuation of the original work from WittyJudge
&lt;a href="https://github.com/WIttyJudge/gruvbox-material.nvim" rel="noopener noreferrer"&gt;https://github.com/WIttyJudge/gruvbox-material.nvim&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;A port of &lt;a href="https://github.com/sainnhe/gruvbox-material" rel="noopener noreferrer"&gt;gruvbox-material&lt;/a&gt; colorscheme for Neovim
written in Lua. It does not aim to be 100% compatible with the mentioned repository, but rather
focuses on keeping the existing scheme stable and to support popular plugins. This colorscheme
supports both &lt;code&gt;dark&lt;/code&gt; and &lt;code&gt;light&lt;/code&gt; themes, based on configured background, and harder or softer
contrasts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dark theme:&lt;/strong&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-medium.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Light theme:&lt;/strong&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-medium.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

    Different contrasts
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contrast&lt;/th&gt;
&lt;th&gt;Dark&lt;/th&gt;
&lt;th&gt;Light&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-hard.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-hard.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-hard.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-hard.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-medium.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-medium.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-medium.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/dark-soft.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Fdark-soft.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/gruvbox-material.nvim/./assets/light-soft.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fgruvbox-material.nvim%2F.%2Fassets%2Flight-soft.png" alt=""&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Supported Plugins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-treesitter/nvim-treesitter" rel="noopener noreferrer"&gt;Treesitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-telescope/telescope.nvim" rel="noopener noreferrer"&gt;Telescope&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neovim.io/doc/user/lsp.html" rel="nofollow noopener noreferrer"&gt;LSP Diagnostics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kyazdani42/nvim-tree.lua" rel="noopener noreferrer"&gt;Nvim Tree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/preservim/nerdtree" rel="noopener noreferrer"&gt;NERDTree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mhinz/vim-startify" rel="noopener noreferrer"&gt;Startify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/airblade/vim-gitgutter" rel="noopener noreferrer"&gt;vim-gitgutter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mbbill/undotree" rel="noopener noreferrer"&gt;undotree&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liuchengxu/vista.vim" rel="noopener noreferrer"&gt;Vista.vim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/phaazon/hop.nvim" rel="noopener noreferrer"&gt;Hop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liuchengxu/vim-which-key" rel="noopener noreferrer"&gt;WhichKey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Yggdroot/indentLine" rel="noopener noreferrer"&gt;indentLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lukas-reineke/indent-blankline.nvim" rel="noopener noreferrer"&gt;Indent Blankline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rcarriga/nvim-notify" rel="noopener noreferrer"&gt;nvim-notify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RRethy/vim-illuminate" rel="noopener noreferrer"&gt;vim-illuminate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/hrsh7th/nvim-cmp" rel="noopener noreferrer"&gt;nvim-cmp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-neorg/neorg" rel="noopener noreferrer"&gt;neorg&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lukas-reineke/headlines.nvim/" rel="noopener noreferrer"&gt;headlines.nvim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-lualine/lualine.nvim/tree/master" rel="noopener noreferrer"&gt;lualine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;and many more ...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please feel free to open an issue if you want some features or other plugins to be included.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/f4z3r/gruvbox-material.nvim" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/f4z3r" rel="noopener noreferrer"&gt;
        f4z3r
      &lt;/a&gt; / &lt;a href="https://github.com/f4z3r/sofa" rel="noopener noreferrer"&gt;
        sofa
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A command execution engine powered by rofi.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/sofa/./assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fsofa%2F.%2Fassets%2Flogo.png" alt="Sofa" width="35%"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Sofa&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/53bf0c03371f1fadda6fe1189a8aa602a39aff60609ed2572c7c4cb35c410169/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f736f66613f6c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f6661253246626c6f622532466d61696e2532464c4943454e5345"&gt;&lt;img src="https://camo.githubusercontent.com/53bf0c03371f1fadda6fe1189a8aa602a39aff60609ed2572c7c4cb35c410169/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f736f66613f6c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f6661253246626c6f622532466d61696e2532464c4943454e5345" alt="GitHub License"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/61c8296308ca8f5d73cb187c82008aa09dc5ac3edb5fa8c42d45187e2b974ea1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f66347a33722f736f66613f6c6f676f3d676974687562266c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f666125324672656c6561736573"&gt;&lt;img src="https://camo.githubusercontent.com/61c8296308ca8f5d73cb187c82008aa09dc5ac3edb5fa8c42d45187e2b974ea1/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f66347a33722f736f66613f6c6f676f3d676974687562266c696e6b3d68747470732533412532462532466769746875622e636f6d25324666347a3372253246736f666125324672656c6561736573" alt="GitHub Release"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8cf94ab4ef3a06a8418879f24d914bbf59f0f2af92d9ff45be926cb4ff4ef543/68747470733a2f2f696d672e736869656c64732e696f2f6c7561726f636b732f762f66347a33722f736f66613f6c6f676f3d6c7561266c696e6b3d68747470732533412532462532466c7561726f636b732e6f72672532466d6f64756c657325324666347a3372253246736f6661"&gt;&lt;img src="https://camo.githubusercontent.com/8cf94ab4ef3a06a8418879f24d914bbf59f0f2af92d9ff45be926cb4ff4ef543/68747470733a2f2f696d672e736869656c64732e696f2f6c7561726f636b732f762f66347a33722f736f66613f6c6f676f3d6c7561266c696e6b3d68747470732533412532462532466c7561726f636b732e6f72672532466d6f64756c657325324666347a3372253246736f6661" alt="LuaRocks"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;A command execution engine powered by &lt;a href="https://github.com/davatorium/rofi" rel="noopener noreferrer"&gt;&lt;code&gt;rofi&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;&lt;code&gt;fzf&lt;/code&gt;&lt;/a&gt;.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/sofa#about" rel="noopener noreferrer"&gt;About&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#examples" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#integration" rel="noopener noreferrer"&gt;Integration&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#configuration" rel="noopener noreferrer"&gt;Configuration&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#development" rel="noopener noreferrer"&gt;Development&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#roadmap" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/sofa#license" rel="noopener noreferrer"&gt;License&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;About&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;sofa&lt;/code&gt; is a small utility to enable easy execution of templated commands. It can be used to store
snippets that you often rely on, or fully template complex commands. It is meant to be used with a
shortcut manager to enable launching from anywhere, but can also inject commands into your current
shell session for commands that make more sense to run there (see &lt;a href="https://github.com/f4z3r/sofa#integration" rel="noopener noreferrer"&gt;Integration&lt;/a&gt;).&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Examples&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;For Snippets Management&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;You can use &lt;code&gt;sofa&lt;/code&gt; for standard snippets management. Use the &lt;a href="https://github.com/f4z3r/sofa#integration" rel="noopener noreferrer"&gt;integration&lt;/a&gt; described
below, and have configuration such as:&lt;/p&gt;

Configuration
&lt;div class="highlight highlight-source-yaml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-ent"&gt;namespaces&lt;/span&gt;
  &lt;span class="pl-ent"&gt;lua&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;commands&lt;/span&gt;:
      &lt;span class="pl-ent"&gt;install-local&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;command&lt;/span&gt;: &lt;span class="pl-s"&gt;luarocks --local make --deps-mode {{ deps_mode }} {{ rockspec }}&lt;/span&gt;
        &lt;span class="pl-ent"&gt;description&lt;/span&gt;: &lt;span class="pl-s"&gt;Install rock locally&lt;/span&gt;
        &lt;span class="pl-ent"&gt;tags&lt;/span&gt;:
        - &lt;span class="pl-s"&gt;local&lt;/span&gt;
        - &lt;span class="pl-s"&gt;luarocks&lt;/span&gt;
        &lt;span class="pl-ent"&gt;interactive&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/f4z3r/sofa" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>devops</category>
      <category>devex</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Comprehensive Guide to Managing Large Scale Infrastructure with GitOps</title>
      <dc:creator>Jakob Beckmann</dc:creator>
      <pubDate>Tue, 08 Apr 2025 04:31:02 +0000</pubDate>
      <link>https://dev.to/ipt/a-comprehensive-guide-to-managing-large-scale-infrastructure-with-gitops-460c</link>
      <guid>https://dev.to/ipt/a-comprehensive-guide-to-managing-large-scale-infrastructure-with-gitops-460c</guid>
      <description>&lt;p&gt;GitOps is getting adopted more and more. However, there still seems to be some confusion as to what GitOps is, how it differs from regular CI/CD pipelines, and how to best adopt it. In this post we&lt;br&gt;
will quickly cover what GitOps is, and the three main lessons learned from using GitOps to manage infrastructure at scale both on premise and in the cloud.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitOps Overview
&lt;/h2&gt;

&lt;p&gt;GitOps is a set of principles enabling the operation of a system via version controlled, declarative configuration. More specifically, the &lt;a href="https://opengitops.dev/" rel="noopener noreferrer"&gt;OpenGitOps&lt;/a&gt; project defines four principles which define whether a system or set of systems is managed via GitOps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declarative: A system managed by GitOps must have its desired state expressed declaratively.&lt;/li&gt;
&lt;li&gt;Versioned and Immutable: Desired state is stored in a way that enforces immutability, versioning and retains a complete version history.&lt;/li&gt;
&lt;li&gt;Pulled Automatically: Software agents automatically pull the desired state declarations from the source.&lt;/li&gt;
&lt;li&gt;Continuously Reconciled: Software agents continuously observe actual system state and attempt to apply the desired state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;code&gt;git&lt;/code&gt; is not referenced anywhere, as GitOps is not bound to any tooling. However, in layman terms, many consider a system operated via &lt;code&gt;git&lt;/code&gt; to be a GitOps system. This is not quite correct.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitOps is More than CI/CD Pipelines
&lt;/h2&gt;

&lt;p&gt;Taking the "layman's definition" from above, any system that has CI/CD via pipelines triggered on repository changes would be a GitOps system. This is not accurate. Consider an IaC pipeline which applies declaratively defined infrastructure (such as a standard &lt;code&gt;opentofu apply&lt;/code&gt; in a pipeline, or a Docker build followed by a &lt;code&gt;kubectl apply&lt;/code&gt;). While such a system adheres to the first two principles, it does not adhere to the latter two. This implies that changes made to the target system are not corrected (reconciled) until the pipeline runs the next time. Similarly, if the pipeline fails for whatever reason, the desired state does not change the pipeline: a configuration drift is not detected, even if not reconciled.&lt;/p&gt;

&lt;p&gt;This is an important distinction when considering "standard CI/CD" and GitOps. Simply having something declared as code does not make it GitOps.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Advantages of GitOps
&lt;/h2&gt;

&lt;p&gt;GitOps has many advantages over standard ways of managing systems. The advantages of having a declarative desired state, version controlling it, and interacting with the system only via &lt;code&gt;git&lt;/code&gt; (or whatever version control system you use) are tremendous. From improved security and higher efficiency to better change visibility. These are well known to most people and will thus not be covered here.&lt;/p&gt;

&lt;p&gt;Drift detection and automatic reconciliation are the two other aspects that make GitOps absolutely amazing. This is especially true in the current day and age, with the proliferation of complex systems being worked on by many people concurrently. Being able to observe that the system is not in the desired state has massive advantages, such as for standard SRE operations. Continuous reconciliation ensures that manual operational tasks are kept to a minimum, and that systems cannot degrade over time as small undesired changes creep in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tooling
&lt;/h2&gt;

&lt;p&gt;In this post we will mostly focus on using GitOps to manage resources handled via the Kubernetes API, but it should be noted that GitOps as a concept is in no way restricted to Kubernetes. In the Kubernetes space there are two major players for GitOps: &lt;a href="https://argoproj.github.io/cd/" rel="noopener noreferrer"&gt;ArgoCD&lt;/a&gt; and &lt;a href="https://fluxcd.io/" rel="noopener noreferrer"&gt;FluxCD&lt;/a&gt;. We will not go into the details as to what the advantages for each tool are, other than saying that according to our own experience, ArgoCD might be more developer focused, while FluxCD might suit platform engineers with more Kubernetes experience that want more flexibility.&lt;/p&gt;

&lt;p&gt;The rest of this post is tool agnostic and everything we are talking about can be done with either tool (but some aspects might be easier to do with one or the other).&lt;/p&gt;
&lt;h2&gt;
  
  
  Infrastructure: Disambiguation
&lt;/h2&gt;

&lt;p&gt;Before we dive into how to structure your GitOps configuration, it might make sense to draw a line as to where infrastructure starts and where it ends. We consider infrastructure everything that is part of the platform provided to an application team. Hence this line might vary depending on the maturity of the platform you provide your teams. If we consider a simple Kubernetes platform with little additional abstraction for its users, the infrastructure would contain the Kubernetes platform itself as well as all system components that are shared between the teams, such as a central monitoring stack, a central credential management solution, centralized policy enforcement of specific Kubernetes resources, and the like.&lt;/p&gt;

&lt;p&gt;The lower end of the spectrum will likely not be managed by GitOps. That is simply because the GitOps tooling itself typically needs to run somewhere, and also needs to be bootstrapped somehow. Some tools such as FluxCD allow the GitOps controller to manage itself, but even in these cases the runtime for the controller needs to exist when the controller is initially installed, and is thus typically not part of the GitOps configuration.&lt;/p&gt;

&lt;p&gt;Now that this is cleared up, let us consider how the configuration should be managed.&lt;/p&gt;
&lt;h2&gt;
  
  
  App-of-Apps
&lt;/h2&gt;

&lt;p&gt;A very popular pattern for managing configuration via GitOps is the "app-of-apps" pattern. This was popularized by ArgoCD, but is also applicable to other tooling. We will use ArgoCD in the example below, but the same can be implemented using FluxCD Kustomizations.&lt;/p&gt;

&lt;p&gt;Let us consider a component from our infrastructure that we want to manage via GitOps. Typically, we would need to tell the GitOps controller how to manage this component. For instance, let us assume the component is installed via raw Kubernetes manifests. Then we would tell the GitOps controller which repository contains these manifests and in which namespace to install them. Depending on the controller you are using, you might also configure additional parameters such as how often it should be reconciled, whether it depends on other components, and so on. In ArgoCD jargon this would be an "Application" (the root of "app-of-apps" naming), and would look as follows:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sealed-secrets&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;chart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sealed-secrets&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://bitnami-labs.github.io/sealed-secrets&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.16.1&lt;/span&gt;
    &lt;span class="na"&gt;helm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;releaseName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sealed-secrets&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://kubernetes.default.svc"&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubeseal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You would then apply this &lt;code&gt;Application&lt;/code&gt; resource to Kubernetes. Your component would then be managed by GitOps, as any changes you push to the manifests repository would be reflected on the Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;Then a second infrastructure component needs to be installed, and you repeat the process. The result would be a second &lt;code&gt;Application&lt;/code&gt; which installs and manages a component. You might also want to version your deployment (such as using version &lt;code&gt;1.16.1&lt;/code&gt; of the Helm chart). This implies that lifecycles require a change to this &lt;code&gt;Application&lt;/code&gt; manifest, and thus a call against the Kubernetes API to edit it.&lt;/p&gt;

&lt;p&gt;The end result is a set of &lt;code&gt;Application&lt;/code&gt; resources, some of which you periodically modify when lifecycling a component. Now imagine you need to deploy your infrastructure elsewhere (for instance a second Kubernetes cluster in our example), or maybe even a couple dozen times. Then you need to manage this entire set of &lt;code&gt;Application&lt;/code&gt; resources on every platform. A better approach is to add an abstraction layer, which itself deploys the &lt;code&gt;Application&lt;/code&gt; resources via GitOps. Hence you put all your &lt;code&gt;Application&lt;/code&gt; resources into a repository, and define another, "higher level" &lt;code&gt;Application&lt;/code&gt; which deploys this repository. This means that when deploying to new platforms, you only need to deploy that one "higher level" &lt;code&gt;Application&lt;/code&gt;, and any changes to the component &lt;code&gt;Application&lt;/code&gt; resources can be made via Git, conforming to our GitOps approach. This "higher level" &lt;code&gt;Application&lt;/code&gt; is only there to deploy the component &lt;code&gt;Application&lt;/code&gt;s thus the name "app-of-apps". Visually, you thus have the following structure:&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%2F9403giq52kn3ptnxfmlo.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%2F9403giq52kn3ptnxfmlo.png" alt="Visual representation of app-of-apps pattern"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It should be noted that this also massively helps when customizing platforms. Typically, components cannot be deployed truly one-to-one in several places, but require slight configuration differences. Consider for instance hostnames for UIs of your components. Two of these components deployed in different locations cannot share the same hostname and routing. Using an "app-of-apps" approach allows you to define variables on the top level application, and inject these into the downstream applications such that they can slightly adapt the way they are installed. We will not dive deeper into how this is done as it is highly dependent on the tooling you use (ArgoCD uses &lt;code&gt;ApplicationSet&lt;/code&gt;, FluxCD uses variable substitution), but know this is enabled by such an approach.&lt;/p&gt;
&lt;h2&gt;
  
  
  Consolidating your Configuration
&lt;/h2&gt;

&lt;p&gt;In the organisation I first used GitOps at scale, we deployed all our components as Helm charts to a Kubernetes cluster. Each component was essentially contained within two different repositories in our version control system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the source code repository which typically built a Docker image as an artefact&lt;/li&gt;
&lt;li&gt;the Helm chart definition which referenced the Docker image from above&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When we then introduced GitOps, we decided to add a third repository containing the exact deployment definition (in our case the &lt;code&gt;Application&lt;/code&gt; declarations) for the component. Using the app-of-apps pattern from above, we could then reference each of these "GitOps repositories" and deploy specific overlays (customizations) of the &lt;code&gt;Application&lt;/code&gt; to specific platforms. This worked well for quite some time. However, with time the number of components we managed increased, and so did the number of target platforms to which these components needed to be deployed. This lead to quite a few issues.&lt;/p&gt;

&lt;p&gt;When a new target platform was introduced, all such "GitOps repositories" needed to be updated to contain a new overlay customizing the &lt;code&gt;Application&lt;/code&gt; to the specific platform. This is very tedious when you have several dozen such repositories.&lt;/p&gt;

&lt;p&gt;Moreover, components had dependencies to other components. This meant that we were referencing components within a repository that were defined in another repository. While not problematic in itself, this can become very tricky when one component has a dependency on a configuration value of another component. The configuration value is then duplicated in both repositories and becomes difficult to maintain. While this sounds like we did not properly separate the components, it is very common to see such cases in infrastructure configurations. Consider for instance a deployment of an ingress controller which defines a hostname suffix for its routes. All components deployed on the same Kubernetes platform that deploy a route/ingress will need to use exactly that hostname suffix in order to have valid routing.&lt;/p&gt;

&lt;p&gt;The above issue also results in tricky situations when configurations need to be changed for components that are dependent on one another. If the deployment configuration is separated into different repositories, PRs to these repositories need to be synchronized to ensure the deployment occurs at the same time.&lt;/p&gt;

&lt;p&gt;Finally, distributing the deployment configuration over so many repositories meant that it became increasingly difficult to have an overview of what is deployed on a target platform. One would need to navigate through dozens of repositories to check this is correctly done.&lt;/p&gt;

&lt;p&gt;After identifying these issues we decided to move all our configuration into a single repository. This repository would then contain a templated definition of the entire set of components which would need to be deployed. A set of platform definitions within the same repository would then feed values to templates to ensure consistent configuration. This massively helped us with to address the issues mentioned above. On top of that, it allows to version the "template" and thus enables rollouts of a versioned infrastructure layer.&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%2Fwt8td49qwxva9p5fnj5w.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%2Fwt8td49qwxva9p5fnj5w.png" alt="Grouping Application declarations into a single repository"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find an example repository of such a structure&lt;br&gt;
designed with FluxCD here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/f4z3r" rel="noopener noreferrer"&gt;
        f4z3r
      &lt;/a&gt; / &lt;a href="https://github.com/f4z3r/flux-demo" rel="noopener noreferrer"&gt;
        flux-demo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      This repository shows an example how one can use a single mono-repository to manage multiple clusters' infrastructure in a controlled fashion.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/f4z3r/flux-demo/./assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Ff4z3r%2Fflux-demo%2F.%2Fassets%2Flogo.png" alt="FluxCD" width="25%"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Flux Demo&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/d49b85fee643a77a1b3e35707a223e5be358cd0ece3c999f218d7e15485d3531/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f666c75782d64656d6f"&gt;&lt;img src="https://camo.githubusercontent.com/d49b85fee643a77a1b3e35707a223e5be358cd0ece3c999f218d7e15485d3531/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f66347a33722f666c75782d64656d6f" alt="GitHub last commit"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/b726528050260dd1b632df604eb56c379d272e973d0007e2595dc75dd83d002d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f666c75782d64656d6f"&gt;&lt;img src="https://camo.githubusercontent.com/b726528050260dd1b632df604eb56c379d272e973d0007e2595dc75dd83d002d/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f66347a33722f666c75782d64656d6f" alt="GitHub License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;An example how one can use a mono-repo to manage large infrastructure in a controlled fashion using FluxCD.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/f4z3r/flux-demo#setup" rel="noopener noreferrer"&gt;Setup&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/flux-demo#structure-of-the-repo" rel="noopener noreferrer"&gt;Structure of the Repo&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/flux-demo#application-vs-infrastructure" rel="noopener noreferrer"&gt;Application vs Infrastructure&lt;/a&gt; |
&lt;a href="https://github.com/f4z3r/flux-demo#workflow" rel="noopener noreferrer"&gt;Workflow&lt;/a&gt;&lt;/p&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Generate a GitHub PAT&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;See the documentation: &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For fined-grained control, grant the token Admin and content read/write permissions on the
repository.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Setup a Cluster with Flux&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Setup the required tooling with &lt;code&gt;devbox shell&lt;/code&gt;, then&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; install a cluster&lt;/span&gt;
kind create cluster -n demo
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; set the token&lt;/span&gt;
&lt;span class="pl-k"&gt;export&lt;/span&gt; GITHUB_TOKEN=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;redacted&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; onboard flux&lt;/span&gt;
flux bootstrap github \
  --token-auth \
  --owner=f4z3r \
  --repository=flux-demo \
  --branch=main \
  --path=clusters/demo \
  --personal&lt;/pre&gt;

&lt;/div&gt;

Sample output from Flux installation
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;► connecting to github.com
► cloning branch "main" from Git repository "https://github.com/f4z3r/flux-demo.git"
✔ cloned repository
► generating component manifests
✔ generated component manifests
✔ committed component manifests to "main" ("158753158f3c760f741f22ed7f68bdee1b66e475")
► pushing component manifests to "https://github.com/f4z3r/flux-demo.git"
► installing components in&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/f4z3r/flux-demo" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Gitops Bridge
&lt;/h2&gt;

&lt;p&gt;The last challenge we want to address in this blog post is a concept called a "GitOps bridge". In public cloud environments, there is typically a relatively strong cut between infrastructure deployed via Terraform (or any similar tool), and the infrastructure deployed via GitOps. For instance, one might deploy an Azure Kubernetes Service and some surrounding services (such as the required network, a container registry, etc) via Terraform, and them deploy components and applications within the AKS using GitOps. The issue that we face here is that the GitOps configuration very often depends on the Terraform configuration. Consider for instance the container registry. Its address is set up by Terraform, but is used in every image declaration in the GitOps configuration. One option is to duplicate such values in the respective configurations, while another option is to use a GitOps bridge.&lt;/p&gt;

&lt;p&gt;The GitOps bridge is an abstract concept on how to pass configuration values from tooling such as Terraform as inputs to the GitOps configuration. How this is done in practice very much depends on which tools you use. For instance, if looking at Terraform and FluxCD, a common way to achieve this is to have Terraform write a ConfigMap onto the AKS where the FluxCD controller will run containing all variables (and their values) that will be required by the GitOps configuration. The FluxCD controller then supports injecting variables from a ConfigMap via &lt;a href="https://fluxcd.io/flux/components/kustomize/kustomizations/#post-build-variable-substitution" rel="noopener noreferrer"&gt;variable substitution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using a GitOps bridge has the advantage that changes in the Terraform configurations are much less likely to break the GitOps configuration that builds on top of it. Moreover, it allows Terraform to directly bootstrap the entire GitOps setup when creating new platforms without the need to manually redefine the required variables in the GitOps repository.&lt;/p&gt;

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

&lt;p&gt;So, to recap, we have looked at what GitOps really is (and isn't). Understanding these basics is critical to correctly implement GitOps in your projects. On top of that, we looked at three best practices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use an app-of-apps pattern to improve resiliency for when you need to recreate platforms.&lt;/li&gt;
&lt;li&gt;Consider using a mono-repository for all your GitOps configuration as your setup grows.&lt;/li&gt;
&lt;li&gt;Have a look at GitOps bridges to improve the automation when setting up platforms and ensuring your Terraform and GitOps configurations are consistent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I hope this has helped you understand a bit better how to use GitOps at scale. If you have any questions or comments, feel free to let me know below.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>platformengineering</category>
      <category>gitops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How to Join Lists in Kafka Streams Applications</title>
      <dc:creator>Jan Kleine</dc:creator>
      <pubDate>Mon, 17 Mar 2025 06:00:00 +0000</pubDate>
      <link>https://dev.to/ipt/how-to-join-lists-in-kafka-streams-applications-1h28</link>
      <guid>https://dev.to/ipt/how-to-join-lists-in-kafka-streams-applications-1h28</guid>
      <description>&lt;p&gt;I recently joined a project where we do data processing with Kafka Streams applications and came across an interesting problem: "list joins". There was already a working solution to the problem, but I was not quite satisfied, so I dug a little deeper. Since I didn't find much on the topic online, I wanted to share my findings here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; The final topology is discussed here and you can find the code here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&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%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/iptch" rel="noopener noreferrer"&gt;
        iptch
      &lt;/a&gt; / &lt;a href="https://github.com/iptch/kafka-list-join-demo" rel="noopener noreferrer"&gt;
        kafka-list-join-demo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A demo showing how to do a list join in a kafka streams application.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Kafka List Join Demo&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;This project shows two ways to perform a list join in a Kafka streams application. A list join refers to joining a
record that contains a list with a KTable, such that each element in the list gets joined with the corresponding element
in the KTable.&lt;/p&gt;
&lt;p&gt;This image shows the high level idea, joining a persons address list, with the corresponding addresses:&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/iptch/kafka-list-join-demo/ListJoin.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fiptch%2Fkafka-list-join-demo%2FListJoin.png" alt="List join overview"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Check out &lt;a href="https://dev.to/ipt/how-to-join-lists-in-kafka-streams-applications-1h28" rel="nofollow"&gt;this blog post&lt;/a&gt; for a discussion of
the approaches.&lt;/p&gt;
&lt;p&gt;The tests should cover all relevant cases of message ordering and updates.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Building&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;To build and test the project, run&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;./gradlew clean build&lt;/pre&gt;

&lt;/div&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/iptch/kafka-list-join-demo" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;I assume you have some understanding of Kafka Streams, but I'll try to link to relevant documentation and resources where possible. It's worth checking out the &lt;a href="https://kafka.apache.org/documentation/streams/developer-guide/dsl-api.html" rel="noopener noreferrer"&gt;Kafka Streams DSL Developer Guide&lt;/a&gt; if you are unfamiliar with Kafka Streams.&lt;/p&gt;

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

&lt;p&gt;We have a topic with records (of type &lt;code&gt;Outer&lt;/code&gt;, e.g., a person) that contain (among other things) a list. The elements in the list reference records on another topic (of type &lt;code&gt;Inner&lt;/code&gt;, e.g., addresses). For every element in the outer topic we want to look up the corresponding record in our inner topic, and merge them to enhance our list.&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%2F4zbrya8uumwcgsy375da.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%2F4zbrya8uumwcgsy375da.png" alt="A topic of persons with multiple address IDs, an address topic. After the list join the person is augmented with full addresses." width="781" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Considerations
&lt;/h3&gt;

&lt;p&gt;There are a few additional constraints we need to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eventual Consistency:&lt;/strong&gt; The corresponding records on the inner topic may not be available right away, that is, records may come in after our outer record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates:&lt;/strong&gt; Our outer record may be updated over time. This may include updates (additions/removals) of inner list elements, but also updates to fields other than the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates:&lt;/strong&gt; In our specific use case we don't need duplicate list entries. However, all approaches discussed here can be modified to allow duplicates without too much effort.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The existing solution to the problem looks roughly like follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We set a timestamp on every record on our outer stream (needed later).&lt;/li&gt;
&lt;li&gt;We flat map our outer records so we have exactly one inner list element in every flat mapped record, to differentiate the flat records we change the keys to composite keys (of the form &lt;code&gt;&amp;lt;outer-key&amp;gt;$$&amp;lt;inner-key&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;We interpret the resulting stream as a KTable and perform a foreign key left join with the other KTable. The &lt;a href="https://kafka.apache.org/38/documentation/streams/developer-guide/dsl-api.html#ktable-ktable-fk-join" rel="noopener noreferrer"&gt;KTable-KTable left join&lt;/a&gt; (as opposed to, e.g., a &lt;a href="https://kafka.apache.org/documentation/streams/developer-guide/dsl-api.html#kstream-ktable-join" rel="noopener noreferrer"&gt;KStream-KTable join&lt;/a&gt;) is needed to fulfill our eventual consistency constrain.&lt;/li&gt;
&lt;li&gt;We group the the resulting records by the first part of the composite key.&lt;/li&gt;
&lt;li&gt;Finally, we reduce the records in each group by appending the lists, but we only consider the newest timestamp we see. Whenever we see a newer timestamp we discard all older records. This ensures that we do not add stale elements to our list that may be deleted in the current list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code looks similar to the below, though I left some smaller details out, such as how we forward tombstones. The full code can be found &lt;a href="https://github.com/iptch/kafka-list-join-demo/blob/main/src/main/java/ch/ipt/jkl/listjoindemo/timestamp/TimestampListJoinTopology.java" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerKStream&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;innerKTable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outerKStream&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mapValues&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerMapper&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flatMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerFlatMapper&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toTable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buildStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerSerde&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"listJoinFlatStore"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;leftJoin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;innerKTable&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerCount&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;outerInnerJoiner&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;buildStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerSerde&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"listJoinJoinerStore"&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;span class="na"&gt;toStream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;groupBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\$\\$"&lt;/span&gt;&lt;span class="o"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt;
            &lt;span class="nc"&gt;Grouped&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Serdes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;String&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;outerSerde&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;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;outerReducer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;buildStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerSerde&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"listJoinReducerStore"&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;span class="na"&gt;toStream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Issues
&lt;/h3&gt;

&lt;p&gt;While this approach works for our use case, I don't like it for two reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It feels hacky:&lt;/strong&gt; The timestamps feel a bit hacky and force us to change our data model just for this operation. While we can (and in production do) clear the timestamps after the list join is complete, we are using protobufs for our records. So the timestamp is part of the protobuf definition, even if downstream services don't need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is inefficient:&lt;/strong&gt; Whenever the original protobuf changes (be it, a change to the list, or any other field), &lt;em&gt;all&lt;/em&gt; list elements go through the join again, followed by reducing them all (including stale old records) again. That just feels wasteful.&lt;/p&gt;

&lt;p&gt;These issues prompted me to spend some time on the problem and see if I can come up with something better.&lt;/p&gt;
&lt;h2&gt;
  
  
  Improvements
&lt;/h2&gt;

&lt;p&gt;We want to reduce the number of join and reduce operations and remove the need for a dedicated timestamp field.&lt;/p&gt;

&lt;p&gt;For this we need a component that keeps track of the changes in the list of a record so that we only process relevant changes.&lt;/p&gt;
&lt;h3&gt;
  
  
  The List Join Pre-Processor
&lt;/h3&gt;

&lt;p&gt;The only way I found to accomplish this, is to write a custom Processor using the &lt;a href="https://kafka.apache.org/documentation/streams/developer-guide/processor-api.html" rel="noopener noreferrer"&gt;Processor API&lt;/a&gt; provided by Kafka Streams.&lt;/p&gt;

&lt;p&gt;The list join pre-processor performs the task of the flat-map operation from before, but maintains internal state&lt;sup id="fnref1"&gt;1&lt;/sup&gt; to remember what list elements are currently in each record. When a record gets updated, it compares the previous and new list, and (1) only issues flat mapped records for new list elements and (2) issues tombstones for elements removed from the list.&lt;/p&gt;

&lt;p&gt;(1) ensures that we only have to join new list elements, reducing duplicate work. (2) allows us to more efficiently remove old list elements, so we don't need to rely on a hacky timestamp.&lt;/p&gt;

&lt;p&gt;The pre-processor also always forwards a copy of the current record with an empty list. We need this later to correctly propagate changes to other fields that are not the list.&lt;/p&gt;

&lt;p&gt;The logic of the pre-processor, which can also be found &lt;a href="https://github.com/iptch/kafka-list-join-demo/blob/7edae940d4a51003d74082d29e171798f1394a3a/src/main/java/ch/ipt/jkl/listjoindemo/current/operator/PreProcessorSupplier.java#L100-L140" rel="noopener noreferrer"&gt;here&lt;/a&gt;, looks as follows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TOuter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// in case of tombstone or empty list, this map is empty&lt;/span&gt;
    &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TOuter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;flatValuesMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flatMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toMap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;innerIdStringExtractor&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;
            &lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;newIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flatValuesMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keySet&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oldIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofNullable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Set:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;copyOf&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// if both new and old ids are empty we don't need to do anything and can short circuit&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newIds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;oldIds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// forward flat mapped records for new list elements&lt;/span&gt;
    &lt;span class="nc"&gt;Sets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SetView&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;addedIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;difference&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newIds&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oldIds&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;newId&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;addedIds&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;forward&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;newId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flatValuesMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// send tombstones for removed list elements&lt;/span&gt;
    &lt;span class="nc"&gt;Sets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SetView&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;removedIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;difference&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldIds&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newIds&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;removedId&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;removedIds&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;forward&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;removedId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// if the current list is empty delete the list from the store and send a tombstone&lt;/span&gt;
    &lt;span class="c1"&gt;// for the empty list record, otherwise save the current list and send an empty list&lt;/span&gt;
    &lt;span class="c1"&gt;// record to propagate changes to other fields&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newIds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;forward&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;listStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;forward&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listCleaner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
        &lt;span class="n"&gt;listStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newIds&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;h3&gt;
  
  
  Better Reduce Operation
&lt;/h3&gt;

&lt;p&gt;Now that the flat-map and the subsequent join are improved, we can further improve the reduce operation.&lt;/p&gt;

&lt;p&gt;Previously, the reduce operation was responsible for filtering out outdated records (based on the timestamp) and aggregating the list of inner elements. This was done on all flat-mapped values (including outdated values) and needed to happen every time anything in the original outer record changed.&lt;/p&gt;

&lt;p&gt;Now, that we can issue tombstones for removed elements, we can transition from a KStream &lt;a href="https://kafka.apache.org/38/javadoc/org/apache/kafka/streams/kstream/KStream.html#groupBy(org.apache.kafka.streams.kstream.KeyValueMapper,org.apache.kafka.streams.kstream.Grouped)" rel="noopener noreferrer"&gt;group-by&lt;/a&gt; and &lt;a href="https://kafka.apache.org/38/javadoc/org/apache/kafka/streams/kstream/KGroupedStream.html#reduce(org.apache.kafka.streams.kstream.Reducer,org.apache.kafka.streams.kstream.Materialized)" rel="noopener noreferrer"&gt;reduce&lt;/a&gt; to a KTable &lt;a href="https://kafka.apache.org/38/javadoc/org/apache/kafka/streams/kstream/KTable.html#groupBy(org.apache.kafka.streams.kstream.KeyValueMapper,org.apache.kafka.streams.kstream.Grouped)" rel="noopener noreferrer"&gt;group-by&lt;/a&gt; and &lt;a href="https://kafka.apache.org/38/javadoc/org/apache/kafka/streams/kstream/KGroupedTable.html#reduce(org.apache.kafka.streams.kstream.Reducer,org.apache.kafka.streams.kstream.Reducer,org.apache.kafka.streams.kstream.Materialized)" rel="noopener noreferrer"&gt;reduce&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The KTable reduce works a bit differently than the KStream reduce. Where the KStream reduce has a single reducer that receives the current aggregate and any new record (be it a tombstone or a normal record), the KTable reduce has an adder reducer and a remover reducer.&lt;/p&gt;

&lt;p&gt;The adder reducer is called when ever an element is added to the KTable and receives the current aggregate and the newly added record.&lt;/p&gt;

&lt;p&gt;The remover reducer is called when ever an element is removed from the KTable (via a tombstone) and is provided with the current aggregate and the removed record. This allows us to remove the inner element from the current aggregate, as we know exactly which one got removed.&lt;/p&gt;

&lt;p&gt;Our adder looks as follows. Remember that our pre-processor also sends a copy of the original record with an empty list. This way we can update other fields as well.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Outer&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Outer&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Outer&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// if list is empty update all fields other than the list&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAllInner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerList&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// otherwise add inner item to current list&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAllInner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerList&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;p&gt;The full code can also be found &lt;a href="https://github.com/iptch/kafka-list-join-demo/blob/main/src/main/java/ch/ipt/jkl/listjoindemo/preprocessor/operator/OuterReducerAdder.java" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our remover only has to worry about removing the list elements. In our case we do it based on the inner ID.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Outer&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Outer&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Outer&lt;/span&gt; &lt;span class="n"&gt;oldValue&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// if oldValue inner list is empty, there is nothing to do&lt;/span&gt;
    &lt;span class="c1"&gt;// the adder handles empty list updates&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// remove the single inner value from the current list,&lt;/span&gt;
    &lt;span class="c1"&gt;// in this case we do it by id&lt;/span&gt;
    &lt;span class="nc"&gt;Inner&lt;/span&gt; &lt;span class="n"&gt;innerToRemove&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oldValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getFirst&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Inner&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;innerList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerToRemove&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;currentValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clearInner&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAllInner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;innerList&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;p&gt;The full code can also be found &lt;a href="https://github.com/iptch/kafka-list-join-demo/blob/main/src/main/java/ch/ipt/jkl/listjoindemo/preprocessor/operator/OuterReducerRemover.java" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  New Topology
&lt;/h2&gt;

&lt;p&gt;Incorporating the two improvements from above, we end up with a topology which looks surprisingly similar to our initial topology.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerKStream&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;innerKtable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outerKStream&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preProcessorSupplier&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toTable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buildStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerSerde&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"listJoinFlatStore"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;leftJoin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;innerKTable&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// if the inner list is empty the foreign key extractor should&lt;/span&gt;
            &lt;span class="c1"&gt;// return null so the outer is joined with null&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInnerCount&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;outerInnerJoiner&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;buildStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerSerde&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"listJoinJoinerStore"&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;span class="na"&gt;groupBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="c1"&gt;// group by first part of composite key&lt;/span&gt;
            &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;KeyValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\$\\$"&lt;/span&gt;&lt;span class="o"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Grouped&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Serdes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;String&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;outerSerde&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;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;listAdder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;listRemover&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;buildStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outerSerde&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"listJoinReducerStore"&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;span class="na"&gt;toStream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Again, I skipped over some smaller details, like how we forward tombstones. The full topology can be found &lt;a href="https://github.com/iptch/kafka-list-join-demo/blob/main/src/main/java/ch/ipt/jkl/listjoindemo/preprocessor/operator/OuterReducerRemover.java" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since copying this code to different topologies is a bit cumbersome, we decided to wrap this entire sub-topology in a &lt;code&gt;ListJoin&lt;/code&gt; utility. This allows easier reusing of the code, as the developer only has to provide a couple key components and make the resulting topologies easier to understand.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// setup&lt;/span&gt;
&lt;span class="nc"&gt;ListJoin&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;...&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;listJoin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ListJoin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;// specify joiner, reducers, etc.&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// later in the topology&lt;/span&gt;
&lt;span class="nc"&gt;KStream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;...&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;joinedKStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listJoin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myLeftKStream&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;myRightKTable&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;This topology is probably still not optimal. For one, it may make sense to deduplicate the empty list records to further reduce downstream work. However, in our case this is not necessary.&lt;/p&gt;

&lt;p&gt;In any case, this new approach is a clear improvement over the previous timestamp based approach.&lt;/p&gt;

&lt;p&gt;Have you solved a similar, or even the same, problem before? If so, please consider leaving a comment as I'd very much like to hear what you did. Likewise, if you have found issues with the above code or have ideas for improvements, I'm keen to hear from you!&lt;/p&gt;
&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;Cover image: &lt;a href="https://www.pexels.com/photo/close-up-photo-of-blue-background-2441454/" rel="noopener noreferrer"&gt;"Close Up Photo of Blue Background" by Harrison  Candlin&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__2732274"&gt;
    &lt;a href="/jankleine" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2732274%2Ff6769039-9e89-4990-b520-8f1f95714d22.jpg" alt="jankleine image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/jankleine"&gt;Jan Kleine&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/jankleine"&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;






&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Maintaining state can be done via a &lt;a href="https://kafka.apache.org/documentation/streams/developer-guide/processor-api.html#state-stores" rel="noopener noreferrer"&gt;state store&lt;/a&gt;, which is automatically backed up to a changelog topic in kafka, ensuring the processor can tolerate application scaling and recover in case of application failures. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>eventdriven</category>
      <category>kafka</category>
      <category>java</category>
    </item>
    <item>
      <title>Data Governance with dbt, Terraform, and Dataplex: A Practical Guide to BigQuery Policy Tags</title>
      <dc:creator>Diana Tahchieva</dc:creator>
      <pubDate>Mon, 24 Feb 2025 07:39:48 +0000</pubDate>
      <link>https://dev.to/ipt/data-governance-with-dbt-terraform-and-dataplex-a-practical-guide-to-bigquery-policy-tags-5f7d</link>
      <guid>https://dev.to/ipt/data-governance-with-dbt-terraform-and-dataplex-a-practical-guide-to-bigquery-policy-tags-5f7d</guid>
      <description>&lt;p&gt;Welcome to a hands-on guide for implementing BigQuery Policy Tags, an important feature for data governance. If you're new to Google Cloud Platform (GCP) and have heard of dbt, Terraform, and Data Catalog but aren't sure how they work together, this tutorial provides a simple, practical example. We'll apply policy tags to a sample clients table in BigQuery to enforce data governance. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Are Policy Tags?&lt;/strong&gt;&lt;br&gt;
Policy Tags are classification labels for data in &lt;a href="https://cloud.google.com/bigquery?hl=en" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;, helping manage privacy, compliance, and access control. These tags are particularly important in industries like healthcare and finance, where data sensitivity is a key concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Use dbt, Terraform, and Dataplex for Policy Tag Management?&lt;/strong&gt;&lt;br&gt;
Using dbt and Terraform to define Policy Tags as code, and Dataplex for governance, you can keep track of changes, facilitate team collaboration, audit activities, and easily roll back to previous configurations.&lt;br&gt;
Additionally, you can consistently manage Policy Tags across multiple datasets and projects, reducing manual labor. Automation through these tools minimizes human error and ensures policy tags are consistently applied.&lt;br&gt;
While dbt integrates seamlessly into existing data pipelines, applying Policy Tags during data transformation and modeling, dataplex unifies governance across various data stores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding the Tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let us have a closer look at what are dbt, terraform and dataplex.&lt;br&gt;
• &lt;a href="https://docs.getdbt.com/" rel="noopener noreferrer"&gt;dbt&lt;/a&gt; (Data Build Tool) is an open-source tool for transforming and modeling data within your data warehouse (like BigQuery). dbt enables you to write a more maintainable SQL code and allows you to attach metadata, such as Policy Tags, to your data transformations. Additionally, as the objects in BigQuery can be referenced, dbt is able to make a Directed Acyclic Graph of the entire data platform, based on which we can observe all dependencies among the data. &lt;br&gt;
• &lt;a href="https://developer.hashicorp.com/terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; is an Infrastructure as Code (IaC) tool that lets you define and manage your cloud infrastructure using configuration files. Terraform automates the provisioning and management of resources including enabling APIs, managing permissions, and creating Policy Taxonomies and Tags.&lt;br&gt;
• &lt;a href="https://cloud.google.com/dataplex?hl=en" rel="noopener noreferrer"&gt;Dataplex&lt;/a&gt; is a Google Cloud service that provides unified data governance and management. It helps discover, organize, and manage data assets, ensuring consistent data handling and enforcement of Policy Tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How They Work Together&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OK, now that we know what dbt, Terraform, and Dataplex are, let's explore how they work together.&lt;br&gt;
Terraform establishes the required infrastructure and permissions for managing Policy Tags by creating taxonomies and tags within Google Cloud Data Catalog. At the same time, dbt handles data transformation in BigQuery, applying Policy Tags to specific columns within your models. The meta section in the dbt model facilitates metadata association, ensuring proper organization and governance. Meanwhile, Dataplex functions as a centralized governance layer, maintaining consistency in the application and monitoring of Policy Tags across all data assets. Together, these tools create a seamless, scalable, and automated data governance system that enhances visibility, reduces manual effort, and minimizes the risk of human error.&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%2Fjfjnb67mkktbqgsr8b7x.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%2Fjfjnb67mkktbqgsr8b7x.png" alt="The figure illustrates how Terraform, Dataplex, BigQuery, and dbt work together for automated data governance. Terraform provisions infrastructure and Policy Tags, while dbt transforms data and applies tags in BigQuery. Dataplex enforces policies and monitors compliance, ensuring secure and consistent data management." width="376" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementing Policy Tags: Step-by-Step Guide&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, you need to create a GCP project and connect it to a billing account; otherwise, Terraform won’t be able to function. Don’t worry—this project is small and won’t incur any costs (my billing account is still at zero). Just remember to delete it after testing by running &lt;code&gt;terraform destroy&lt;/code&gt; in the command line—I’ll remind you at the end of the tutorial.&lt;br&gt;
For convenience, I’ve created a Git project that you can &lt;a href="https://github.com/dianaTahchieva/gcp-data-governance.git" rel="noopener noreferrer"&gt;clone&lt;/a&gt;. It contains two sub-projects: one for Terraform (gcp-data-catalog-terraform) and one for dbt (data_catalog_dbt_project). In a real-world scenario, these sub-projects would likely be managed as separate projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The terraform project has the following structure:&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;variables.tf&lt;/strong&gt;: Defines the GCP project ID and region.&lt;br&gt;
• &lt;strong&gt;iam.tf&lt;/strong&gt;: Creates service accounts for Terraform and dbt, assigning necessary IAM roles.&lt;br&gt;
• &lt;strong&gt;datacatalog.tf&lt;/strong&gt;: Defines the taxonomy for organizing Policy Tags and creates tags for PII and non-PII data.&lt;br&gt;
• &lt;strong&gt;bigquery.tf&lt;/strong&gt;: Creates a BigQuery dataset and a table without predefined policy tags.&lt;br&gt;
• &lt;strong&gt;output.tf&lt;/strong&gt;: Outputs IDs for easy access to created resources.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;dbt project&lt;/strong&gt; in this example is a significantly simplified version of a standard dbt project and includes only the essential files necessary for our use case. Here’s a brief description of each file and its purpose:&lt;br&gt;
• &lt;strong&gt;dbt_project.yml&lt;/strong&gt;: This is the primary configuration file for your dbt project. It includes metadata about the project, such as the project name and version, paths to model files, and configurations for materializations and other project-wide settings.&lt;br&gt;
• &lt;strong&gt;profiles.yml&lt;/strong&gt;: This configuration file contains the connection details and credentials required for dbt to connect to your data warehouse (BigQuery in this case). It includes information such as the project ID, dataset, and authentication method (service account key file).&lt;br&gt;
• &lt;strong&gt;models/customers/&lt;/strong&gt;: This directory holds the models for the project. In dbt, a model is essentially a SQL file that transforms raw data into more refined tables. This directory contains our specific model for customers.&lt;br&gt;
o   &lt;strong&gt;customers.sql&lt;/strong&gt;: This SQL file represents the transformation logic for the customers’ data. It selects and processes the necessary columns from the raw data, applying the transformations required for our data analysis needs.&lt;br&gt;
o   &lt;strong&gt;customers.yml&lt;/strong&gt;: This YAML file provides additional metadata about the customers model. It includes descriptions of each column, tests to ensure data quality, and policy tags to enforce data governance rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Enable APIs with Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, enable the necessary Google Cloud APIs for your project using Terraform. These are: &lt;br&gt;
• Identity and Access Management (IAM) API&lt;br&gt;
• BigQuery &lt;br&gt;
• Data Catalog API &lt;br&gt;
• Dataplex API&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Grants Permissions with Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let us walk through how to create and configure a service account that Terraform and dbt can use to interact with Google Cloud resources.&lt;br&gt;
A service account in Google Cloud is like a robot user—it allows Terraform and dbt to authenticate and interact with GCP without needing a human user to log in.&lt;br&gt;
In our setup, Terraform will provision BigQuery datasets, tables, and policies, while dbt will query and transform the data. To ensure that the permissions for Terraform and dbt are properly separated, we will define two distinct service accounts.&lt;/p&gt;

&lt;p&gt;We can create the new service accounts (called &lt;strong&gt;terraform-sa&lt;/strong&gt; and &lt;strong&gt;dbt-sa&lt;/strong&gt;) by running:&lt;br&gt;
&lt;code&gt;gcloud iam service-accounts create terraform-sa --display-name "Terraform Service Account" --project gcp-data-governance&lt;/code&gt;&lt;br&gt;
&lt;code&gt;gcloud iam service-accounts create dbt-sa --display-name "dbt Service Account" --project gcp-data-governance&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Alternatively, you can add the service account manually in the GCP console (IAM &amp;amp; Admin &amp;gt; Service Accounts). Then, in Terraform, you only manage the IAM roles in &lt;strong&gt;iam.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Terraform service account needs permissions to manage BigQuery, Data Catalog, and Dataplex. Add these IAM roles in your Terraform configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Give the Terraform user permissions to manage IAM, Dataplex, and BigQuery&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_project_iam_member"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_bigquery_admin"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"roles/bigquery.admin"&lt;/span&gt;
  &lt;span class="nx"&gt;member&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="s2"&gt;"serviceAccount:terraform-sa@gcp-data-governance.iam.gserviceaccount.com"&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_project_iam_member"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_datacatalog_admin"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"roles/datacatalog.admin"&lt;/span&gt;
  &lt;span class="nx"&gt;member&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"serviceAccount:terraform-sa@gcp-data-governance.iam.gserviceaccount.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_project_iam_member"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_dataplex_admin"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"roles/dataplex.admin"&lt;/span&gt;
  &lt;span class="nx"&gt;member&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"serviceAccount:terraform-sa@gcp-data-governance.iam.gserviceaccount.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_project_iam_member"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_sa_admin"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"roles/iam.serviceAccountAdmin"&lt;/span&gt;
  &lt;span class="nx"&gt;member&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"serviceAccount:terraform-sa@gcp-data-governance.iam.gserviceaccount.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: For more information on the available roles, refer to the &lt;a href="https://cloud.google.com/iam/docs/understanding-roles" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. As a best practice, always assign the minimum permissions necessary. For instance, if a user only needs to view the data, provide read-only access.&lt;/p&gt;

&lt;p&gt;• To allow dbt to authenticate, we need to generate a JSON &lt;strong&gt;key file&lt;/strong&gt; for our service account, which we can do in two ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1&lt;/strong&gt;: Using the Google Cloud Console&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Go to IAM &amp;amp; Admin → Service Accounts in the Google Cloud Console.&lt;/li&gt;
&lt;li&gt; Select the service account dbt-sa.&lt;/li&gt;
&lt;li&gt; Navigate to the Keys tab and click "Add Key".&lt;/li&gt;
&lt;li&gt; Choose "JSON", then click "Create".&lt;/li&gt;
&lt;li&gt; The key.json file will be downloaded to your computer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Option 2&lt;/strong&gt;: Using the gcloud CLI&lt;/p&gt;

&lt;p&gt;If you prefer using the command line, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts keys create dbt-sa-key.json  &lt;span class="nt"&gt;--iam-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dbt-sa@your-project-id.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;• Now that we have our dbt-sa-key.json, we need to  update dbt's configuration to use the service account. Open &lt;strong&gt;profiles.yml&lt;/strong&gt; and update the value of your keyfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data_catalog_dbt&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dev&lt;/span&gt;
  &lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigquery&lt;/span&gt;
      &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;service-account&lt;/span&gt;
      &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"gcp-data-governance"&lt;/span&gt;  &lt;span class="c1"&gt;# project_id&lt;/span&gt;
      &lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;multiplexer_dataset&lt;/span&gt;
      &lt;span class="nx"&gt;threads&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
      &lt;span class="nx"&gt;keyfile&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/path/to/dbt-sa-key.json"&lt;/span&gt;  &lt;span class="c1"&gt;# Reference your environment variable&lt;/span&gt;
      &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"europe-west6"&lt;/span&gt;  &lt;span class="c1"&gt;# Set this to your BigQuery region&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Create Data Policy Taxonomies and Tags with Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To allow Terraform to interact with Google Data Catalog we need to specify providers. The providers allow us to manage resources on a specific cloud platform. We have created them in &lt;strong&gt;datacatalog.tf&lt;/strong&gt; as they are relevant to Data Catolg API, however you can make a separate file providers.tf and define them there. The google provider is the standard Terraform provider for managing GCP resources, while the google-beta provider gives access to Google Data Catalog, which is needed for policy tags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Not Just Use google-beta?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some resources (e.g., IAM, BigQuery datasets) don’t need google-beta, so we keep google for those. On the other hand side Data Catalog resources require google-beta, so we configure both providers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;google&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;"hashicorp/google"&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;"~&amp;gt; 4.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"google"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"google-beta"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A taxonomy is a container containing data policy tags ( a grouping mechanism). &lt;strong&gt;Fine-Grained Access Control&lt;/strong&gt; specified in the activated policy tags ensures that only authorized users can see or query certain columns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a Data Catalog Taxonomy&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_data_catalog_taxonomy"&lt;/span&gt; &lt;span class="s2"&gt;"multiplexer_pii_taxonomy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Multiplexer PII Taxonomy"&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;"Taxonomy for sensitive data classification"&lt;/span&gt;
  &lt;span class="nx"&gt;project&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;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;activated_policy_types&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FINE_GRAINED_ACCESS_CONTROL"&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;In our test-case we define only two tags: sensitive (PII) and non-sensitive data. The PII tags ensure proper access control. These tags will later be attached to BigQuery columns in dbt. More tags can be added as needed for better governance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create Policy Tags for PII and Non-PII&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_data_catalog_policy_tag"&lt;/span&gt; &lt;span class="s2"&gt;"pii_sensitive"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;taxonomy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_data_catalog_taxonomy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiplexer_pii_taxonomy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PII"&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;"Policy tag for Personally Identifiable Information"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_data_catalog_policy_tag"&lt;/span&gt; &lt;span class="s2"&gt;"non_pii_sensitive"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;taxonomy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_data_catalog_taxonomy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiplexer_pii_taxonomy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Non-PII"&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;"Policy tag for non-sensitive data"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Apply Policy Tags with dbt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that the Policy Tags are defined, we will attach them to relevant columns in the dbt project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Set up your dbt project in a directory parallel to your Terraform project.
Before running dbt, ensure that your service account key is correctly set (see explanation in &lt;strong&gt;Step 2&lt;/strong&gt;). Remember, dbt &lt;strong&gt;profile.yml&lt;/strong&gt; should be configured to use BigQuery and your service account key.
Before running dbt, make sure all dependencies are installed:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt deps –upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Afterwards check if dbt can connect to BigQuery, by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If successful, you’ll see the message: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All checks passed!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To run all dbt models use &lt;code&gt;dbt run&lt;/code&gt; and to run only specific models, e.g., customers use &lt;code&gt;dbt run -s customers&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Define columns and attach Policy Tags in your dbt models.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Navigate to your dbt project and modify your customers.yml to attach Policy Tags:&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;version&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customers&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Customers data with PII"&lt;/span&gt;
    &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customer_id&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Unique customer identifier"&lt;/span&gt;
        &lt;span class="nx"&gt;policy_tags&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"{{ var('non_pii_sensitive_policy_tag_id') }}"&lt;/span&gt;
      &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Customer email"&lt;/span&gt;
        &lt;span class="nx"&gt;policy_tags&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"{{ var('pii_sensitive_policy_tag_id') }}"&lt;/span&gt;
      &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;phone_number&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Customer phone number"&lt;/span&gt;
        &lt;span class="nx"&gt;policy_tags&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"{{ var('pii_sensitive_policy_tag_id') }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run dbt to apply the Policy Tags to BigQuery automatically: &lt;code&gt;dbt run -s customers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Integrating dbt, Terraform, and Dataplex allows you to efficiently manage BigQuery Policy Tags, enforcing data governance policies in a scalable and automated way. This approach enhances security, compliance, and operational efficiency while reducing manual effort.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To avoid unnecessary charges, remember to destroy your project after testing by running &lt;code&gt;terraform destroy&lt;/code&gt; in the command line.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datagovernance</category>
      <category>googlecloud</category>
      <category>terraform</category>
      <category>dbt</category>
    </item>
    <item>
      <title>Rusty Backends</title>
      <dc:creator>Jan Kleine</dc:creator>
      <pubDate>Tue, 21 Jan 2025 06:30:00 +0000</pubDate>
      <link>https://dev.to/ipt/rusty-backends-3551</link>
      <guid>https://dev.to/ipt/rusty-backends-3551</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post was written together &lt;a class="mentioned-user" href="https://dev.to/sekael"&gt;@sekael&lt;/a&gt; and &lt;a class="mentioned-user" href="https://dev.to/zkck"&gt;@zkck&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rust has been a developer favorite for many years now and is consistently highly admired by developers&lt;a href="https://survey.stackoverflow.co/2023/#section-admired-and-desired-programming-scripting-and-markup-languages" rel="noopener noreferrer"&gt;[1]&lt;/a&gt;. It made a buzz when it was introduced as a complementary language to C and Assembly in the Linux kernel with version 6.8&lt;a href="https://en.wikipedia.org/wiki/Rust_for_Linux" rel="noopener noreferrer"&gt;[2]&lt;/a&gt; and its speed and memory safety make it a popular choice for low-level and performance-critical applications. But with the whole world using the internet, we wanted to find out whether it can also do web and challenge the reigning backend champions like Java Spring or Go.&lt;/p&gt;

&lt;p&gt;To find answers, we wanted to get our hands dirty with three popular Rust web frameworks including &lt;a href="https://rocket.rs/" rel="noopener noreferrer"&gt;rocket&lt;/a&gt;, &lt;a href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;axum&lt;/a&gt;, and &lt;a href="https://actix.rs/" rel="noopener noreferrer"&gt;actix&lt;/a&gt;, and get a feeling for their performance, features, and most importantly the developer experience.&lt;/p&gt;

&lt;p&gt;In this post, we will examine each of these frameworks by implementing the same example API endpoints in each one, connect to a MongoDB database, and run the server in a Docker container. &lt;/p&gt;

&lt;p&gt;All three, rocket, axum, and actix, cover the full range of functionality you would expect from a web framework, like routes, handlers, request and response parsing, middleware, state management, database interactions, logging, and testing. Furthermore, all of these frameworks perform well and will meet and surpass the needs of most web applications, so the choice really comes down to ergonomics and how it feels to develop within each framework.&lt;/p&gt;

&lt;p&gt;So let’s jump right in…&lt;/p&gt;

&lt;h2&gt;
  
  
  Rocket (&lt;a href="https://rocket.rs/" rel="noopener noreferrer"&gt;rocket.rs&lt;/a&gt;)
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&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%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/rwf2" rel="noopener noreferrer"&gt;
        rwf2
      &lt;/a&gt; / &lt;a href="https://github.com/rwf2/Rocket" rel="noopener noreferrer"&gt;
        Rocket
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A web framework for Rust.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Rocket&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/rwf2/Rocket/actions" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/rwf2/Rocket/workflows/CI/badge.svg" alt="Build Status"&gt;&lt;/a&gt;
&lt;a href="https://rocket.rs" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7d0b5bf90043737d85dec76c33fcefdc3d8b6f9d16c2c661c34bdb49f6f79cf9/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7765622d726f636b65742e72732d7265642e7376673f7374796c653d666c6174266c6162656c3d687474707326636f6c6f72423d643333383437" alt="Rocket Homepage"&gt;&lt;/a&gt;
&lt;a href="https://crates.io/crates/rocket" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/acb274b13536de976ca8dcc0000e83d44bed4020529c7013bfbbc90636e5e7df/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f762f726f636b65742e737667" alt="Current Crates.io Version"&gt;&lt;/a&gt;
&lt;a href="https://chat.mozilla.org/#/room/#rocket:mozilla.org" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2616d5193f1cd46c72cec148694e33d2bf8628dd926d1591f6b9730778adcd79/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374796c652d253233726f636b65743a6d6f7a696c6c612e6f72672d626c75652e7376673f7374796c653d666c6174266c6162656c3d2535426d253544" alt="Matrix: #rocket:mozilla.org"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Rocket is an async web framework for Rust with a focus on usability, security
extensibility, and speed.&lt;/p&gt;
&lt;div class="highlight highlight-source-rust notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;#&lt;span class="pl-kos"&gt;[&lt;/span&gt;macro_use&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;extern&lt;/span&gt; &lt;span class="pl-k"&gt;crate&lt;/span&gt; rocket&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c1"&gt;#&lt;span class="pl-kos"&gt;[&lt;/span&gt;get&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"/&amp;lt;name&amp;gt;/&amp;lt;age&amp;gt;"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;fn&lt;/span&gt; &lt;span class="pl-en"&gt;hello&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;name&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&lt;/span&gt;&lt;span class="pl-smi"&gt;str&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;age&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-smi"&gt;u8&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="pl-smi"&gt;String&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-en"&gt;format&lt;/span&gt;&lt;span class="pl-en"&gt;!&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"Hello, {} year old named {}!"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; age&lt;span class="pl-kos"&gt;,&lt;/span&gt; name&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;

&lt;span class="pl-c1"&gt;#&lt;span class="pl-kos"&gt;[&lt;/span&gt;launch&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;fn&lt;/span&gt; &lt;span class="pl-en"&gt;rocket&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="pl-smi"&gt;_&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    rocket&lt;span class="pl-kos"&gt;::&lt;/span&gt;&lt;span class="pl-en"&gt;build&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;mount&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"/hello"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-en"&gt;routes&lt;/span&gt;&lt;span class="pl-en"&gt;!&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;hello&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Visiting &lt;code&gt;localhost:8000/hello/John/58&lt;/code&gt;, for example, will trigger the &lt;code&gt;hello&lt;/code&gt;
route resulting in the string &lt;code&gt;Hello, 58 year old named John!&lt;/code&gt; being sent to the
browser. If an &lt;code&gt;&amp;lt;age&amp;gt;&lt;/code&gt; string was passed in that can't be parsed as a &lt;code&gt;u8&lt;/code&gt;, the
route won't get called, resulting in a 404 error.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Documentation&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Rocket is extensively documented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rocket.rs/overview/" rel="nofollow noopener noreferrer"&gt;Overview&lt;/a&gt;: A brief…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/rwf2/Rocket" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Rocket is a web framework for Rust that comes with the most batteries included out of the three we looked at. It positions itself in the same league as Rails (Ruby) or Flask (Python) and aims to offer functionality for anything a web backend might need, including route handling, request and response parsing, database integrations, validation, and so on. One of its main goals is to minimize the amount of boiler plate code you will have to write, and it does so through metaprogramming, i.e. heavy use of Rust macros.&lt;/p&gt;

&lt;p&gt;This makes it easy for the developer to integrate her actual logic with the rocket framework. Let’s have a quick look on how you might define a route handler including parameter guards and validation, a database connection pool, and a JSON object.&lt;/p&gt;

&lt;p&gt;We can define a route handler simply by adding the appropriate macro to a function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[get(&lt;/span&gt;&lt;span class="s"&gt;"/texts/&amp;lt;uuid&amp;gt;"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextsDatabase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&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 the request parameter cannot be parsed, the appropriate response code is sent instead of calling the function, so we have the usual comfort of type safety. Request bodies are handled very similarly, and of course, parsing integrates seamlessly with &lt;a href="https://serde.rs/" rel="noopener noreferrer"&gt;serde&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'m&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'m&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[post(&lt;/span&gt;&lt;span class="s"&gt;"/texts"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;format&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;data&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;msg&amp;gt;"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;post_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextsDatabase&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Adding a database is as simple as annotating the appropriate struct and initializing it at startup. Of course many popular DBMS's are supported.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Database)]&lt;/span&gt;
&lt;span class="nd"&gt;#[database(&lt;/span&gt;&lt;span class="s"&gt;"texts"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;TextsDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;mongodb&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Rocket goes heavy on the nerdy space vocabulary - which adds to its charm. Thus, to get your web server off the ground, you simply launch it. Notice again, how annotations are used to get the rocket going.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[launch]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;rocket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;rocket&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;TextsDatabase&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;routes!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post_text&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;We have found rocket very enjoyable to work with, especially if you want to focus on the business logic of your server and feel safe trusting the framework to take handling of the boilerplate code off your hands. If you are rather new to Rust and maybe switching over from other frameworks that make heavy use of annotations, you will feel at home with Rocket.&lt;/p&gt;
&lt;h2&gt;
  
  
  Axum
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&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%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/tokio-rs" rel="noopener noreferrer"&gt;
        tokio-rs
      &lt;/a&gt; / &lt;a href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;
        axum
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Ergonomic and modular web framework built with Tokio, Tower, and Hyper
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;axum&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;code&gt;axum&lt;/code&gt; is a web application framework that focuses on ergonomics and modularity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tokio-rs/axum/actions/workflows/CI.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/tokio-rs/axum/actions/workflows/CI.yml/badge.svg?branch=main" alt="Build status"&gt;&lt;/a&gt;
&lt;a href="https://crates.io/crates/axum" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0636f88c6507dba7ce401faa57637af8a9237dbdcad132c323579743d4091ab9/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f762f6178756d" alt="Crates.io"&gt;&lt;/a&gt;
&lt;a href="https://docs.rs/axum" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/194681819e1e75555dd9124488f6f2362c8a691049c849aa6d73b2de0a4ff0d3/68747470733a2f2f646f63732e72732f6178756d2f62616467652e737667" alt="Documentation"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;More information about this crate can be found in the &lt;a href="https://docs.rs/axum" rel="nofollow noopener noreferrer"&gt;crate documentation&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;High level features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Route requests to handlers with a macro free API.&lt;/li&gt;
&lt;li&gt;Declaratively parse requests using extractors.&lt;/li&gt;
&lt;li&gt;Simple and predictable error handling model.&lt;/li&gt;
&lt;li&gt;Generate responses with minimal boilerplate.&lt;/li&gt;
&lt;li&gt;Take full advantage of the &lt;a href="https://crates.io/crates/tower" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;tower&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://crates.io/crates/tower-http" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;tower-http&lt;/code&gt;&lt;/a&gt; ecosystem of
middleware, services, and utilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In particular the last point is what sets &lt;code&gt;axum&lt;/code&gt; apart from other frameworks
&lt;code&gt;axum&lt;/code&gt; doesn't have its own middleware system but instead uses
&lt;a href="https://docs.rs/tower/latest/tower/trait.Service.html" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;tower::Service&lt;/code&gt;&lt;/a&gt;. This means &lt;code&gt;axum&lt;/code&gt; gets timeouts, tracing, compression,
authorization, and more, for free. It also enables you to share middleware with
applications written using &lt;a href="https://crates.io/crates/hyper" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;hyper&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://crates.io/crates/tonic" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;tonic&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage example&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-rust notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;use&lt;/span&gt; axum&lt;span class="pl-kos"&gt;::&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    routing&lt;span class="pl-kos"&gt;::&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;get&lt;span class="pl-kos"&gt;,&lt;/span&gt; post&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    http&lt;span class="pl-kos"&gt;::&lt;/span&gt;&lt;span class="pl-v"&gt;StatusCode&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-v"&gt;Json&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-v"&gt;Router&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;use&lt;/span&gt; serde&lt;span class="pl-kos"&gt;::&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-v"&gt;Deserialize&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you are looking for a framework that is more bare-metal Rust and comes with less batteries included, you may want to check out the axum framework developed by tokio-rs, the same team that is responsible for the tokio async library. Opting for axum means you will have to take care of some more of the boilerplate code but you are also not fighting a potentially opinionated framework to get your logic just the way you want it. Compared to rocket, axum does not depend heavily on macros or annotations, and it sets itself apart from its peers by not implementing its own middleware but rather relying on tower for this. Through the tower ecosystem axum can offer timeouts, tracing, compression, authorization, and much more, while also enabling you to share your middleware with applications written with other web libraries like hyper.&lt;/p&gt;

&lt;p&gt;Looking at axum’s ergonomics, previous Go developers will feel right at home. The syntax looks very similar to a web server written e.g. with the gorilla/mux module in Go. So how would you go about the implementation of your first route handler including connecting to a database and starting the server? Let’s have a look.&lt;/p&gt;

&lt;p&gt;The same two functions from above look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;post_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;state&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MongoAppState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;payloads&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TextPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;payloads&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InsertedResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;payloads&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;state&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MongoAppState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;payloads&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TextPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;payloads&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No macros, but a lot more verbose. We see the same when we look at the startup code, we have to do a lot more manually, e.g., pass the DB connection along.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;mongodb&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_uri_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mongodb://..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shared_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;state&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;MongoAppState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// build our application with a single route&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/texts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/texts/:text_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delete_text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.with_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared_state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// run our app with hyper, listening globally on port 3000&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;net&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;TcpListener&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.0.0.0:3000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nn"&gt;axum&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&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;Axum is a very powerful web framework that benefits from the expertise of the tokio-rs team in creating great Rust tooling. It is being very actively developed and relies on other state-of-the-art crates for middleware. You will most likely enjoy axum the most if you are already familiar with Rust, enjoy having control over a lot of detail in implementation, or maybe if you are switching from another low-level web language like Go.&lt;/p&gt;
&lt;h2&gt;
  
  
  Actix (&lt;a href="https://actix.rs/" rel="noopener noreferrer"&gt;actix.rs&lt;/a&gt;)
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&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%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/actix" rel="noopener noreferrer"&gt;
        actix
      &lt;/a&gt; / &lt;a href="https://github.com/actix/actix-web" rel="noopener noreferrer"&gt;
        actix-web
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
  &lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Actix Web&lt;/h1&gt;
&lt;/div&gt;


&lt;p&gt;&lt;br&gt;
    &lt;strong&gt;Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust&lt;/strong&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;br&gt;
  

&lt;p&gt;&lt;a href="https://crates.io/crates/actix-web" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/9d0f6286b07978ec075fbf14cec9a77aef18d902038dfad49a1c144f3144a48b/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f762f61637469782d7765623f6c6162656c3d6c6174657374" alt="crates.io"&gt;&lt;/a&gt;
&lt;a href="https://docs.rs/actix-web/4.10.2" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/092e02d184fb4dcc590316d01c8d42e08673d216c6c912f010ba0aa40d026523/68747470733a2f2f646f63732e72732f61637469782d7765622f62616467652e7376673f76657273696f6e3d342e31302e32" alt="Documentation"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/7016575f331e1c1ea989b89d469d6d0ad702336ff951ad732350ce3ea51ba8c2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f72757374632d312e37322b2d6162363030302e737667"&gt;&lt;img src="https://camo.githubusercontent.com/7016575f331e1c1ea989b89d469d6d0ad702336ff951ad732350ce3ea51ba8c2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f72757374632d312e37322b2d6162363030302e737667" alt="MSRV"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/05b9b2ec090e401a08316dd7dccf4899f66da4053fcdfab7dae79cd6498cbdca/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f6c2f61637469782d7765622e737667"&gt;&lt;img src="https://camo.githubusercontent.com/05b9b2ec090e401a08316dd7dccf4899f66da4053fcdfab7dae79cd6498cbdca/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f6c2f61637469782d7765622e737667" alt="MIT or Apache 2.0 licensed"&gt;&lt;/a&gt;
&lt;a href="https://deps.rs/crate/actix-web/4.10.2" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b23691383d2d696d127d8b907ed42ccdc56112aa9d1a540688628585db6cd008/68747470733a2f2f646570732e72732f63726174652f61637469782d7765622f342e31302e322f7374617475732e737667" alt="Dependency Status"&gt;&lt;/a&gt;
&lt;br&gt;
&lt;a href="https://github.com/actix/actix-web/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/actix/actix-web" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/2569e52cdb414899ac7f7603a71440db28ace74ddb5921963163711939e6a874/68747470733a2f2f636f6465636f762e696f2f67682f61637469782f61637469782d7765622f67726170682f62616467652e7376673f746f6b656e3d6453774f6e7039514376" alt="codecov"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1217bf1d5bf2fb8a566022771a18bc71619911a7404fda8cb95202393e138df4/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f642f61637469782d7765622e737667"&gt;&lt;img src="https://camo.githubusercontent.com/1217bf1d5bf2fb8a566022771a18bc71619911a7404fda8cb95202393e138df4/68747470733a2f2f696d672e736869656c64732e696f2f6372617465732f642f61637469782d7765622e737667" alt="downloads"&gt;&lt;/a&gt;
&lt;a href="https://discord.gg/NWpN5mmg3x" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdd6540432dffa2f99734a2d9279ef1ae31e81c93213039de8e1c2db082a5dfb/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f3737313434343936313338333135333639353f6c6162656c3d63686174266c6f676f3d646973636f7264" alt="Chat on Discord"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Supports &lt;em&gt;HTTP/1.x&lt;/em&gt; and &lt;em&gt;HTTP/2&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Streaming and pipelining&lt;/li&gt;
&lt;li&gt;Powerful &lt;a href="https://actix.rs/docs/url-dispatch/" rel="nofollow noopener noreferrer"&gt;request routing&lt;/a&gt; with optional macros&lt;/li&gt;
&lt;li&gt;Full &lt;a href="https://tokio.rs" rel="nofollow noopener noreferrer"&gt;Tokio&lt;/a&gt; compatibility&lt;/li&gt;
&lt;li&gt;Keep-alive and slow requests handling&lt;/li&gt;
&lt;li&gt;Client/server &lt;a href="https://actix.rs/docs/websockets/" rel="nofollow noopener noreferrer"&gt;WebSockets&lt;/a&gt; support&lt;/li&gt;
&lt;li&gt;Transparent content compression/decompression (br, gzip, deflate, zstd)&lt;/li&gt;
&lt;li&gt;Multipart streams&lt;/li&gt;
&lt;li&gt;Static assets&lt;/li&gt;
&lt;li&gt;SSL support using OpenSSL or Rustls&lt;/li&gt;
&lt;li&gt;Middlewares (&lt;a href="https://actix.rs/docs/middleware/" rel="nofollow noopener noreferrer"&gt;Logger, Session, CORS, etc&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Integrates with the &lt;a href="https://docs.rs/awc/" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;awc&lt;/code&gt; HTTP client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Runs on stable Rust 1.72+&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Documentation&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://actix.rs" rel="nofollow noopener noreferrer"&gt;Website &amp;amp; User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/actix/examples" rel="noopener noreferrer"&gt;Examples Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.rs/actix-web" rel="nofollow noopener noreferrer"&gt;API Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://actix.rs/actix-web/actix_web" rel="nofollow noopener noreferrer"&gt;API Documentation (master branch)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Example&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Dependencies:&lt;/p&gt;
&lt;div class="highlight highlight-source-toml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;[&lt;span class="pl-en"&gt;dependencies&lt;/span&gt;]
&lt;span class="pl-smi"&gt;actix-web&lt;/span&gt; = &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;4&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Code:&lt;/p&gt;
&lt;div class="highlight highlight-source-rust notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;use&lt;/span&gt; actix_web&lt;span class="pl-kos"&gt;::&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;get&lt;span class="pl-kos"&gt;,&lt;/span&gt; web&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-v"&gt;App&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-v"&gt;HttpServer&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-v"&gt;Responder&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-c1"&gt;#&lt;span class="pl-kos"&gt;[&lt;/span&gt;get&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"/hello/{name}"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;fn&lt;/span&gt; &lt;span class="pl-en"&gt;greet&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;name&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; web&lt;span class="pl-kos"&gt;::&lt;/span&gt;&lt;span class="pl-smi"&gt;Path&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-smi"&gt;String&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="pl-k"&gt;impl&lt;/span&gt; &lt;span class="pl-smi"&gt;Responder&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-en"&gt;format&lt;/span&gt;&lt;span class="pl-en"&gt;!&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"Hello {name}!"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;

&lt;span class="pl-c1"&gt;#&lt;span class="pl-kos"&gt;[&lt;/span&gt;actix_web&lt;span class="pl-kos"&gt;::&lt;/span&gt;main&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/actix/actix-web" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Last but not least we want to take a look at actix-web. It combines aspects from both previous frameworks, rocket as well as axum, which is resembled in its ergonomics. Annotations are back on the menu and macros are more heavily used, especially in the definition of route handlers. When implementing middleware or database connections, however, you will not find the same level of abstraction as you might with rocket. Actix is extremely fast &lt;a href="https://www.techempower.com/benchmarks/#section=data-r22&amp;amp;test=composite&amp;amp;hw=ph" rel="noopener noreferrer"&gt;3&lt;/a&gt; and it aims to cater to both experienced Rust developers as well as newcomers who are just starting with Rust development. It ships with its own middleware, e.g. for logging, session management, or cross-origin resource sharing, and allows you to expand the framework with your own middleware that can hook into actix. Let’s have a look at how route handlers, database connections, and starting the server are handled in actix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[post(&lt;/span&gt;&lt;span class="s"&gt;"/texts"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;post_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;web&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;web&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Responder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[get(&lt;/span&gt;&lt;span class="s"&gt;"/texts/{uuid}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;web&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;web&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Responder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Again, we have a lot less boilerplate. Though launching the server falls in between the first two examples with regards to verbosity.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[actix_web::main]&lt;/span&gt; &lt;span class="c1"&gt;// or #[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;db_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_uri_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mongodb://...."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to connect"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nn"&gt;HttpServer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.app_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;web&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_client&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
            &lt;span class="nf"&gt;.service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;.bind&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;.await&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The benchmarks speak for themselves, actix is indeed blazingly fast! And that does not come at the cost of developer experience, which actix manages to keep to a very high standard. It ships with more batteries included than axum, but does not abstract everything quite as much as rocket. If you already feel comfortable writing programs in Rust and other low-level web programming languages, you will enjoy developing in actix, and your user will enjoy the performance!&lt;/p&gt;
&lt;h3&gt;
  
  
  Acknolegements
&lt;/h3&gt;

&lt;p&gt;Selim wrote the initial draft of this post and Selim, Zak, and I each implemented the sample API using one of the above frameworks.&lt;/p&gt;

&lt;p&gt;Cover image: &lt;a href="https://www.pexels.com/photo/brown-chains-114108/" rel="noopener noreferrer"&gt;"Brown Chains" by Miguel Á. Padriñán&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__2524490"&gt;
    &lt;a href="/sekael" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2524490%2F2a0fb332-56a6-4702-83cd-df476e44ef5a.png" alt="sekael image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/sekael"&gt;Selim Kaelin&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/sekael"&gt;I am a mountain athlete at heart, enjoy learning about and writing code, and passionate about Rust, Cloud, and maps!&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag__user ltag__user__id__2477319"&gt;
    &lt;a href="/zkck" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2477319%2Fe4c56b64-a970-4d41-86f6-9f6933fbc0c9.jpeg" alt="zkck image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/zkck"&gt;Zak Cook&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/zkck"&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag__user ltag__user__id__2732274"&gt;
    &lt;a href="/jankleine" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2732274%2Ff6769039-9e89-4990-b520-8f1f95714d22.jpg" alt="jankleine image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/jankleine"&gt;Jan Kleine&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/jankleine"&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>rust</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Two Years in the Vault: 4 Best Practices 🔒</title>
      <dc:creator>Zak Cook</dc:creator>
      <pubDate>Mon, 09 Dec 2024 08:30:00 +0000</pubDate>
      <link>https://dev.to/ipt/two-years-in-the-vault-4-best-practices-4acb</link>
      <guid>https://dev.to/ipt/two-years-in-the-vault-4-best-practices-4acb</guid>
      <description>&lt;p&gt;I work as an IT consultant. Over the past two years, we've been working with our client on a &lt;a href="https://en.wikipedia.org/wiki/Platform_engineering" rel="noopener noreferrer"&gt;platform engineering&lt;/a&gt; project, where the use of &lt;a href="https://www.vaultproject.io/" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt; for &lt;a href="https://www.redhat.com/en/topics/devops/what-is-secrets-management" rel="noopener noreferrer"&gt;secrets management&lt;/a&gt; was prevalent. Our Vault has enabled us to manage hundreds of credentials, increasing the security of our developer platform, and has resulted in a thorough and battle-tested configuration of our Vault which we can be proud of.&lt;/p&gt;

&lt;p&gt;However, over the past two years, there have been quite a few learning moments. Vault is a great product with lots of flexibility, but this opens up a lot of possibilities for misconfiguration or misuse. For us, that meant a few tedious reconfigurations of our Vault. This post aims to share learnings that I wish we had known from the very beginning, in the form into digestible tips.&lt;/p&gt;

&lt;p&gt;These tips are for everyone that needs to work with HashiCorp Vault, whether it be as a developer, or as an administrator (you may still learn something). If you don't use Vault, maybe simply bookmark this post, it may come in handy in the future.&lt;/p&gt;

&lt;p&gt;TL;DR:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure with an infrastructure-as-code (IaC) tool&lt;/li&gt;
&lt;li&gt;Policies describe permissions&lt;/li&gt;
&lt;li&gt;KVv2: save raw data, format elsewhere&lt;/li&gt;
&lt;li&gt;Beware of write permissions on &lt;code&gt;sys/policy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Consider templated policies (upcoming)&lt;/li&gt;
&lt;li&gt;KVv2: make secrets "connection bundles" (upcoming)&lt;/li&gt;
&lt;li&gt;Utilize &lt;code&gt;-output-policy&lt;/code&gt; from the CLI (upcoming)&lt;/li&gt;
&lt;li&gt;Think about your path structure (upcoming)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a two-part series, I'll cover the first four tips here, and the remaining four in an upcoming post. Stay tuned.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Glossary:&lt;/strong&gt; I overuse the words &lt;em&gt;component&lt;/em&gt; and &lt;em&gt;system&lt;/em&gt; a lot. This could refer to microservices, short running jobs, monolith servers, which all run together, to form some sort of workload. If you're running on Kubernetes, think of a &lt;em&gt;component&lt;/em&gt; as a Deployment, and a &lt;em&gt;system&lt;/em&gt; as the Kubernetes cluster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tip 1: Configure with an infrastructure-as-code (IaC) tool
&lt;/h2&gt;

&lt;p&gt;When first starting to integrate a Vault into a large system, one naturally does explorative work locally with the Vault CLI. This is all good, but concretizing that configuration into infrastructure-as-code (IaC) is essential, and by doing it early you'd avoid the pain of migrating your configuration down the road.&lt;/p&gt;

&lt;p&gt;Potential solutions for IaC are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt;/&lt;a href="https://opentofu.org/" rel="noopener noreferrer"&gt;OpenTofu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/" rel="noopener noreferrer"&gt;Pulumi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For configuring your Vault, I would generally recommend going for Terraform/OpenTofu, which takes the declarative approach as opposed to Pulumi. Pulumi is a good solution when you have lots of dependencies in a complex system, which need to be handled programmatically.&lt;/p&gt;

&lt;p&gt;We started off on our Vault journey by simply documenting how we configured our secrets engines, auth methods and policies. So when we had to set up a new Vault, that meant going through that documentation and running the commands one-by-one. We at some point (painfully) changed to Terraform/OpenTofu, and it directly opened up many doors for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing a testing framework, where we would test the access to Vault paths by simulating the components in our system, allowing us to check for any regressions&lt;/li&gt;
&lt;li&gt;Reusing the IaC to configure Vaults in multiple environments&lt;/li&gt;
&lt;li&gt;Integrate with CI pipelines to automate updates to our Vaults&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's many more advantages. Point is, start with IaC directly, so you don't have to go through the same process as we did, of integrating an already configured Vault with IaC. In case you have to: &lt;a href="https://developer.hashicorp.com/terraform/language/import" rel="noopener noreferrer"&gt;Terraform Import Blocks&lt;/a&gt; saved our lives :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Tip 2: Policies describe permissions
&lt;/h2&gt;

&lt;p&gt;Policies are core to HashiCorp Vault, being one of the pillars in its access control mechanisms. A Vault policy is a set of permissions, each being a combination of a path and capabilities on that path. Efficient management of policies is important in order to keep your Vault lean and scalable.&lt;/p&gt;

&lt;p&gt;Let's consider a very basic Vault, which has a &lt;a href="https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2" rel="noopener noreferrer"&gt;KVv2 engine&lt;/a&gt; under &lt;code&gt;secrets/&lt;/code&gt;, used to store secret recipes under the &lt;code&gt;recipes/&lt;/code&gt; subpath. The following policy could be the "head chef" policy, allowing the head chef to browse the entire secrets engine and update any of the secret recipes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write head-chef - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
# browse secrets
path "secrets/metadata/*" {
  capabilities = ["read", "list"]
}
# manage recipes
path "secrets/data/recipes/*" {
  capabilities = ["create", "update", "read", "delete"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We would then attach this policy to a role with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vault write auth/approle/role/head-chef token_policies=head-chef
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we decided that the name of the policy would match the role, namely &lt;code&gt;head-chef&lt;/code&gt;. This works, but we have found that as policies become larger and more roles are added to the Vault, &lt;strong&gt;breaking down policies into smaller, more meaningful sets of permissions&lt;/strong&gt; becomes increasingly important. &lt;strong&gt;Policy names should also describe the permissions encapsulated&lt;/strong&gt;, and should not refer back to the role that uses it.&lt;/p&gt;

&lt;p&gt;To help breaking down policies, it is useful to decide on a &lt;strong&gt;naming standard&lt;/strong&gt; for your policies. An example would be &lt;code&gt;&amp;lt;verb&amp;gt;-&amp;lt;subject of policy&amp;gt;&lt;/code&gt;, like &lt;code&gt;read-nuclear-codes&lt;/code&gt;. In this example we would have to break down the policy a bit more in order for our convention to apply. Let's split the &lt;code&gt;head-chef&lt;/code&gt; policy into two policies, &lt;code&gt;browse-secrets&lt;/code&gt; and &lt;code&gt;manage-recipes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write browse-secrets - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
# browse secrets
path "secrets/metadata/*" {
  capabilities = ["read", "list"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;vault policy write manage-recipes - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
# manage recipes
path "secrets/data/recipes/*" {
  capabilities = ["create", "update", "read", "delete"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;vault write auth/approle/role/head-chef &lt;span class="nv"&gt;token_policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;browse-secrets,manage-recipes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is now much more transparent what permissions are associated with this role. In addition, breaking down the policy made parts of the policy reusable, as we could now do something like the following, reusing the &lt;code&gt;browse-secrets&lt;/code&gt; policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write manage-greek-recipes - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
# manage greek recipes
path "secrets/data/recipes/greek/*" {
  capabilities = ["create", "update", "read", "delete"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;vault write auth/approle/role/chef-george &lt;span class="nv"&gt;token_policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;browse-secrets,manage-greek-recipes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deciding on this naming convention for our policies allowed us to scale to many more roles in our Vault. One thing to consider though, is that paths may end up overlapping among the policies assigned to a role. This requires being aware of how Vault's &lt;a href="https://developer.hashicorp.com/vault/docs/concepts/policies#priority-matching" rel="noopener noreferrer"&gt;priority matching&lt;/a&gt; works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tip 3: KVv2: save raw data, format elsewhere
&lt;/h2&gt;

&lt;p&gt;Vault's KVv2 engine allows for saving static secrets in the form of key-value pairs. Choosing the appropriate key-value pairs to save in a secret will both maximize flexibility in how your infrastructure consumes these secrets and would minimize duplicated data.&lt;/p&gt;

&lt;p&gt;Let's say you have a component which requires credentials to a database, and needs them provided inside a config, e.g. in TOML format. Since the config holds secrets, it may be tempting to save the entire config in Vault's KVv2 engine, and fetch that config when starting your component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv put secrets/components/my-component config.toml&lt;span class="o"&gt;=&lt;/span&gt;- &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
[database]
host = "database.example.com"
username = "postgres"
password = "1234"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this approach is limiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reusing the database credentials means copy-pasting to another config, and doesn't allow for a source of truth&lt;/li&gt;
&lt;li&gt;Any non-secret config changes need to go over the Vault&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can be avoided: the Vault ecosystem has a large amount of helper components which support templating, among which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent/template" rel="noopener noreferrer"&gt;Vault agent templating&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://external-secrets.io/latest/guides/templating/" rel="noopener noreferrer"&gt;External Secrets Operator templating&lt;/a&gt; in case you're running on Kubernetes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As an example let's make use of an &lt;code&gt;ExternalSecret&lt;/code&gt; for our templating. First, let's save the &lt;strong&gt;standalone credentials&lt;/strong&gt; instead of our config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv put secrets/databases/my-database &lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we'll write an &lt;code&gt;ExternalSecret&lt;/code&gt; making use of this raw data:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExternalSecret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ... (reference to SecretStore)&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;engineVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;config.toml&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;[database]&lt;/span&gt;
          &lt;span class="s"&gt;host = "database.example.com"&lt;/span&gt;
          &lt;span class="s"&gt;username = {{ .username | quote }}&lt;/span&gt;
          &lt;span class="s"&gt;password = {{ .password | quote }}&lt;/span&gt;
  &lt;span class="na"&gt;dataFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/secrets/databases/my-database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would then render a &lt;code&gt;Secret&lt;/code&gt; resource with the following content under the &lt;code&gt;config.toml&lt;/code&gt; key, which can be mounted in our component:&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;[database]&lt;/span&gt;
&lt;span class="py"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"database.example.com"&lt;/span&gt;
&lt;span class="py"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"postgres"&lt;/span&gt;
&lt;span class="py"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1234"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure of our KVv2 engine is now cleaner, as it is not aware of the components that access it, and holds sensible reusable secrets. Another option is to tweak how the component is configured. Instead of taking credentials directly, our component could take a Vault path instead:&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;[database]&lt;/span&gt;
&lt;span class="py"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"database.example.com"&lt;/span&gt;
&lt;span class="py"&gt;vault_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"secrets/databases/my-database"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This however means a dependency on HashiCorp Vault in the code of the application itself, as the code now uses the Vault API in order to retrieve the secret. Some applications may want to be agnostic to the type of software storing the credentials, in which case a templating solution would be more suitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tip 4: Beware of write permissions on &lt;code&gt;sys/policy&lt;/code&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Understanding this tip is a bit more involved, but please bear with me. It is arguably the most important tip in this series, especially in environments where security is critical.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vaults used in a complex system sometimes need to support new users being asynchronously added to the Vault: for example adding a new role/user which has access to a subdirectory of a KVv2 secrets engine. To automate this, companies may resort to building some management component which automates the creation of a role/user and policies attached to it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.hashicorp.com/vault/api-docs/system/policy#create-update-policy" rel="noopener noreferrer"&gt;&lt;code&gt;sys/policy&lt;/code&gt;&lt;/a&gt; (or &lt;a href="https://developer.hashicorp.com/vault/api-docs/system/policies#create-update-acl-policy" rel="noopener noreferrer"&gt;&lt;code&gt;sys/policies/acl&lt;/code&gt;&lt;/a&gt;) path is used to grant permissions on managing policies in Vault, and is necessary to create policies on the fly. We will see in this chapter how granting write permissions on &lt;code&gt;sys/policy&lt;/code&gt; &lt;strong&gt;can lead to severe security risks&lt;/strong&gt;, and provide some examples on how to mitigate these risks.&lt;/p&gt;

&lt;p&gt;As an example, let's consider our Vault which is meant to manage recipes. Our company wants to support employees requesting their own subdirectory in our &lt;code&gt;secrets/&lt;/code&gt; KVv2 engine. This would involve the employees sending an API request to a component, let's call it the &lt;code&gt;employee-manager&lt;/code&gt;, that would need to execute following commands on the Vault upon receiving one of these requests:&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;# create policy for employee&lt;/span&gt;
vault policy write &lt;span class="s2"&gt;"manage-recipes-&lt;/span&gt;&lt;span class="nv"&gt;$EMPLOYEE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
path "secrets/data/recipes/&lt;/span&gt;&lt;span class="nv"&gt;$EMPLOYEE_NAME&lt;/span&gt;&lt;span class="sh"&gt;/*" {
  capabilities = ["create", "read", "update", "delete"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="c"&gt;# create role for employee with attached policy&lt;/span&gt;
vault write auth/approle/role/&lt;span class="nv"&gt;$EMPLOYEE_NAME&lt;/span&gt; &lt;span class="nv"&gt;token_policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"manage-recipes-&lt;/span&gt;&lt;span class="nv"&gt;$EMPLOYEE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would require the &lt;code&gt;employee-manager&lt;/code&gt; component to have the necessary permissions to create both roles and policies for our employees. Let's setup a role and policy to allow the component to execute the above commands:&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;# create "create-policies", "create-roles" policies&lt;/span&gt;
vault policy write &lt;span class="s2"&gt;"create-policies"&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
path "sys/policy/+" {
  capabilities = ["create", "update"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;vault policy write &lt;span class="s2"&gt;"create-roles"&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
path "auth/approle/role/+" {
  capabilities = ["create", "update"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="c"&gt;# attach policies to role&lt;/span&gt;
vault write auth/approle/role/employee-manager &lt;span class="nv"&gt;token_policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;create-policies,create-roles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above permissions are &lt;strong&gt;extremely dangerous&lt;/strong&gt; to provide to any component or user of Vault, especially in conjunction.&lt;/p&gt;

&lt;p&gt;Imagine our &lt;code&gt;employee-manager&lt;/code&gt; component was insecure, and is compromised by an attacker. The attacker then uses the component's credentials to log in to Vault. They can now &lt;strong&gt;create new policies, with any permissions, and assign those permissions to the &lt;code&gt;employee-manager&lt;/code&gt; which they have control over&lt;/strong&gt;:&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;# create malicious policy&lt;/span&gt;
vault policy write &lt;span class="s2"&gt;"read-secrets"&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
path "secrets/data/*" {
  capabilities = ["read"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="c"&gt;# update the compromised role with malicious policy&lt;/span&gt;
vault write auth/approle/role/employee-manager &lt;span class="nv"&gt;token_policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;create-policies,create-roles,read-secrets

&lt;span class="c"&gt;# after logging in again, the attacker could execute:&lt;/span&gt;
vault &lt;span class="nb"&gt;read &lt;/span&gt;secrets/data/recipes/gordon/super-secret-meatballs-recipe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the component must technically be considered as &lt;strong&gt;admin&lt;/strong&gt; on the Vault, as it can create policies for absolutely anything and assign it to itself. To mitigate this security risk, there are the following solutions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Avoid granting permissions on &lt;code&gt;sys/policy&lt;/code&gt; altogether and consider &lt;strong&gt;using &lt;a href="https://developer.hashicorp.com/vault/docs/concepts/policies#templated-policies" rel="noopener noreferrer"&gt;templated policies&lt;/a&gt;&lt;/strong&gt;. This is often the cleanest solution, and has a big advantage of reducing the amount of configuration.&lt;/li&gt;
&lt;li&gt;Grant permissions on a &lt;strong&gt;subfolder of &lt;code&gt;sys/policy&lt;/code&gt;&lt;/strong&gt;. This subfolder must be disjoint from the policies granted to &lt;code&gt;employee-manager&lt;/code&gt;, and &lt;code&gt;employee-manager&lt;/code&gt; should not be able to &lt;code&gt;update&lt;/code&gt; its own role.&lt;/li&gt;
&lt;li&gt;Grant only &lt;code&gt;create&lt;/code&gt; permissions on anything under &lt;code&gt;sys/policy&lt;/code&gt;, and &lt;code&gt;employee-manager&lt;/code&gt; should not be able to &lt;code&gt;update&lt;/code&gt; its own role.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There may be more mitigation options, but the point of this chapter is to consider the event of a breach and be aware of the &lt;strong&gt;effective permissions&lt;/strong&gt; that you may be giving to your system components.&lt;/p&gt;

&lt;p&gt;That's all the tips for now. Thanks for reading, I hope you learned something, and stay tuned for the second half and other posts to come.&lt;/p&gt;

</description>
      <category>hashicorp</category>
      <category>vault</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
