<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Developer Experience - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Developer Experience - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/blog/category/developer-experience</link>
    </image>
    <link>https://www.elastic.co/search-labs/blog/category/developer-experience</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/category/developer-experience.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sat, 24 Oct 2026 11:59:35 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Bringing Elastic into Claude: The ‘why,’ the ‘how,’ and the ‘gotchas’]]></title>
    <description><![CDATA[Point your Claude plugin at main and every merge ships as a release, and that’s the least surprising thing that happened between submitting and going live.]]></description>
    <content:encoded><![CDATA[<p>Previously, if you were inside Claude looking for Elastic, you wouldn't find us: </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7ca6e3c9ef50e62c/6ad8735dfad7344787d08b16/image14.png" alt="Claude Marketplace search for Elastic returning no plugins, before publishing a Claude plugin to the directory" /><p>We wanted to fix that! This blog is the story of publishing a plugin that puts Elastic's capabilities where developers are already working. We already had the <a href="https://github.com/elastic/agent-skills">elastic/agent-skills</a> repository, and we’ve both <a href="https://www.elastic.co/search-labs/blog/agent-skills-elastic">blogged about</a> and <a href="https://www.elastic.co/docs/explore-analyze/ai-features/agent-skills">documented</a> them, but an official Claude plugin makes it that much easier for our customers to use those skills.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt17cd078ff68f11c1/6ad8737425ebe702b0f0e501/image5.png" alt="Elastic plugin listed in the Claude app’s plugins settings, found by searching the Anthropic Directory for elastic" /><p>Getting here was a journey. Along the way, we learned a lot, and wanted to share our experience. </p><p>It took us three weeks from first commit to live. Claude docs got us 80% of the way, the other 20% was undocumented review criteria and a versioning model that cuts a new release on every commit. There was limited visibility on when, or where, the plugin would appear.</p><p>Here’s what we learned:</p><h2>Agent Skills versus a Claude plugin: Why we needed both</h2><p>As we worked our way through all the reference materials, we realized that <a href="https://github.com/elastic/agent-skills">our Agent Skills</a> were only part of what a Claude plugin can include. Although <a href="https://agentskills.io/home">Agent Skills</a> are a generic open standard, adopted by major agent vendors and ecosystems, a Claude plugin can contain much more; however, some of it may not be as generically applicable across platforms. 
One <a href="https://code.claude.com/docs/en/plugins/manifest-reference#standard-layout">example from Claude Code Docs</a>:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2ab0b01edfd6771f/6ad8739123d2b4245821eb65/image7.png" alt="Claude plugin folder structure showing .claude-plugin/plugin.json plus skills, commands, agents and hooks" /><p>This contains quite a bit more than skills, and we want room to grow into those. </p><p>So we decided that we needed a new repo for our Claude plugin. That way, we’d have a home where all Claude-specific features could live, separate from our generic Agent Skills. Thus was born the <a href="https://github.com/elastic/claude-plugin">elastic/claude-plugin</a> repository.</p><h2>Submitting a plugin to the Claude directory</h2><p>Claude has thorough documentation, and I won’t rehash it all here, but for reference, you may want to read:</p><ul><li><p><a href="https://claude.com/docs/directory/publish">Publish to the directory</a></p></li><li><p><a href="https://claude.com/docs/plugins/pre-submission-checklist">Plugin pre-submission checklist</a></p></li><li><p><a href="https://claude.com/docs/plugins/quickstart">Build your first plugin</a></p></li></ul><p>We went through the above and made sure that we addressed everything on the checklist. But when I tried to submit the plugin at <a href="https://claude.ai/directory/manage">https://claude.ai/directory/manage</a>, I got the following message:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt13aa2c5843238c35/6ad873bffcdfd12cff05ef42/image6.png" alt="Claude error: you don’t have access to organization settings, blocking a Claude plugin directory submission" /><p>I’m an engineer. An individual contributor. I have no business being an “owner” of an account for a software tool used by most Elastic employees. But the IT team that owns it wouldn’t be maintaining and supporting this external-facing plugin. </p><p>This was our first bit of friction, but it also led to our first lesson, namely, <em>if you have a contact at Anthropic, make use of them</em>.
Elastic had already established a partnership with Anthropic, so when we hit this issue, we simply messaged our point of contact, who quickly got back to us:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7edc81534fb0af61/6ad873e3301f93b701ac65c3/image1.png" alt="Anthropic Slack reply explaining a custom role with Directory management permission for plugin submission" /><p>Once we had the right permissions, it was relatively straightforward to fill out the submission form:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte010e9ed8fa7318c/6ad873fbe0c9dc31a2fd1d80/image13.png" alt="Claude plugin submission form fields: repository, plugin path and branch or tag the directory follows" /><p>Despite the simplicity, this is where we made our first two mistakes.</p><h3>Choosing your plugin path: Why <em>not</em> the repository root</h3><p>As the screenshot above shows, this is the folder where Anthropic will look for your <code>.claude-plugin/plugin.json</code>. It’s also the root of what Anthropic’s automated systems will be scanning. </p><p>We started with the default <code>repository root</code> for our plugin path, but this was a mistake, because the files for our own internal development purposes were also at the repository root. They included the <a href="http://claude.md"><code>CLAUDE.md</code></a> that our engineers could use to maintain the plugin, along with our continuous integration (CI) pipelines, tests, and <code>package-lock.json</code>.</p><p>Scanning those files can lead to unexpected holdups during verification:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta34520bdb92501b4/6ad8741cf490a756723b8e4a/image9.png" alt="Claude directory policy hold on package-lock.json, flagging dependencies that install from a lockfile" /><p>We should note that the Anthropic team didn’t let this be a blocker and, after manual review, waived these warnings.</p><h3>Claude plugin versioning: Why we stopped pointing at <code>main</code></h3><p>There’s no single “right” way to version or release software. But at Elastic, we often stick to the following conventions:</p><ul><li><p>Development is done on feature branches (sometimes in forks).</p></li><li><p>Pull requests (PRs) target <code>main</code>.</p></li><li><p>Versioning uses SemVer (<code>MAJOR.MINOR.PATCH</code>).</p></li><li><p>Releases are cut from release branches (<code>MAJOR.MINOR</code>) and tagged (<code>vMAJOR.MINOR.PATCH</code>).</p></li></ul><p>On my initial read of <a href="https://code.claude.com/docs/en/plugins/loading#versions-and-updates">Claude’s plugin versioning documentation</a>, I knew that we’d need a different system for this repo. We only have one live version at once, rather than maintaining multiple majors and minors. So we planned to just use <code>main</code> with a specific <code>version</code> in the plugin metadata. We’d merge PRs into <code>main</code>, as normal, and when ready to release, we’d bump that <code>version</code> and create a Git tag. Easy. </p><p>But that’s not how it works:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7c51f4000c540617/6ad8743a25ebe75079f0e505/image11.png" alt="Claude plugin versions list showing repeated v0.0.1 policy holds from scheduled checks on every commit" /><p>That initial approach actually led to many<code>v0.0.1</code> “versions” of the plugin, one for each merge commit. </p><p>With help from our Anthropic contacts (the astute reader will notice that this is a theme), we got the following clarification:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6dc3b0c4777263ef/6ad8745472826a774fab3e92/image12.png" alt="Anthropic explaining the version field doesn’t pick the commit, recommending a release branch over main" /><p>After realizing this, we changed our plugin to point at a dedicated <code>stable</code> branch. Now we can merge to <code>main</code> as normal but only merge <code>main</code> into <code>stable</code> when we’re ready to release a batch of changes and update our version number. This results in our flow looking like:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt717358e5112cd6b0/6ad874779e743522827455ed/image3.png" alt="Claude plugin branching model: feature branches merge to main, main merges to a stable branch tagged for release" /><h2>What happens after you submit a Claude plugin</h2><p>You wait.</p><p>When we first submitted, we saw only the following:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt70b7ae3f5b220d04/6ad8749884280d3e10538233/image8.png" alt="Claude plugins dashboard showing the Elastic plugin submitted and pending review ten minutes after sending" /><p>While we were in the process of writing this blog, Anthropic made some changes to its UI, providing a bit more visibility. Now the submissions UI looks more like this:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdb70c7fe85936a67/6ad874ad0aea04487e2a5dba/image2.png" alt="Updated Claude plugin dashboard with a Submitted, Security scan, Approved and Published review timeline" /><p>Even so, the review of our first plugin submission took about a week, and that may have been faster than normal as a result of our direct line of contact with Anthropic solution architects and account executives (to stress again how valuable that relationship has been). We were given a rough estimate of nine days for average review time.</p><p>We couldn’t see it then, but we later learned that part of the holdup was that our submission raised a number of warnings to the Anthropic review team:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1f52284c5ec0c647/6ad874c72299f5ae8ab95dfe/image10.png" alt="Claude plugin review log showing policy warnings for credentials, missing icon and download-and-run commands" /><h2>Policy warnings from the automated review</h2><p>In retrospect, the extra visibility is nice; but, at the time, I might have ended up making a bunch of unnecessary changes. The Anthropic team ultimately waived all of these warnings. The takeaway here is to wait for a call to action from the reviewer, rather than jumping straight into trying to resolve every policy warning, even if they sound serious.</p><p><strong>Warning</strong></p><p><strong>What triggered it</strong></p><p><strong>Resolution</strong></p><p>Uses a credential from the user’s machine</p><p>Example <code>curl</code> commands with credentials templated as environment variables.</p><p>Waived after manual review. False positive.</p><p>No icon</p><p>The <code>icon</code> field was undocumented at submission time, and <code>claude plugin validate --strict</code> passed without it.</p><p>Waived. The field is now documented.</p><p>Contains a download-and-run command</p><p>Skill directives instructing an agent to call an API and interpret the response.</p><p>Waived after manual review.</p><p></p><h3>Warning: Uses a credential from the user’s machine</h3><p>The credential warnings pointed at skills that included example <code>curl</code> commands, where the credentials were templated with environment variable syntax. The Elastic CLI was specially designed to ensure that credentials were never exposed to an agentic caller, so we were initially quite alarmed by this report and relieved to discover that these were false positives.</p><h3>Warning: No icon</h3><p>The icon warning surprised us, too. At the time, there was no documentation for an <code>icon</code> field being an option as part of the manifest, and our CI had run <code>claude plugin validate --strict .</code>, so we knew that the manifest was valid without that field.</p><p>Today, the <a href="https://code.claude.com/docs/en/plugins-reference#fields">Plugin manifest: Fields reference</a> <em>does</em> document <code>icon</code>. We take this as a sign that Anthropic is clearly iterating on the plugin experience. New fields show up, but Anthropic doesn’t retroactively block submissions made without them. </p><h3>Warning: Contains a download-and-run command</h3><p>The download-and-run warning also sounded concerning at first, but on closer inspection, it seems to be taking issue with our skills having directives like the following, where API calls are suggested, as is instruction on interpreting the response:</p><p><code>**Verify Kibana connectivity.** Call `GET kbn:/api/status`. If the call fails, stop and surface the error — do not guess endpoints or credentials. Read `version.number` to confirm the cluster meets the 9.4+ requirement.</code></p><h2>Surprise: Your Claude plugin is live</h2><p>When Anthropic concluded its review, the plugin was immediately visible. This didn’t require an extra confirmation step from us, and there was no staging area. We also received no notification that it was live. This truly surprised us. Our plugin was published late on a Thursday, but I didn’t check on it on Friday. Only on the following Monday did we realize that it was live and visible. In our case, no harm done (the skills had been public for months already), but for anyone who’s not shipping something battle-hardened, be sure to do your testing <em>before</em> submitting. </p><h3>Where your Claude plugin actually appears</h3><p>Publishing doesn’t get you into every Claude directory. There are at least four places where a developer might look for plugins:</p><p><strong>Directory</strong></p><p><strong>Visible on publish?</strong></p><p><strong>How to get in</strong></p><p>Claude app’s <strong>Setting</strong> &gt; <strong>Plugins</strong> &gt; <strong>Discover</strong> UI</p><p>Yes</p><p>Submit your plugin at <a href="https://claude.ai/directory/manage">https://claude.ai/directory/manage</a>.</p><p>Claude Marketplace (<a href="https://claude.com/plugins">https://claude.com/plugins</a>, <a href="https://claude.com/marketplace">https://claude.com/marketplace</a>)</p><p>No</p><p>Verification process done separately by Anthropic, after publishing.</p><p><a href="https://github.com/anthropics/claude-plugins-community/">anthropics/claude-plugins-community repo</a></p><p>No</p><p>Curation process done separately by Anthropic, after publishing.</p><p><a href="https://github.com/anthropics/claude-plugins-official/">anthropics/claude-plugins-official repo</a></p><p>No</p><p>Partner plugins can be listed through Anthropic contact.</p><p>When it’s initially published, your plugin only appears in the Anthropic directory in an unverified state, labeled as “Community.” And this is only if your license or account isn’t configured to exclude unverified plugins. This caused some initial confusion internally (our IT department has Claude Enterprise set up to hide “Community” plugins); we could only see our own plugin when we weren’t logged in with our work accounts.</p><p>To get into the other directories and marketplaces, the plugin needs to go through a separate “verification” or “marking” process. Once again, DMs with Anthropic partners proved very helpful:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt86642ee23b56c77b/6ad874f7c149b80e43c3c602/image15.png" alt="Anthropic Slack reply explaining claude.com/marketplace only lists Claude plugins marked verified" /><h2>What’s next for the Elastic Claude plugin</h2><p>We're still working on getting the plugin verified for the web marketplace and on getting it installed for Elastic's own Claude org so our engineers can dogfood it. The plugin is young, and there are a lot of features we’d like to add. </p><p>But if there's one thing I'd want you to take from this post, it's that the hardest parts of this process weren't technical. They were the gaps between what was expected and what actually happened, and every one of those gaps got closed by a conversation with a real person. The tools are getting smarter, but the communities around them still run on people helping people.</p><p>If you're working through publishing your own plugin and get stuck, reach out on our <a href="https://ela.st/slack">Elastic Stack Community Slack</a> or find <a href="https://www.linkedin.com/in/sestory/">me on LinkedIn</a>. And consider sharing what you’ve learned, too.
See you in the marketplace.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/publish-claude-plugin-directory</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/publish-claude-plugin-directory</guid>
    <category><![CDATA[AI Tools ]]></category>
    <category><![CDATA[Developer Experience]]></category>
    <category><![CDATA[Agentic AI]]></category>
    <dc:creator><![CDATA[Sean Story]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt49d535ad85c44096/6ad8732b176afc1af810c437/image4.png" length="0" type="image/png"/>
    <pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[One button, three places: How we rebuilt Kibana's page headers with stricter APIs]]></title>
    <description><![CDATA[We gave Kibana's shared shell typed contracts, which is how design system governance became the default, and why the new page headers have no breadcrumbs.]]></description>
    <content:encoded><![CDATA[<p>We replaced the open-ended React APIs behind every page header in Kibana with typed contracts, which put design system governance in the shell itself. Dozens of teams no longer have to get each header right by hand. Now a page declares what a control means, and the shared shell decides how it looks and where it goes. We removed breadcrumbs along the way, because the redesigned navigation already shows you where you are. The new chrome is live in Elastic Cloud Serverless today and ships to Elastic Cloud Hosted and self-managed in 9.6.</p><h2>What is the Kibana chrome?</h2><p>The chrome is everything that frames an application in <a href="https://www.elastic.co/kibana">Kibana</a>: the global header and the navigation, along with the header of the page that you’re currently on. Every application renders inside it. Because the chrome is shared, its APIs decide how consistent the whole product feels and how hard the next redesign will be.</p><h2>What inconsistent page headers looked like to users</h2><p>The clearest way to see the problem is to look at what users saw:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63a6acf0d30ae807/6aaa27addcc13f41b2527962/unnamed_(1).png" alt="Three Kibana pages with different page header layouts, showing inconsistent action button and link placement" /><p></p><p>Take something as basic as a primary action, the one button a page most wants you to click, like <strong>Create index</strong>. Before this work, primary actions lived in three different places, depending on the page:</p><ul><li><p>In the old app header.</p></li><li><p>In the page template header.</p></li><li><p>Somewhere in the page body itself.</p></li></ul><p>Common links had the same problem, with share, feedback, and documentation links appearing in separate spots with different styling from one app to the next. Breadcrumbs were misconfigured on many pages. For example, some showed a breadcrumb for each tab within a page, and some weren't clickable. Still others repeated the title of the page you were already on.</p><p>Every one of these was the output of a well-intentioned team using a flexible API in the way it’s usually used. Users had to relearn each page's layout, and every attempt to redesign the chrome had to account for hundreds of local variations.</p><h2>Goals for the page header redesign</h2><p>The goal of the redesign was simple; that is, pages should express what they need (for example, a title, badge, or primary action), and the shared shell should decide how it looks and where it goes. Common links and primary actions should get one home, everywhere. If it's the primary action, it's always in the same place, styled the same way. And consistency should stop being something that each of dozens of teams has to get right by hand, forever.</p><p>Getting there wasn't a restyling exercise; it required changing the contract between applications and the platform.</p><h2>How typed props replaced EuiPageHeader's open React nodes</h2><p>Each page header was easy to build in isolation, and the local choices piled up into visible differences across the product: inconsistent spacing, title styling, and action placement from one page to the next.</p><p>The fix, everywhere this showed up, was the same mechanism: stricter APIs. Instead of accepting arbitrary React nodes, a control declares what it means through typed props, and the shared shell decides how it looks and where it goes. That one change buys two things at once: consistency, and governance over what the shared surface is allowed to contain.</p><p>Take the page header. With <code>EuiPageHeader</code>, the API exposed layout settings and accepted React nodes for nearly every visible area:</p>&lt;EuiPageHeader
  pageTitle={
    &lt;&gt;
      Index Management
      &lt;EuiBadge color="accent"&gt;Beta&lt;/EuiBadge&gt;
    &lt;/&gt;
  }
  description={
    &lt;&gt;
      View and manage your Elasticsearch indices.{' '}
      &lt;EuiLink href={docsUrl}&gt;Learn more&lt;/EuiLink&gt;
    &lt;/&gt;
  }
  bottomBorder
  alignItems="top"
  responsive={false}
  tabs={tabs}
  rightSideItems={[
    &lt;RefreshButton onClick={onRefresh} /&gt;,
    &lt;CreateButton onClick={onCreate} /&gt;,
  ]}
/&gt;<p>Because <code>pageTitle</code>, <code>description</code>, and <code>rightSideItems</code> all accept arbitrary React nodes, every team filled them in differently. One page put a badge next to the title, while another styled its own pill. One team's actions were plain buttons in one order; the next team's were a different mix in another order, some collapsing into a menu and others not. Everyone used the same shell component, yet the headers looked and behaved like they came from different products. The shared component guaranteed the wrapper, not what went inside it.</p><p>The <code>AppHeader</code> API closes that off. The same header is expressed as typed props:</p>&lt;AppHeader
  title="Index Management"
  badges={[
    {
      label: 'Beta',
      color: 'accent',
      tooltip: 'This feature is in beta.',
    },
  ]}
  description={{
    text: 'View and manage your Elasticsearch indices.',
    learnMoreUrl: docsUrl,
  }}
  tabs={tabs}
  menu={{
    primaryActionItem: {
      id: 'create',
      label: 'Create index',
      iconType: 'plusInCircle',
      run: onCreate,
    },
    items: [
      {
        id: 'refresh',
        label: 'Refresh',
        iconType: 'refresh',
        run: onRefresh,
      },
    ],
  }}
/&gt;<p>Now there’s only one way to express each part, so every page renders it the same. Badges live in their own typed collection, separate from the title. A description carries its text and, optionally, a "Learn more" URL. Actions declare whether they’re primary or secondary, and the component decides how they look and when they collapse. Plus, richer controls fit the same shape. An editable title or a favorite toggle is a structured config rather than a bespoke React tree:</p>&lt;AppHeader
  title={{
    text: indexName,
    onSave: renameIndex,
  }}
  favorite={{
    status: favoriteStatus,
    onToggle: toggleFavorite,
  }}
/&gt;<p>The split is consistent throughout; the application supplies state and behavior (the current name, what happens on rename, how to toggle a favorite) and the header owns presentation. It renders the controls, handles keyboard interaction, shows validation errors, and reflects pending changes. Because those roles are explicit, layout and responsive behavior stay inside the shared component, and applications don't need a coordinated update every time the header changes.</p><h3>Why a shared UI shell needs a closed set of controls</h3><p>The global shell had a similar problem that was one step more open-ended. Any plugin could register arbitrary content on the left or right and pick its position with a number, and no conversation with the platform team or designers was required:</p>chrome.navControls.registerLeft({
  content: &lt;ProjectPicker /&gt;,
});

chrome.navControls.registerRight({
  order: 10,
  content: &lt;AiAssistantButton /&gt;,
});

chrome.navControls.registerRight({
  order: 20,
  content: &lt;FeedbackButton /&gt;,
});<p>That <code>registerRight</code> API is an open door. Any team can add a control, and the header fills up with elements no one designed together. The controls compete for space and carry their own styling, and their order is decided by whoever picked the larger number. The shell only knows that one React node follows another; it can't tell that one opens an AI assistant and another collects feedback, so it can't reason about them or keep them coherent.</p><p>The new shell replaces that open canvas with a closed set of named roles under <code>chrome.controls</code> and <code>chrome.help</code>. The old registry hasn't gone anywhere yet, because the classic header still runs alongside the new one while applications migrate, but nothing in the new shell is reachable through it:</p>chrome.controls.projectPicker.set(&lt;ProjectPicker /&gt;);
chrome.controls.aiButton.register({ content: &lt;AiAssistantButton /&gt; });
chrome.controls.globalSearch.set({ onClick: openGlobalSearch });
chrome.help.registerFeedbackHandler(openFeedback);<h3>Global chrome header versus application page header</h3><p>Part of what made the old chrome hard to evolve was that "the header" was really several things tangled together. The redesign draws a hard line between two surfaces with different owners:</p><p>
</p><p><strong>Chrome (global) header</strong></p><p><strong>Application header</strong></p><p>Owner</p><p>The platform</p><p>The page</p><p>Scope</p><p>What's true everywhere in Kibana</p><p>What's true on this page</p><p>Contents</p><p>Navigation, project or deployment picker, search, help, AI assistant, feedback</p><p>Title, badges, description, tabs, page actions</p><p>How it's populated</p><p>Named slots under <code>chrome.controls</code> and <code>chrome.help</code></p><p><code>AppHeader</code> typed props, rendered directly or set via <code>chrome.appHeader.set()</code></p><p>Because each surface has one owner and a typed contract between them, the platform can redesign the chrome without auditing hundreds of pages, and an application can evolve its header without colliding with global controls. Setting a header config returns a cleanup callback, so an application tears down its own contribution when it unmounts.</p><h2>Design decisions that strict APIs force</h2><p>Stricter APIs forced design decisions that flexible APIs had let everyone defer. Two are worth calling out.</p><h3>Why we removed breadcrumbs from Kibana's navigation</h3><p>Removing breadcrumbs from the chrome was one of the more debated calls, and the state of the data made it easier than expected. In practice, breadcrumbs across Kibana were widely misconfigured:</p><ul><li><p>Some pages generated a breadcrumb for every tab on the page.</p></li><li><p>Some breadcrumbs weren't clickable.</p></li><li><p>Some repeated the title of the current page.</p></li></ul><p>They added visual weight without reliably adding orientation.</p><p>At the same time, the redesigned navigation already conveys hierarchy. The primary and secondary navigation show you where you are and give you a direct way back up. Keeping breadcrumbs would have meant maintaining a third (and frequently wrong) representation of the same information, so we stopped drawing the trail and let the navigation do that job.</p><p>The breadcrumb API itself didn't disappear. Applications still register breadcrumbs, and the new shell reads them to derive the back button and to fall back to a page title when an app hasn't supplied one. The data changed jobs rather than going away, which is why migrating an app to the new header rarely starts with ripping breadcrumbs out:</p><p><strong>Before:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt45c047fdfcad7c14/6aaa27d97d925c9a3baf92a0/unnamed.png" alt="Kibana page header with a breadcrumb trail repeating the GenAI Settings page title" /><p><strong>After:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6b04544a35718f97/6aaa27fe92336926efb1c2d0/unnamed.png" alt="the same Kibana page header with breadcrumbs removed, showing only the page title" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2ecbc17acb523ead/6aaa28219936f5d5f78b976d/unnamed.png" alt="Kibana breadcrumbs repeating a page title and tab name, annotated to show misconfigured navigation hierarchy" /><h3>How many action buttons a page header should show</h3><p>The other recurring debate was priority. With every page's actions now flowing through one menu structure, which buttons deserve to be visible, and in what order? Priorities shift as products evolve, so this conversation is ongoing.</p><p>For launch, we made one deliberate simplification: we locked the number of buttons that can appear in the app menu. A page gets one primary action and a bounded set of secondary actions before the rest collapse into a menu. The constraint keeps any single page from dividing the user's attention across a wall of buttons, and it makes the priority conversation explicit instead of letting it be settled by whoever adds the next button.</p><p>We went through several iterations to land here. Initially, we had many visible actions. A page could have up to three buttons, plus a secondary action to the left of the primary one. That took up too much space, so we gradually simplified the layout by removing the secondary action and reducing the number of visible buttons. We also made the overflow menu more structured. Some items, such as feedback and documentation, now have a fixed place in the footer of the overflow menu.</p><h2>What strict design system governance costs teams</h2><p>Under the old API, a plugin could add a novel UI by choosing a side, picking an order, and mounting a React tree. Under the stricter API, a new kind of control may need a shared capability before a plugin can add it. That takes more work up front and forces a design decision that teams could previously avoid.</p><p>We accepted the cost of stricter APIs because the alternative pushed it into every later redesign. As long as arbitrary React trees mounted at arbitrary points, every change to the shell's layout or accessibility had to account for all of them.</p><p>That’s the trade we made, and it pays back on both fronts. Consistency stops being something each team has to get right by hand; there’s one way to express a control, so pages match by default. And the shared surface stays governed. Its set of controls is a deliberate design decision, not whatever accumulated at the edges. Applications describe what their controls mean, and the shell is free to decide how they look, both today and in the next redesign.</p><h2>Where the redesigned Kibana chrome is available</h2><p>The redesigned chrome is available now in <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a>. Open any project, and you're already using it. For <a href="https://www.elastic.co/cloud">Elastic Cloud Hosted</a> and self-managed users, it ships in 9.6.</p><p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/design-system-governance-kibana-page-headers</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/design-system-governance-kibana-page-headers</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Developer Experience]]></category>
    <dc:creator><![CDATA[Anton Dosov,Ryan Keairns,Krzysztof Kowalczyk,Alex Marhaba]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc5228b5443fe9d20/6aaa28eb27e4f0a047befaa4/unnamed.png" length="0" type="image/png"/>
    <pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[You and your AI agent shouldn't be using curl: Introducing the Elastic CLI and Agent Skills]]></title>
    <description><![CDATA[Elastic CLI reaches every Elasticsearch, Kibana and Cloud API from one command, and it's what Elastic Agent Skills run on. Input is validated against a JSON Schema before anything leaves your machine, and API keys stay in your OS keychain.]]></description>
    <content:encoded><![CDATA[<p><a href="https://github.com/elastic/cli">The Elastic CLI</a> gives you one command for every public Elastic API: Elasticsearch, Kibana, and Elastic Cloud's control plane, including Serverless projects. Learn <code>elastic es search</code>, and you already know how <code>elastic kb data-views list</code> behaves. It's built to be driven by an AI coding agent as easily as by you, so every command takes JSON in and out and validates input against a JSON Schema before sending anything. Plus, it exits with a code that an agent can branch on. Administrators control which commands run at all, and API keys go to your OS keychain, never into a large language model (LLM) transcript. Our Agent Skills now run on it. The command line interface (CLI) is in technical preview today.</p><p><a href="https://cloud.elastic.co/serverless-registration">Start a free Elastic Cloud Serverless trial</a> or <a href="https://cloud.elastic.co/login">log in to Elastic Cloud</a> to follow along, and install the CLI via <a href="https://www.npmjs.com/">npm</a>: </p>npm install -g @elastic/cli<h2>Designing the Elastic CLI for people and AI agents</h2><p>A useful side effect of building flexible tools for developers is that they’re more useful to AI agents, too. It’s also the tool that our<a href="https://github.com/elastic/agent-skills"> Agent Skills</a> now use to get things done, closing <a href="https://www.elastic.co/search-labs/blog/agent-skills-elastic">the loop that we opened in March 2026</a>, when we said that a CLI for agent workflows was coming.</p><p>The CLI gives every public API across Elasticsearch, Kibana, and Elastic Cloud one shape: the same flags, input and output conventions, authentication method, and failure mode. Consistency is the ergonomic feature; everything else is built on it.</p><p>Agents need the same thing, only stricter. An agent won’t know how to craft a valid CLI command or notice if a tool “feels” wrong; it needs output it can parse and input it can validate before sending, along with failures it can branch on. Agents are now a first-class interface to Elastic, alongside people, whether they live on the platform or in your editor and terminal.<a href="https://github.com/elastic/agent-skills"> Agent Skills</a>, and now the CLI, are how we serve the second kind, so those needs are built into the core of the CLI rather than tacked on.</p><h3>JSON input and output for every command</h3><p>Agents love structured text, and almost every Elastic API already speaks JSON, so first-class JSON support was a hard requirement. Developers who are quick with a <u><code>jq</code></u> query will be equally satisfied.</p><ul><li><p><strong>JSON output:</strong> Any command, like <code>elastic version</code> and <code>elastic es indices delete ...</code>, supports <code>--json</code>, which prints JSON-parseable output to stdout and nothing else. Failed commands print <code>{"error": {"code": "...", "message": "..."}}</code> to stderr.</p></li><li><p><strong>JSON input:</strong> Every command that takes input accepts JSON on stdin or via <code>--input-file</code>. Every top-level key in that JSON also works as a CLI argument, and inline arguments take precedence, so you can keep a big request body in a file and tweak a value or two per invocation.</p></li><li><p><strong>JSON Schema as </strong><strong><code>-</code></strong><strong><code>-help</code></strong><strong> output:</strong> Pass <code>--help --json</code> to any command, and it prints a valid JSON Schema for its input, which also feeds nicely into codegen tools. <code>elastic cli-schema</code> prints the whole command tree.</p></li></ul><h3>Exit codes that an AI agent can branch on</h3><p>Agents loop on exit codes as much as they do over stdout. All failure modes are distinguishable from success, even if stdout and stderr are never read.</p><h3>Safety rails: Keychain storage, allow lists, and validation</h3><p>No model uses tools perfectly 100% of the time, so an agent-friendly CLI should provide safety rails wherever possible.</p><ul><li><p><strong>Contexts and secret storage:</strong> Connection details live in named contexts in <code>~/.elasticrc.yml</code>, <code>kubectl</code>-style; switch with <code>--use-context</code>. API commands never take credentials as flags. <code>elastic config context add</code> writes API keys to your OS keychain (macOS, Linux, Windows) and leaves a <code>$(keychain:...)</code> reference in the YAML; <code>$(env:...)</code>, <code>$(cmd:...)</code>, and <code>$(file:...)</code> work, too. Creating a Serverless project with <code>--save-as</code> writes its credentials straight to the keychain and never prints them, so nothing leaks into logs or LLM transcripts.</p></li><li><p><strong>Allowlists/blocklists:</strong> A <code>commands.allowed</code> (or <code>commands.blocked</code>) list in the config file, globally or per context, ensures that only the commands an administrator wants are runnable.</p></li></ul>commands:
   allowed:
     - version
     - stack.es.search
     - stack.es.esql.*<ul><li><p><strong>Validation:</strong> Every command has a JSON Schema, so inputs are validated before any request is sent. Add <code>--dry-run</code> to any command that takes input, and it validates and exits without sending anything.</p></li><li><p><strong>Confirmation:</strong> Destructive commands prompt in a terminal. In a noninteractive session, where agents live, they refuse to run without <code>--yes</code> and say so in a structured error.</p></li><li><p><strong>Sanitization:</strong> Index, field, and pipeline names have length limits and forbidden characters. <code>elastic sanitize index-name '&lt;value&gt;'</code> (and <code>field-name</code>, <code>pipeline-name</code>, …) prints a version stripped of anything invalid.</p></li></ul><h3>Keeping API responses inside an agent's context window</h3><p>Elastic APIs return a lot of data, and an agent’s context window is finite. Three controls help keep unnecessary text out of the context window:</p><ul><li><p><strong>Field masks:</strong> <code>--output-fields</code> takes a comma-separated list, with dot notation for nested fields.</p></li></ul>elastic es info --output-fields 'name,version.number'
 # {
 #   "name": "serverless",
 #   "version": { "number": "9.5.0" }
 # }<ul><li><p><strong>String templates:</strong> For total control, <code>--output-template</code> takes a <a href="https://mustache.github.io/">mustache</a>-style template.</p></li></ul>elastic es info --output-template 'ES version: {{ version.number }}'
# ES version: 9.5.0<ul><li><strong>Command profiles:</strong> <code>--command-profile</code> serverless (or <code>default_profile: serverless</code> in your config) hides Elastic Cloud Hosted commands and the Elasticsearch namespaces that don’t exist on Serverless. That means less to scroll past and less for an agent to guess wrong. It’s the profile we recommend for agents.</li></ul><p><strong>Control</strong></p><p><strong>What it does</strong></p><p><strong>Syntax</strong></p><p><strong>When to use</strong></p><p>Field mask</p><p>Returns only the fields you name, using dot notation for nested fields</p><p><code>--output-fields 'name,version.number'</code></p><p>You want valid JSON back, just less of it. This is the default choice for agents parsing structured output.</p><p>String template</p><p>Renders the response through a mustache-style template</p><p><code>--output-template 'ES version: {{ version.number }}'</code></p><p>You need one value in a specific shape, for a shell variable, a log line, or a prompt.</p><p>Command profile</p><p>Hides commands and namespaces that don't apply to your deployment</p><p><code>--command-profile serverless</code>or <code>default_profile: serverless</code></p><p>You want a smaller command surface so an agent has less to scroll past and less to guess wrong. This is recommended for agents.</p><p></p><h2>Helpers for bulk ingest, scroll search, and msearch</h2><p>Some of Elasticsearch’s most popular APIs have a learning curve, so elastic es helpers wraps them:</p><ul><li><p><code>scroll-search</code>: Stream a large result set as NDJSON with paging handled for you.</p></li><li><p><code>bulk-ingest</code>: Ingest from a file, a directory, or stdin (NDJSON, JSON arrays, or CSV) with streaming, batching, concurrency, and retries.</p></li><li><p><code>msearch</code>: Send multiple searches in one request.</p></li><li><p><code>watch</code>: Print new documents from an index to stdout as they’re indexed. This is great for piping into logging tools.</p></li></ul><p><code>elastic es</code> and <code>elastic kb</code> are aliases for <code>elastic stack elasticsearch</code> and <code>elastic stack kibana</code>. If we don’t ship a command you need, <code>elastic extension create</code> scaffolds one for you.</p><h2>Searching Elastic docs from the terminal</h2><p>If you or your agents don’t know which API to use, elastic docs search (plus docs read and docs ask) searches Elastic’s documentation from the terminal, returning Markdown or <code>--json</code>. These are experimental. You’ll see a warning until you pass <code>--accept-experimental</code>, so explore, but don’t script against them yet.</p><h2>Shell completion for Bash, Zsh, and Fish</h2><p>Autocomplete hooks are available for Bash, Zsh, and Fish, and they always respect your <code>commands.allowed</code> or <code>commands.blocked</code> policy.</p><h2>How Elastic Agent Skills use the CLI</h2><p><a href="https://github.com/elastic/agent-skills">Agent Skills</a> teach an AI coding agent how an Elastic expert approaches a job; for example, which cluster health field is the verdict or how to stage a reindex so it doesn’t fall over. They capture process and judgment but not transport. A skill that embeds <a href="https://curl.se/">curl</a> with an auth header has hard-coded a hostname, key, and runtime, and it breaks when any of those change.</p><p>So our skills now use a <em>universal</em> format that runs unchanged in any runtime that can execute the <code>elastic</code> CLI, including Claude Code, Codex, Cursor, and GitHub Copilot. The body refers to operations in HTTP shorthand (<code>GET /_cluster/health</code>, <code>POST /_query</code>), and an operations table at the end binds each to a CLI command. That table is the only place transport appears:</p><p>HTTP API (shorthand)</p><p><code>elastic</code> CLI command</p><p><code>GET /{index}/_mapping</code></p><p><code>elastic es indices get-mapping --index '&lt;index&gt;'</code></p><p><code>POST /_query</code></p><p><code>elastic es esql query --format tsv --query "&lt;esql&gt;"</code></p><p><code>POST cloud:/api/v1/serverless/projects/elasticsearch</code></p><p><code>elastic cloud serverless projects search create --input-file &lt;json&gt; --wait --save-as &lt;ctx&gt;</code></p><p>Every universal skill also inherits a blunt preamble; that is, use the CLI, don’t guess credentials, don’t call the HTTP API directly, and never ask the user to paste an API key into the chat.</p><p>The two halves need each other. The skill supplies the expertise that the model doesn’t have, and the CLI supplies a way to act on it that’s validated, credential-safe, and scoped by your allowlist. Tell your agent to <em>spin up a Serverless project and load products.csv into it</em>, and the provisioning skill creates it with <code>--save-as</code>. The ingest skill dry-runs a mapping and loads with <code>elastic es bulk</code>, and the Elasticsearch Query Language (ES|QL) skill writes a query that parses on the first try. Every step returns JSON, exits non-zero on failure, and can only do what your policy allows.</p><p>Skills for Elastic Cloud onboarding and provisioning, Elastic Workflows, and Kubernetes investigation are available today. Skills for Elasticsearch query, ingest, reindex, and index design, plus Kibana dashboards and alerting, are close behind.</p><h2>What’s in the Elastic CLI technical preview, and what’s next</h2><p>The preview covers all public Elasticsearch Serverless, Kibana Serverless, and Elastic Cloud APIs. Hosted-only 9.x Elasticsearch API coverage is nearly 100%, and hosted-only 9.x Kibana APIs will be added soon.</p><p>We’re actively planning more developer experience work, including broader coverage for all supported stack releases, more helpers for common workflows, more skills in the public catalog, and loading the same skills into agents that run on the Elastic platform itself. What shapes that list is hearing how you and your agents use the CLI. Tell us what’s awkward and what’s missing, along with what you’d automate next.</p><h2>Install the Elastic CLI and Agent Skills</h2><p>The Elastic CLI is available now on npm (Node.js 22+). Install it and the skills together:</p>npm install -g @elastic/cli # or: npx -y @elastic/cli --help
npx skills add elastic/agent-skills<p>Then, add a context and check it:</p>elastic config context add prod --es-url https://&lt;project&gt;.es.us-east-1.aws.elastic.cloud --es-api-key &lt;KEY&gt;
 elastic status<p>Even without a project, you can <a href="https://cloud.elastic.co/serverless-registration">start a free Serverless trial</a> in about a minute, with no credit card. If you already have a project, <a href="https://cloud.elastic.co/login">log in</a> and create API keys for Elastic Cloud and your Elasticsearch clusters. Before pointing an agent at anything real, start with a trial project, a read-only key, and a scoped commands.allowed list. Be sure to take five minutes to read the <a href="https://github.com/elastic/agent-skills#security-considerations">security notes</a> in the skills repo.</p><p>Replace all those curl commands in your Bash scripts, and add some usage instructions to your AGENTS.md. Then let your agent’s skills work efficiently and accurately with our APIs. Let us know what you think, and don’t hesitate to<a href="https://github.com/elastic/cli/issues"> open an issue</a> if you find a bug or if your use case isn’t well supported. Your feedback directly shapes what we build next.</p><h2>Elastic CLI and Agent Skills resources</h2><ul><li><p><a href="https://github.com/elastic/cli">Elastic CLI on GitHub</a> and<a href="https://github.com/elastic/cli/tree/main/docs/cli"> CLI documentation</a></p></li><li><p><a href="https://github.com/elastic/agent-skills">Elastic Agent Skills on GitHub</a></p></li><li><p><a href="https://agentskills.io">agentskills.io specification</a></p></li><li><p><a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless documentation</a></p></li><li><p><a href="https://github.com/elastic/cli/issues">Report a CLI issue</a> ·<a href="https://github.com/elastic/agent-skills/issues"> Report a skills issue</a> ·<a href="https://discuss.elastic.co/"> Discuss</a></p></li></ul>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elastic-cli-ai-agents</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elastic-cli-ai-agents</guid>
    <category><![CDATA[Developer Experience]]></category>
    <category><![CDATA[AI Tools ]]></category>
    <category><![CDATA[Agentic AI]]></category>
    <dc:creator><![CDATA[Josh Mock,Matt Ryan]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt111f4783cff3ef01/6aa7bad035eddc3a1a11d192/image1.png" length="0" type="image/png"/>
    <pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Migrating 1,100 files to Redux Toolkit v2 without freezing the Kibana monorepo]]></title>
    <description><![CDATA[Kibana gave Redux Toolkit v2 the default package name and pushed v1 onto an explicit alias, which inverts the usual migration order. Webpack externals, yarn resolutions and an ESLint rule keep React Redux v7 and v9 out of each other's way.]]></description>
    <content:encoded><![CDATA[<p>We moved roughly 1,100 files in the Kibana monorepo onto <a href="https://github.com/elastic/kibana/pull/235577">Redux Toolkit (RTK) v2</a> aliases without asking a single plugin team to pause feature work. The usual migration pattern runs the other way around. Default package names (<code>@reduxjs/toolkit</code>, <code>react-redux</code>, <code>redux</code>) now resolve to v2, and existing v1 code sits behind explicit aliases, like <code>redux-toolkit-v1</code> and <code>react-redux-v7</code>. Both versions live in <code>node_modules</code> at once, kept apart at runtime by npm aliases and webpack module replacement. An ESLint rule scoped to 36 plugin paths catches anything that tries to cross. When a team is ready, it deletes its path from that list and switches back to the default imports, and the teams around it carry on shipping.</p><h2>Why upgrade to Redux Toolkit v2?</h2><p>RTK v2 was released in late 2023. That's nearly three years of running on a major version behind in one of the most widely used state management libraries in the JavaScript ecosystem. It reflects how hard this upgrade is in a codebase of Kibana's size. A <a href="https://github.com/elastic/kibana/pull/178986">previous attempt</a> tried the big-bang approach and stalled when the real scope became clearer. </p><p>So what does v2 actually bring? It ships alongside Redux core 5.0, React-Redux 9.0, Reselect 5.0, and Redux Thunk 3.0. React-Redux 9.0 requires React 18 and drops the <code>useSyncExternalStore</code> shim that v8 carried for React 16/17. Since Kibana already runs React 18, upgrading sheds legacy compatibility code and keeps Kibana on the actively maintained Redux majors.</p><p>RTK v2 also brings genuinely useful new features, including inline selectors in <code>createSlice</code> and opt-in inline async thunks through a customized <code>buildCreateSlice</code> setup, along with a <code>combineSlices</code> API with slice reducer injection for code splitting. That last one is particularly interesting for Kibana's plugin architecture where lazy-loading is the norm.</p><h2>How Redux is used across the Kibana monorepo</h2><p>Before diving into the solution, it's worth understanding just how varied Redux usage is across Kibana. A full audit of the codebase (tracked in <a href="https://github.com/elastic/kibana/issues/239863">#239863</a>) revealed several distinct camps:</p><p><strong>Pattern</strong></p><p><strong>Plugins and packages</strong></p><p><strong>What the migration needs</strong></p><p>Redux Toolkit v1</p><p>Discover, Lens, Synthetics, Security Solution</p><p>Full v1 to v2 migration</p><p>Plain Redux v4</p><p>Canvas, Maps, Index Management, Cross-Cluster Replication</p><p><code>redux-v4</code> alias only, no RTK migration</p><p>Kea</p><p>Enterprise Search (150+ files), Content Connectors</p><p><code>react-redux-v7</code> alias, no RTK migration</p><p><code>redux-saga</code></p><p>Synthetics, Graph, Uptime</p><p>Store setup only, saga is version-independent</p><p><code>typescript-fsa</code></p><p>Security Solution data-table package</p><p>Out of scope</p><p>Types and single imports</p><p>Expressions, Monitoring</p><p>Alias swap only</p><p>Plugins such as Discover, Lens, Synthetics, and Security Solution use RTK v1 APIs,including <code>createSlice</code>, <code>configureStore</code>, <code>createAsyncThunk</code>, and <code>createSelector</code>. These are the ones that actually need the v1 to v2 migration. But even here, complexity varies wildly. Lens uses stand-alone <code>getDefaultMiddleware</code> (removed in v2) and <code>PreloadedState</code> (also removed). Security Solution is the largest consumer at 300+ files, mixing modern RTK with legacy plain Redux patterns.</p><p>Canvas, Maps, Index Management, Cross-Cluster Replication, and several others still use plain Redux v4via <code>createStore</code>, <code>combineReducers</code>, <code>applyMiddleware</code>, and <code>connect</code>, which are classic patterns from the pre-RTK era. These don't need RTK migration at all since they're not using it  in the first place, but they do need the <code>redux-v4</code> alias since the default <code>redux</code> package is now v5.</p><p>Enterprise Search and Content Connectors use <code>kea</code>, a Redux abstraction layer with its own logic builders (<code>kea()</code>, <code>useValues</code>, <code>useActions</code>). There are more than 150 files in Enterprise Search alone. RTK migration isn't applicable here, since <code>kea</code> is its own world. But it <em>does</em> depend on <code>react-redux</code> v7 under the hood, which is where the bundler tricks come in.</p><p>Synthetics, Graph, and Uptime use <code>redux-saga</code> for side effects. Saga integration is actually independent of the RTK version, but these plugins need their store setup migrated.</p><p>The Security Solution data-table package uses <code>typescript-fsa</code> and <code>typescript-fsa-reducers</code> instead of RTK entirely, with its reducer embedded into Security Solution's main store, and isn’t part of the RTK migration at all.</p><p>The Expressions plugin only imports <code>shallowEqual</code> from <code>react-redux</code>, and Monitoring only imports types. These just need an alias swap.</p><p>Asking every team to migrate simultaneously was a nonstarter. The breaking changes in RTK v2 include stricter type checking and removed APIs, like <code>enableES5()</code> from immer, <code>getDefaultMiddleware</code> and <code>PreloadedState</code> gone entirely, <code>AnyAction</code> replaced by <code>UnknownAction</code>, and behavioral changes in how middleware is configured.</p><h2>Running Redux Toolkit v1 and v2 side by side</h2><p>The solution was to flip the typical migration pattern on its head. Instead of keeping the default imports on v1 and introducing v2 under aliases, the default package names (for example, <code>@reduxjs/toolkit</code>, <code>react-redux</code>, and <code>redux</code>) now point to v2. The old versions live under versioned aliases:</p><ul><li><p><code>redux-toolkit-v1</code></p></li><li><p><code>react-redux-v7</code></p></li><li><p><code>redux-v4</code></p></li><li><p><code>immer-v9</code></p></li><li><p><code>reselect-v4</code></p></li><li><p><code>redux-thunk-v2</code></p></li></ul>{
"@reduxjs/toolkit": "2.12.0",
"redux-toolkit-v1": "npm:@reduxjs/toolkit@1.9.7",
"react-redux": "9.2.0",
"react-redux-v7": "npm:react-redux@7.2.8"
}<p>This is npm's alias syntax. <code>"react-redux-v7": "npm:react-redux@7.2.8"</code> installs the old version under a different name. Both versions coexist in <code>node_modules</code> without conflicts.</p><p>The insight here is that all existing code in this pull request (PR) was moved to v1 aliases. Every <code>import { useSelector } from 'react-redux'</code> became <code>import { useSelector } from 'react-redux-v7'</code>. That's ~1,100 files touched, but the vast majority (~1,000) are mechanical one-liner import swaps. When a team is ready to migrate to v2, they switch back to the default import names. Once all v1 aliases disappear from the codebase, the old packages can be removed entirely.</p><p>This avoids the alternative, where v2 imports would end up under nonstandard names permanently, leaving nonstandard imports in the codebase for the long term.</p><h2>Serving both versions through the bundler</h2><p>Getting two versions of the same library to coexist at runtime is where things got interesting. Kibana uses <code>kbn-ui-shared-deps-npm</code> to bundle common dependencies as shared webpack externals. This needed to serve both the new v2 packages <em>and</em> the v1 aliases so that both are available at runtime.</p><h3>Pinning @elastic/charts with yarn resolutions</h3><p>Then there's <code>@elastic/charts</code>. It depends on RTK v1 internally and can't just be upgraded independently since it's an upstream package. Yarn resolutions pin its nested dependencies to v1 versions:</p>{
"@elastic/charts/@reduxjs/toolkit": "npm:@reduxjs/toolkit@1.9.7"
}<p>A <code>NormalModuleReplacementPlugin</code> in the shared deps webpack config detects when an import of <code>immer</code>, <code>@reduxjs/toolkit</code>, <code>redux</code>, <code>react-redux</code>, or <code>reselect</code> originates from within <code>@elastic/charts</code> and redirects resolution to the nested v1 copies. This ensures that <code>@elastic/charts</code> resolves to its compatible v1 dependency set.</p><h3>Keeping Kea on React Redux v7 with webpack externals</h3><p>The <code>kea</code> library was another fun case. It declares <code>react-redux</code> as a peer dependency (<code>&gt;= 7</code>), so without special handling its imports resolve to Kibana's default v9 package. The migration keeps Kea consumers on <code>react-redux-v7</code>, so Kea must use that same React context. The fix uses function-based webpack/rspack externals that skip externalizing <code>react-redux</code> when the import comes from <code>node_modules/kea</code>, combined with a <code>NormalModuleReplacementPlugin</code> that rewrites it to <code>react-redux-v7</code>. This ensures that <code>kea</code> uses the v7 React context that matches the <code>&lt;Provider&gt;</code> wrapping its consumers.</p><p>Both the webpack (<code>kbn-optimizer</code>) and the rspack (<code>kbn-rspack-optimizer</code>) configs needed these changes, with a shared <code>isKeaReactReduxImport</code> helper extracted to keep the logic consistent.</p><h2>Using an ESLint rule to prevent cross-version imports</h2><p>With two versions available, accidental cross-version imports are the biggest risk. A new <code>@kbn/imports/no_redux_toolkit_v2_imports</code> ESLint rule catches any import of the v2 default packages (such as <code>@reduxjs/toolkit</code>, <code>react-redux</code>, or <code>redux</code>, among others) in code that hasn't been migrated yet. It even auto-fixes them to the v1 aliases for file imports and Jest mock paths.</p><p>The rule is scoped via an override in <code>.eslintrc.js</code> to the ~36 plugin and package paths currently using v1. When a team migrates, they simply remove their path from the override list. This clean, self-service approach requires no coordination.</p>// .eslintrc.js (simplified)
overrides: [{
  files: [
'src/platform/plugins/shared/discover/**/*.{ts,tsx}',
'src/platform/plugins/shared/workflows_management/**/*.{ts,tsx}',
// ... 34 more paths
],
  rules: {
'@kbn/imports/no_redux_toolkit_v2_imports': 'error',
  },
}]<h2>Why mixing React Redux v7 and v9 breaks the context</h2><p>This is worth calling out because it's an easy failure mode to miss during an upgrade. <code>react-redux</code> v9 and v7 create separate React contexts. If a component tree has a v9 <code>&lt;Provider&gt;</code> at the top but a child component calls <code>useSelector</code> from v7 (or vice versa), React-Redux cannot find the matching context. In development, it throws an error explaining that the component must be wrapped in a matching <code>&lt;Provider&gt;</code>; in production, the missing context causes a runtime error when the hook accesses the store.</p>Error: could not find react-redux context value; please ensure the component is wrapped in a &lt;Provider&gt;<p>This means that each plugin needs to be explicitly pinned to one version. Shared packages that use <code>react-redux</code> can only be consumed by code on the same version, since mixing isn't possible. This is a constraint that makes the migration inherently per plugin rather than per file.</p><h2>Migration batches: What can move independently</h2><p>The dual-version setup gives every team a clear path forward, and the dependency graph analysis from the tracking issue identified natural migration batches:</p><ul><li><p><strong>Batch 1: Independent, self-contained stores.</strong> Packages like <code>kbn-coloring</code>, <code>transform</code>, <code>timelines</code>, and <code>expandable-flyout</code> have fully internal Redux stores with no types leaking through their public APIs. These can be migrated independently by their owning teams, with minimal risk.</p></li></ul><ul><li><p><strong>Batch 2: Coupled packages.</strong> Some packages share RTK types across boundaries and <em>must</em> migrate together. The machine learning (ML)/artificial intelligence for IT operations (AIOps) chain is one example: <code>@kbn/ml-response-stream</code> exports a <code>streamSlice</code> (a <code>createSlice</code> return value) that <code>@kbn/aiops-log-rate-analysis</code> embeds directly into its <code>configureStore</code>. Migrating one without the other causes type mismatches between v1 and v2 slice types. Similar coupling exists across the Lens ecosystem. The Lens plugin depends on <code>@kbn/coloring</code> (which has its own RTK store), <code>@kbn/lens-embeddable-utils</code>, and <code>@kbn/lens-common</code>, while itself being consumed by 40+ packages and plugins across chart expressions, visualizations, Maps, Canvas, and observability plugins. Whether Redux types leak through a package's public API determines if it can be migrated independently or needs coordination. <code>kbn-coloring</code>'s store is internal to its React components so it's safe to migrate alone, but other coupling points need careful analysis.</p></li></ul><ul><li><p><strong>Batch 3+: The big ones.</strong> Discover, Security Solution, and Lens each have their own migration timelines. Security Solution's 300+ files and mix of RTK with plain Redux v4 and <code>typescript-fsa</code> make it the largest effort, but the different patterns can be addressed independently. Lens has the trickiest v2 breaking changes around middleware configuration; stand-alone <code>getDefaultMiddleware</code> and <code>PreloadedState</code> are both removed in v2, and it has four custom middleware files with complex typing.</p></li></ul><p>Beyond the batched migrations:</p><ul><li><p><strong>Deprecated features</strong> can stay on v1 aliases. When the feature is removed, the v1 imports disappear through code deletion, without any migration work.</p></li><li><p><strong>Plain Redux v4 plugins</strong> (Canvas, Maps, and others) are entirely out of scope for RTK migration. They'd benefit from modernization, but that's a separate initiative.</p></li><li><p><strong>Kea plugins</strong> need <code>react-redux-v7</code> to <code>react-redux</code> alias updates eventually, but no RTK migration. The longer-term question (whether to keep Kea or migrate to RTK v2) is a separate decision.</p></li><li><p><strong>The dual-version approach</strong> adds measurable bundle overhead during the transition. It’s a trade-off but is acceptable for the migration period.</p></li></ul><h2>Lessons for other large monorepo upgrades</h2><p>The ESLint rule turned out to be the linchpin. Without automated enforcement, aliased imports would drift back to default names within weeks. With it, the migration state is visible in the paths listed in the override. As of the initial PR, zero files import from <code>@reduxjs/toolkit</code> v2. Every RTK usage goes through the <code>redux-toolkit-v1</code> alias. That's the starting line.</p><p>The preparation work also reached beyond import paths. Jest mocks referencing <code>react-redux</code> needed updating to <code>react-redux-v7</code>, as did Storybook previews, test helpers, and ambient type declarations. Multiple rounds of <code>node scripts/eslint_all_files --no-cache --fix</code> caught the mechanical cases; the remaining cases needed manual fixes.</p><p>If you're facing a similar major dependency upgrade in a large monorepo, the pattern of giving the new version the default name and the old version an explicit alias is worth considering. New code naturally uses the current version, while older usage stays visible and trackable until it reaches zero.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/redux-toolkit-v2-migration-kibana-monorepo</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/redux-toolkit-v2-migration-kibana-monorepo</guid>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Developer Experience]]></category>
    <category><![CDATA[Kibana]]></category>
    <dc:creator><![CDATA[Walter Rafelsberger]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3fa3c79a7e246c89/6a9111235c3126655043df06/unnamed.png" length="0" type="image/png"/>
    <pubDate>Fri, 28 Aug 2026 15:20:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Kibana Dashboards API: A stable contract for every panel type, tested by 50+ teams before GA]]></title>
    <description><![CDATA[Manage Kibana dashboards as code: Commit to Git, promote across environments, and automate deployments with the Kibana API and Terraform.]]></description>
    <content:encoded><![CDATA[<p>The<a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards"> Kibana Dashboards and Visualizations APIs</a> are production-ready in Elastic 9.5, available across all subscription tiers, with full backward compatibility. Define your dashboards as JSON, commit them to Git, and then deploy across environments using continuous integration and continuous deployment (CI/CD) pipelines,<a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard"> Terraform</a>, or whatever tooling you already have. Over 50 teams tested the API during<a href="https://www.elastic.co/search-labs/blog/kibana-dashboards-as-code-terraform-api"> technical preview in 9.4</a>, some already running it in production. Version 9.5 also adds new endpoints (in technical preview) for<a href="https://dashboardsapispec.kibana.dev/tags.html"> Tags</a>, with<a href="https://dashboardsapispec.kibana.dev/markdowns.html"> Markdown</a> and<a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links"> Links</a> panel endpoints available now in Elastic Cloud Serverless and landing in 9.6.</p><h2>What backward compatibility means for the Kibana Dashboards API</h2><p>During technical preview, the API shape could change between releases.[1] That's no longer the case. General availability (GA) means:</p><ul><li><p><strong>Complete backward compatibility.</strong> New fields and panel types will be added over time, but existing fields and behavior remain unchanged. Any future breaking changes would be very carefully considered and would only be introduced in a new major stack version.</p></li><li><p><strong>Production-ready with full support.</strong> The API carries Elastic's full support guarantees. You can safely use it in production environments for automated deployments, environment promotion, and programmatic dashboard management.</p></li></ul><h2>New Kibana API endpoints for Tags, Markdown, and Links panels</h2><p>Elastic 9.5 also introduces a new  standalone endpoint for <a href="https://dashboardsapispec.kibana.dev/tags.html"><strong>Tags</strong></a>, which let you categorize and filter dashboards. Now you can manage them programmatically through dedicated CRUD endpoints, making it easier to organize dashboards at scale across environments.	</p><p>New <a href="https://dashboardsapispec.kibana.dev/markdowns.html"><strong>Markdown</strong></a> and <a href="https://dashboardsapispec.kibana.dev/links.html#tag/Links"><strong>Links</strong></a> panel endpoints are available now in Serverless and will land in the next stack release (9.6).</p><h2>What panel types does the Kibana Dashboards API support?</h2><p>The Dashboards API supports all <em>by-value</em> panels in 9.5 (those defined directly in a dashboard, as opposed to library panels saved for reuse). Every supported panel type has a typed, validated schema.</p><p><strong>Panel type</strong></p><p><strong>Status</strong></p><p>XY charts</p><p>Supported</p><p>Metrics</p><p>Supported</p><p>Pie</p><p>Supported</p><p>Gauge</p><p>Supported</p><p>Heatmap</p><p>Supported</p><p>Data tables</p><p>Supported</p><p>Treemap</p><p>Supported</p><p>Discover sessions</p><p>Supported</p><p>Controls</p><p>Supported</p><p>Markdown</p><p>Supported</p><p>Links</p><p>Supported</p><p>ML panels</p><p>Supported</p><p>Observability panels</p><p>Supported</p><p>Maps</p><p>Coming soon</p><p>Vega</p><p>Coming soon</p><h2>How to manage Kibana dashboards as code</h2><p>The Dashboards API enables a full dashboards-as-code workflow: Export a dashboard as clean, diffable JSON, commit it to Git as the source of truth, review changes in pull requests, and deploy the same definition across development, staging, and production. Once a dashboard is managed as code, treat Git as the single source of truth: Changes made directly in the UI are overwritten the next time you deploy.</p><p>The main challenge when moving a dashboard between spaces, clusters, or stages is that dashboards reference objects like data views and library visualizations by ID. Because these IDs are auto-generated and differ across environments, a dashboard exported from one environment can point at objects that don't exist in another. There are three ways to handle this, listed here from most to least automated:</p><ul><li><p><strong>Use Terraform.</strong> The <a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraform provider</a> tracks each resource and maps IDs per environment automatically, so references stay consistent as you promote a dashboard from development to production.</p></li><li><p><strong>Define by-value </strong><a href="https://www.elastic.co/docs/explore-analyze/visualize/esorql"><strong>Elasticsearch Query Language (ES|QL) panels</strong></a><strong>.</strong> The most portable way to build a panel is to define its visualization with ES|QL directly in the dashboard. An <a href="https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql-kibana">ES|QL</a> query reads from the indices you name in it, so the panel carries no external references to data views or library objects. The result is a fully self-contained, portable dashboard.</p></li><li><p><strong>Assign matching IDs.</strong> If you reference saved objects, like data views or library visualizations, create them with a chosen ID using <code>PUT</code> (upsert) rather than <code>POST</code> (which auto-generates an ID). Use human-readable IDs, like <code>logs-prod</code>, so they're easy to reuse and recognize across environments.</p></li></ul><p>For a detailed walkthrough of these portability patterns and the full dashboards-as-code workflow, see the <a href="https://www.elastic.co/docs/explore-analyze/dashboards/manage-dashboards-as-code#dashboards-as-code-portability">Manage dashboards as code</a> documentation.</p><h3>Create a Kibana dashboard with the Dashboards API using PUT</h3><p>Here's a quick example creating a dashboard with a metric panel using <code>PUT</code> instead of <code>POST</code> to assign a custom ID using the dashboard name (<code>service-health-overview</code>). The same logic works for creating standalone visualizations saved in the library.</p>PUT kbn:/api/dashboards/service-health-overview
{
  "title": "Service health overview",
  "description": "Key service metrics — managed via API",
  "tags": [
    "production",
    "sre-team"
  ],
  "panels": [
    {
      "type": "vis",
      "grid": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 8
      },
      "config": {
        "title": "Error rate (5xx)",
        "type": "metric",
        "data_source": {
          "type": "esql",
          "query": "FROM logs-* | WHERE http.response.status_code &gt;= 500 | STATS error_rate=count(*) BY host.name"
        },
        "metrics": [
          {
            "type": "primary",
            "column": "count"
          }
        ]
      }
    }
  ]
}<h2>Kibana Dashboards API roadmap: Maps, Vega, and standalone endpoints</h2><p>We're actively expanding the API surface. Maps and Vega panel support is next, adding typed schemas for them. We're also building standalone CRUD endpoints for Discover sessions (beyond their existing support as dashboard panels), Vega, Maps, and Annotations, decoupled from the dashboard lifecycle.</p><p>For the full schema definitions, visit the <a href="https://dashboardsapispec.kibana.dev/dashboards#tag/Dashboards">Dashboards API documentation</a>. For Terraform users, the <a href="https://registry.terraform.io/providers/elastic/elasticstack/latest/docs/resources/kibana_dashboard">Elastic Stack Terraform provider</a> supports the GA Dashboards API.</p><h2>Note</h2><ol><li><p>The core endpoints are unchanged from the technical preview. If you built integrations against 9.4, they work in 9.5. The only breaking changes are two minor ones affecting dashboard listing and duration unit formats, documented <a href="https://www.elastic.co/docs/release-notes/kibana/breaking-changes">here</a>.</p></li></ol>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dashboards-as-code-kibana-api</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[Developer Experience]]></category>
    <category><![CDATA[Integrations]]></category>
    <dc:creator><![CDATA[Teresa Alvarez Soler]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ed7e33de291f255/6a730619c8b7ac02b251f9d3/image1.png" length="0" type="image/png"/>
    <pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Dependency management on Kubernetes]]></title>
    <description><![CDATA[How to streamline dependency management on Kubernetes using Renovate CLI and Argo Workflows.]]></description>
    <content:encoded><![CDATA[<p>This is how we built a self-hosted dependency management platform using Kubernetes, Argo Workflows, Argo Events, and Renovate CLI to automate updates, quickly address Common Vulnerabilities and Exposures (CVEs), and efficiently propagate new package versions across thousands of repositories.</p><h2><strong>Dependency management at Elastic</strong></h2><p>At Elastic, we have to manage hundreds or even thousands of repositories, both private and public. When a critical CVE is discovered, we need immediate answers and actions: Which repositories are vulnerable? How quickly can we patch them? Apart from security, productivity questions also arise: How can we quickly propagate the release of a new package version across all the repositories that depend on it without spending too much time on manual tasks?</p><p>The initial trigger for searching ways of doing dependency management was the need to establish a secure foundation with automated updates for <a href="https://www.elastic.co/blog/reducing-cves-in-elastic-container-images">reducing CVEs</a>. After carefully considering solutions on dependency management, we first started working on a self-hosted infrastructure. We were using our own Kubernetes cluster to run Mend Renovate Community Self-Hosted. The idea was to be able to provide a dependency management platform that our users could access in a self-service manner.</p><p>The initial experiment was successful, so more and more teams started onboarding our platform and using it in their everyday repositories’ lifecycle for updates and CVE patching. This happened so fast that we soon hit the ceiling of our self-hosted installation.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc99617fc3eed538d/6a170ea9964cea459d08bc67/e14d9f98d4eccaa08a335d5bd23d88e5debbb344-1600x1103.png" alt="Dependency management at Elastic" /><h3><strong>The challenge: How can we scale a dependency management platform in a large organization with a significant number of repositories?</strong></h3><p>Our dependency management platform was processing one repository at a time and the sequential processing model couldn’t keep up, due to the large number of repositories that we own. We had already identified that the issue resided within the concept that <strong>a single instance</strong> of our dependency management tool could process our big and ever-growing list of repositories. Repositories waited in a queue, sometimes for many hours. More than 50% of our repositories were not even processed daily. That means that more than 50% of our repositories waited more than 24 hours between scans.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d205fd379e3c07a/6a170eab961e691e1fc4cfca/45ade5bda08f82bed0b3d0d3736cbd6f056e7a4e-1312x816.jpg" alt="Dependency management problem" /><p>Large repositories created larger bottlenecks, due to their sizable codebases and their multiple open PRs. GitHub webhook events disrupted the sequence. Automerge became unreliable because scan timing was unpredictable. We had made a promise to our users for the frequency of scans, and we couldn’t fulfill it.</p><h3><strong>The decision to build in-house: Meeting Elastic's unique scale and security needs</strong></h3><p>While we considered commercial options, including <strong>Mend's Renovate Self-Hosted Enterprise Self-Hosted edition</strong>, internally at Elastic we had a few key initiatives ramping up.</p><p>Our decision to build an in-house platform was driven by the recognition that only a deeply customized solution could meet Elastic's specific, nonnegotiable requirements:</p><ol><li><p><strong>Investing in our internal developer platform:</strong> At the time, we had already started heavily investing in our internal developer platform. We were discussing and designing ways that each one of our services could fit into that. This meant that we wanted to test-drive our own rules and practices for our dependency management platform. On top of that, new guidelines were coming into play and we wanted to design the platform ahead of events.</p></li><li><p><strong>Native integration and workflow customization:</strong> We required straightforward integration with our internal tooling and internal processes. For example, we wanted to centralize configuration as code with our Service Catalog (Backstage). We have specific needs around the usage of Backstage that we wanted to make our platform compatible with. So, although it would be possible to make use of the Renovate Self-Hosted APIs alongside our Backstage automation, this wouldn’t cover completely for our internal processes.</p></li><li><p><strong>Elastic-specific defense-in-depth security:</strong> Our stringent security compliance required bespoke security mechanisms tailored to our ecosystem. We were working to <a href="https://entro.security/blog/how-elastic-scaled-secrets-nhi-security-elastics-playbook-from-visibility-to-automation/">harden our usage of “non-human identities.”</a> The way this hardening of access worked meant that the nonstandard means to authenticate to GitHub wouldn’t work with an off-the-shelf tool that didn’t support this internal implementation.Our workflow included implementing a parent-child workflow secret encryption pattern and using transient, single-use GitHub tokens. Building in-house was the only practical way to embed these unique security layers and minimize the attack surface across our complex multicloud environment.</p></li></ol><h2><strong>The solution: Workflow orchestration for dependency management</strong></h2><p>Our solution started from the fact that we wanted to build on the dependency management tool that we already used and not replace it and look for other solutions. It had shown signs of its potential, and its flexibility is important for different needs throughout our organization. We considered different solutions, and what helped us make up our minds was the big and sometimes special needs that we have to cover for. We decided to build a reliable and scalable dependency management platform, where each repository will be processed on its own, removing bottlenecks and setting us up for growth.</p><p>We designed the platform abiding to three core principles:</p><h3><strong>1. Parallel processing</strong></h3><p>Every repository gets its own dependency management processing environment. No more queues. Our concurrency is only limited by the number of resources we spend. We have also applied smart distributed scheduling to avoid getting rate limited by GitHub.</p><h3><strong>2. Self-serviceable</strong></h3><p>We use our Service Catalog (Backstage) to automatically onboard and manage any new repository. We use our own resource definition to give the end user the option to select how often a repository will be processed, how many resources they want to allocate to their schedules, and if they want to turn processing off or back on for any reason. We plan to add more options that way as our users’ needs evolve and they get more fluent with the new installation.</p><h3><strong>3. Reduced secret scope and namespace isolation</strong></h3><p>For increased security, we supply our dependency management pods with ephemeral GitHub tokens that are being generated at the start of each workflow. On top of that, we isolate our workloads in specific namespaces so they can be provided only the necessary secrets. We control what secrets can be accessed by each dependency management workflow using Kubernetes RBAC. We also use encryption to propagate the GitHub token from the parent to the child workflows.</p><p>We rebuilt our platform using Kubernetes and harnessing the power of Kubernetes, Argo Workflows powers the logic of our processes, and Renovate CLI is set up for scanning and processing one repository at a time.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3548539ab52fbb79/6a170eac0c48573da601ab26/5560ed20e2bd9ecdd574a9c835126d12b24c332f-1600x1157.png" alt="Overview of dependency management workflows in Kubernetes" /><p><strong>The beauty:</strong> We’re using battle-tested open source projects in an original way, providing new working examples for all of those projects and, at the same time, amplifying development velocity and consolidating CVE reduction for our teams.</p><h2><strong>Dependency management architecture: Four microservices</strong></h2><p>The platform comprises four custom-built components:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6451ff19da4db511/6a170eaec1e8a562e0f88378/2b3d4046c05bb261e45d40c59f864eb51fb9eaa9-1217x1600.png" alt="Components for dependency management in Kubernetes" /><h3><strong>Workflows Operator (Go/Kubebuilder)</strong></h3><p>A Kubernetes operator managing workflow lifecycle through three Custom Resource Definitions (CRDs):</p><ul><li><p><strong>RepoConfig CRD:</strong> Single source of truth for repository configuration.</p></li></ul><p>This is how RepoConfig is defined in the operator:</p>// RepoConfig is the Schema for the repoconfigs API
type RepoConfig struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty,omitzero"`

	// spec defines the desired state of RepoConfig
	// +required
	Spec RepoConfigSpec `json:"spec"`

	// status defines the observed state of RepoConfig
	// +optional
	Status RepoConfigStatus `json:"status,omitempty,omitzero"`
}<p>And this is what an instance of RepoConfig would look like:</p>apiVersion: workflows.elastic.co/v1
kind: RepoConfig
metadata:
  generation: 3
  name: elastic-test-repo
  namespace: dependency-management-operator
spec:
  owner: group:my-team
  renovate:
    config:
      resourceGroup: SMALL
      runFrequency: 4h
    enabled: true
  repository: elastic/test-repo<ul><li><p><strong>Parent CRD:</strong> Manages CronWorkflows for scheduled scans.</p></li></ul><p>Inside the reconciliation loop of the parent controller, we make sure that workflow settings are created and kept up to date or even deleted if needed.</p><p>First, it gets some globally configured settings for workflows:</p>func (r *ParentReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, parent *workflowsv1.Parent) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Parent", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		RunFrequency:   parent.Spec.RunFrequency,
		ResourceGroups: "parent",
	}<p>It makes sure a mutex configmap is up to date to prevent similar workflows from running together:</p>	cfMngr := resources.NewConfigMapManager(r.Client, r.Scheme, r.OperatorConfig.ParentNamespace)
	err := cfMngr.CreateOrUpdateSyncMutexConfigmap(ctx, fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), strings.TrimPrefix(parent.Spec.Repository, "elastic/"), r.OperatorConfig.SemaphoreConcurrencyLimit)<p>Then it creates a Workflow Manager that’s the struct which will create or update the CronWorkflows and the Workflow Templates:</p>	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(parent.Spec.Repository, "/", "-"),
		),
		parent.Namespace,
		"parent-workflow",
		false).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithRepoName(parent.Spec.Repository).
		Init(true, true).
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithWfTemplateName(r.OperatorConfig.ParentWorkflowTemplate).
		WithResources(wfSet.GetResourceCategory()).
		WithSchedule(wfSet.GetCronSchedule()).
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddArgument(true, true, "extra_cli_args").
		SetArgument(true, false, "extra_cli_args", "none").
		AddTemplate(resources.NewParentDAGTemplateInstance()).
		AddTemplate(resources.NewWorkflowsTemplateInstance("check-child-workflows", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("security", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddTemplate(resources.NewWorkflowsTemplateInstance("submit-child-workflow", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector))
	wfMngr.OverWriteCommand("submit-child-workflow", r.OperatorConfig.ChildNamespace)
	wfMngr.OverwriteWfTemplateName("parent-wftmpl")
	wfMngr.AddSynchronization(fmt.Sprintf("%s%s", r.OperatorConfig.ResourcesPrefix, r.OperatorConfig.SyncMutexCfgMapName), "{{workflow.parameters.repo_name}}")
	err = wfMngr.CreateOrUpdateCronWorkflow(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update cron workflow: %w", err)
	}
	err = wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil<ul><li><p><strong>Child CRD:</strong> Manages WorkflowTemplates with per-repository resources.</p></li></ul><p>The child controller has a similar reconciliation duty to the parent, but this time it’s responsible for workflow templates in the child namespace that will be triggered by the parent workflows.</p>func (r *ChildReconciler) reconcileSubResources(ctx context.Context, req ctrl.Request, child *workflowsv1.Child) error {
	logger := logf.FromContext(ctx)
	logger.Info("Reconcile SubResources for Child", "name", req.NamespacedName)
	wfSet := workflowsettings.WorkflowSettings{
		ResourceGroups: child.Spec.ResourceCategory,
	}
	wfMngr := resources.NewArgoWorkflowManager(r.Client,
		r.Scheme,
		curateResourceName(
			strings.ReplaceAll(child.Spec.Repository, "/", "-"),
		),
		child.Namespace,
		"runner",
		true).
		Init(false, true). // only manage workflow template
		WithPrefix(r.OperatorConfig.ResourcesPrefix).
		WithSuffix("-child-wftmpl").
		WithRepoName(child.Spec.Repository).
		WithOrganization(r.OperatorConfig.GitHubOrg).
		WithResources(wfSet.GetResourceCategory()). // will override resources of presets if set
		WithImagePullSecrets([]corev1.LocalObjectReference{{
			Name: r.OperatorConfig.WorkflowImagePullSecrets,
		}}).
		AddTemplate(resources.NewWorkflowsTemplateInstance("runner", r.OperatorConfig.WorkflowImagePullPolicy, r.OperatorConfig.WorkflowNodeSelector)).
		AddArgument(false, true, "repo_full_name").
		AddArgument(false, true, "repo_name").
		AddArgument(false, true, "encrypted_token").
		AddArgument(false, true, "extra_cli_args")
	wfMngr.OverWriteCommand("runner", r.OperatorConfig.ChildNamespace)
	err := wfMngr.CreateOrUpdateWorkflowTemplate(ctx)
	if err != nil {
		return fmt.Errorf("failed to create or update workflow template: %w", err)
	}
	return nil
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta735156ba6e370ef/6a170eaf7d8d6706fd70e7e4/7ac70492a1266ba02cb8afbafc5a486cb38a0edc-1600x1290.png" alt="Workflows for dependency management in Kubernetes" /><p>The multi-controller pattern provides clear separation: RepoConfig Controller handles onboarding/offboarding, Parent Controller manages scheduling, and Child Controller handles execution templates.</p><h3><strong>GitHub Events Gateway (Go)</strong></h3><p>A secure webhook proxy that receives GitHub webhooks, verifies signatures, filters by organization/repository, and routes to Argo Events. We built 10 distinct sensors responding to dependency dashboard interactions, PR events, and package updates.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e748ceb93c13a5a/6a170eb1a6c2b908f8e797b4/4828625456cbd6efa8020a20f10d23f294f98a02-1306x1600.png" alt="dependency dashboard interactions on Kubernetes" /><p>This gateway enables integration with GitHub Apps by:</p><ul><li><p>Verifying incoming GitHub webhook signatures for security.</p></li><li><p>Forwarding valid events to the Argo Events EventSource with all relevant headers and authentication.</p></li><li><p>We also configure an authSecret on the EventSource and provide this as a Bearer header in forwarded requests.</p></li><li><p>Providing logging, metrics, and retry logic.</p></li></ul><p>It performs various validations on each GitHub Event request.</p><p>It makes sure some HTTP attributes are present:</p>// ValidateRequestMethod checks if the request method is POST.
func ValidateRequestMethod(r *http.Request) error {
	if r.Method != http.MethodPost {
		return fmt.Errorf("method not allowed, only POST is accepted")
	}
	return nil
}

// ValidateRequiredHeaders checks for required GitHub headers.
func ValidateRequiredHeaders(r *http.Request) error {
	eventType := r.Header.Get("X-GitHub-Event")
	deliveryID := r.Header.Get("X-GitHub-Delivery")
	signature := r.Header.Get("X-Hub-Signature-256")
	if eventType == "" || deliveryID == "" || signature == "" {
		return fmt.Errorf("missing required GitHub headers")
	}
	return nil
}

// ValidateUserAgent checks that the User-Agent header starts with GitHub-Hookshot/
func ValidateUserAgent(r *http.Request) error {
	userAgent := r.Header.Get("User-Agent")
	if !strings.HasPrefix(userAgent, "GitHub-Hookshot/") {
		return fmt.Errorf("invalid User-Agent")
	}
	return nil
}<p>While it also validates the signature of each request and its organizsation:.</p>// ValidateSignature verifies the GitHub webhook signature.
func ValidateSignature(r *http.Request, secret string) ([]byte, error) {
	payload, err := GitHub.ValidatePayload(r, []byte(secret))
	if err != nil {
		return nil, fmt.Errorf("invalid GitHub signature: %w", err)
	}
	return payload, nil
}

// ValidateAllowedOwner checks if the organization login is in the allowed organizations list.
func ValidateAllowedOwner(payload []byte, allowedGitHubOrganizations []string) (string, error) {
	var orgLogin string
	var payloadMap map[string]any
	if err := json.Unmarshal(payload, &amp;payloadMap); err == nil {
		if orgObj, ok := payloadMap["organization"].(map[string]any); ok {
			if login, ok := orgObj["login"].(string); ok {
				orgLogin = login
			} else if name, ok := orgObj["name"].(string); ok {
				orgLogin = name
			}
		}
	}
	if !slices.Contains(allowedGitHubOrganizations, orgLogin) {
		return orgLogin, fmt.Errorf("organization login not allowed")
	}
	return orgLogin, nil
}<p>Finally, it routes to Argo Events based on event type:</p>	// Map eventType to Argo `EventSource` path
	var endpoint string
	switch eventType {
	case "push":
		endpoint = "/push"
	case "issues":
		endpoint = "/issues"
	case "pull_request":
		endpoint = "/pull-requests"
	default:
		slog.Info("Ignoring unhandled event type", "event_type", eventType, "delivery_id", deliveryID)
		w.WriteHeader(http.StatusOK)
		_,  = w.Write([]byte("ok"))
		return
	}
	forwardURL := h.config.ArgoEventSourceForwardURL + endpoint<p>On the Argo Events side of things, 10 sensors watch the Argo Events EventBus for new events:.</p>apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: {{ .Values.sensors.packageUpdateOnDefaultBranch.name }}
  namespace: {{ .Release.Namespace }}
spec:
  eventBusName: {{ .Values.eventBus.name }}<p>Then the script applies each sensor’s logic:</p>script: |
          local e = event
          if not e or not e.body or not e.body.repository then
            return false
          end

          -- e.g., "refs/heads/main"
          local ref = e.body.ref
          local default_branch = e.body.repository.default_branch
          if not ref or not default_branch then
            return false
          end

          local expected = "refs/heads/" .. default_branch
          if ref ~= expected then
            return false
          end

        {{- if .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}
          patterns = { {{- range $i, $f := .Values.sensors.packageUpdateOnDefaultBranch.packageFiles }}{{ if $i }}, {{ end }}"{{ $f }}"{{- end }} }
        {{- end }}

          local function anyMatch(path)
            if type(path) ~= "string" then return false end
            for _, pat in ipairs(patterns) do
              -- match filename at repo root, or anywhere under subdirs
              if path:match(pat) or path:match(".+/" .. pat) then
                return true
              end
            end
            return false
          end

          local function filesContainPackage(paths)
            if type(paths) ~= "table" then return false end
            for _, p in ipairs(paths) do
              if anyMatch(p) then return true end
            end
            return false
          end

          -- Inspect all commits (GitHub includes added/modified/removed lists)
          local commits = e.body.commits
          if type(commits) ~= "table" then
            -- Fallback: some payloads include only head_commit
            commits = {}
            if type(e.body.head_commit) == "table" then
              table.insert(commits, e.body.head_commit)
            end
          end

          for _, c in ipairs(commits) do
            if filesContainPackage(c.added) or filesContainPackage(c.modified) or filesContainPackage(c.removed) then
              return true
            end
          end

          return false<h3><strong>Backstage Syncer (Go)</strong></h3><p>This polls our Service Catalog (Backstage) for Repository Real Resource Entities, transforms them into RepoConfig CRDs, and keeps the platform in sync with configuration changes. Changes apply within three minutes.</p>repoMap := make(map[string]map[string]interface{})
			for i := range entities {
				entity := &amp;entities[i]
				if entity.Spec.Type != "GitHub-repository" {
					continue
				}

				implRaw, err := json.Marshal(entity.Spec.Implementation)
				if err != nil {
					logger.Error("Failed to marshal implementation", "error", err)
					continue
				}

				var implMap map[string]interface{}
				err = json.Unmarshal(implRaw, &amp;implMap)
				if err != nil {
					logger.Error("Failed to unmarshal implementation map", "error", err)
					continue
				}
				var repoName string
				if specMap, ok := implMap["spec"].(map[string]interface{}); ok {
					if repo, ok := specMap["repository"].(string); ok {
						repoName = repo
					}
				}
				if repoName == "" {
					continue
				}

				var workflowsRaw []byte
				if v, ok := implMap["spec"].(map[string]interface{}); ok {
					if r, ok := v["renovate"]; ok {
						workflowsRaw,  = json.Marshal(r)
					} else {
						workflowsRaw = []byte(`{}`)
					}
				} else {
					workflowsRaw = []byte(`{}`)
				}

				var workflowsWithDefaults schema.WorkflowsMetadata
				err = json.Unmarshal(workflowsRaw, &amp;rworkflowsWithDefaults)
				if err != nil {
					logger.Error("Failed to unmarshal workflows config", "error", err)
					continue
				}

				workflowsMap := map[string]interface{}{
					"enabled":        workflowsWithDefaults.Enabled,
					"require_pr":     workflowsWithDefaults.RequirePr,
					"resource_group": string(workflowsWithDefaults.ResourceGroup),
					"run_frequency":  string(workflowsWithDefaults.RunFrequency),
				}
				repoMap[repoName] = map[string]interface{}{
					"renovate": workflowsMap,
					"owner":    entity.Spec.Owner,
				}
			}
			logger.Info("Fetched GitHub Repository data from Backstage", "repository_count", len(repoMap), "status_code", resp.StatusCode)<p>Finally, it writes that data into RepoConfig instances.</p><h3><strong>Workflows base (Mixed: JavaScript, Go, Helm)</strong></h3><p>The foundation layer contains Helm charts, JavaScript configs, a Go wrapper for Renovate CLI with encryption support, and a custom APK Indexer for Alpine packages.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4c1b5b840854ddf5/6a170eb47d8d67694e70e7e8/908d19278face3ce1119dbee9146c1264b6e2f30-1600x873.png" alt=" Foundational components for dependency management on Kubernetes" /><h2><strong>Self-service configuration</strong></h2><p>Teams configure their repositories declaratively through Backstage:</p>spec:
  renovate:
    enabled: true
    config:
      resourceGroup: LARGE      # SMALL | MEDIUM | LARGE  
      runFrequency: "0 */4 * * *"  # Every 4 hours<p>Resource groups allocate CPU and memory based on repository size:</p><ul><li><p><strong>SMALL:</strong> 500m CPU, 1Gi memory.</p></li><li><p><strong>MEDIUM:</strong> 1000m CPU, 2Gi memory.</p></li><li><p><strong>LARGE:</strong> 2000m CPU, 4Gi memory.</p></li></ul><p>Configuration is version-controlled, auditable, and applies automatically.</p><h2><strong>The parent-child pattern</strong></h2><p>The execution model uses a parent-child workflow pattern:</p><ul><li><p><strong>Parent workflow:</strong> Lightweight CronWorkflow running on schedule. Encrypts secrets, determines whether a scan should run, passes configuration to the child.</p></li><li><p><strong>Child workflow:</strong> Ephemeral pod where Renovate CLI runs. Allocated resources dynamically, decrypts secrets in isolation, terminates after completion.</p></li></ul><p>This separation provides security (secrets encrypted at parent level), resource optimization (parents use minimal resources), and scalability (children run in parallel).</p><h2><strong>The results</strong></h2><h3><strong>Performance transformation</strong></h3><ul><li><p><strong>Before:</strong> One repository at a time, some repositories would not get processed possibly even for a day or more, less than 1,000 scans per day.</p></li><li><p><strong>After:</strong> 100+ concurrent scans, usually 8,000 scans and up to 10,000 recorded scans per day, limited only by the amount of resources we’re willing to spend and how we handle GitHub rate limits.</p></li></ul><h3><strong>Cost efficiency</strong></h3><p>However weird it may sound, running 8,000 pods a day can get you the same result much cheaper than having one long-running pod trying to achieve the same results.</p><p>In the previous setup, we were running a single instance that, on a good day, would perform 500–600 scans. At the same time, due to the fact that different kinds of repositories would be executed on the same pod, we needed to size the pod for the biggest ones. That sizing would be much bigger than our current extra large offering, using 8 CPUs for the pod and 16G of memory.</p><p>To meet the current daily output, the single pod would need to run for 12 days. So comparing the cost of that single pod running for 12 days to 8,000 pods of our “MEDIUM” size running each day, our new design is far more efficient for the same output of scans:</p><p>Metric</p><p>Scenario A (Workflows)</p><p>Scenario B (The long-running single pod)</p><p>Setup</p><p>8,000 pods (1 vCPU / 2GB)</p><p>1 pod (8 vCPU / 16 GB)*</p><p>Duration</p><p>10 minutes each</p><p>12 days continuous</p><p>Total work time</p><p>1,333 compute hours</p><p>288 compute hours</p><p>Total cost</p><p>$65.83</p><p>$113.75</p><p>However, let’s take into consideration that our default for our workloads is set to “SMALL,” with the great majority running successfully with 0.5 CPU and 1G RAM, and only a few need to change to medium, large. Let’s see what happens if 60% of our workloads are running on “SMALL,” 30% at “MEDIUM,” and 10% at “LARGE,” which is closer to the truth.</p><p>Metric</p><p>Scenario A (Mixed swarm)</p><p>Scenario B (The long runner)</p><p>Strategy</p><p>8,000 pods (mixed sizes)</p><p>1 pod (8 vCPU / 16 GB)*</p><p>Duration</p><p>10 minutes each</p><p>12 days continuous</p><p>Total cost</p><p>$52.66</p><p>$113.75</p><p>Savings</p><p>$61.09 (54% cheaper)</p><p>—</p><p>We can see that, for the same output, we’re far more cost-efficient in our current setup.</p><h3><strong>Enhanced security</strong></h3><ul><li><p>Ephemeral GitHub tokens (minutes of exposure versus days).</p></li><li><p>Namespace isolation with Role-Based Access Control (RBAC) boundaries.</p></li><li><p>Secret encryption at rest in parent workflows.</p></li><li><p>Removed direct vault access.</p></li></ul><h3><strong>Predictable performance</strong></h3><p>With guaranteed scan frequency, we can finally set Service Level Objectives (SLOs). Automerge works reliably. Teams trust the platform to deliver what’s promised.</p><h2><strong>Key architectural decisions</strong></h2><p>Here are some of the milestone design decisions that shaped how the platform looks.</p><ul><li><p><strong>Why parent-child workflows?</strong></p></li></ul><p>We adopted this pattern to enforce a <strong>defense-in-depth</strong> strategy. By restricting high-value credentials (such as GitHub App secrets) to a dedicated, locked-down namespace, we use <strong>RBAC</strong> to ensure that ephemeral execution pods cannot arbitrarily access sensitive data. Recent supply chain vulnerabilities (for example, the <strong>"Shai Hulud"</strong> continuous integration/continuous delivery [CI/CD] attacks) have demonstrated the criticality of isolating runtime environments that execute dynamic scripts from the credential store.</p><p>Simultaneously, this decoupling enables <strong>granular resource optimization</strong>. The "parent" workflows act as lightweight orchestrators with a minimal footprint, while the "child" workflows handle the compute-intensive dependency scanning. This separation simplifies <strong>lifecycle management</strong> by allowing us to apply distinct reconciliation logic to each layer, granting users control over execution parameters (child) while retaining administrative control over the scheduling and security infrastructure (parent).</p><ul><li><p><strong>Why self-serviceable?</strong></p></li></ul><p>Eliminating our team as a bottleneck for repository configuration was a critical requirement. Our mission was to architect a scalable, <strong>self-service platform</strong> capable of supporting diverse use cases. We recognized that acting as <strong>gatekeepers</strong> for every configuration change was unsustainable, given the sheer volume of repositories. Instead, we adopted a philosophy of enablement: providing the “rails” (infrastructure and <strong>guardrails</strong>) while empowering users to drive the “trains” (execution and customization). We believe this shift toward <strong>team autonomy</strong> significantly enhances productivity by allowing users to tailor the system to their specific operational needs.</p><ul><li><p><strong>Why Kubernetes Operator pattern?</strong></p></li></ul><p>As mentioned above, a foundational design principle was to ensure that the platform was fully <strong>self-serviceable</strong>. We required an automated mechanism to capture user intent (such as toggling scans, adjusting scheduling frequency, or tuning runtime resource limits) and instantly propagate those changes to the underlying workflows. Anticipating future requirements, the system also needed to be easily <strong>extensible</strong>.</p><p>To achieve this, we developed a custom <strong>Dependency Management Kubernetes Operator</strong>. By using <strong>CRDs</strong> as the interface for configuration, we established a <strong>Kubernetes-native reconciliation loop</strong>. This operator continuously monitors the desired state defined by the user and automatically orchestrates the necessary updates to the workflow infrastructure. This ensures an <strong>event-driven</strong>, seamless operation, where the platform logic handles all complexity behind the scenes.</p><ul><li><p><strong>Why design a GitHub Events Gateway?</strong></p></li></ul><p>Adopting an <strong>event-driven architecture (EDA)</strong> was essential for the platform's responsiveness. While CronWorkflows provided a reliable baseline schedule, we required the agility to handle <strong>ad hoc executions, </strong>such as users manually triggering scans via the dashboard. To achieve this, we needed a dedicated <strong>ingestion gateway</strong> to validate payload integrity and route requests intelligently.</p><p>We evaluated existing solutions, including the native GitHub EventSource for Argo, but we identified significant risks regarding <strong>operational overhead</strong> and strict <strong>GitHub API quotas</strong> (for example, webhook limits per repository). Consequently, we built a custom gateway to decouple our infrastructure from these limitations.</p><p>Crucially, this gateway served as a strategic <strong>traffic control point</strong> during our migration. It acted as a switch, enabling us to perform a <strong>gradual, granular rollout</strong> (traffic shifting) from the legacy system to the new infrastructure. This ensured that onboarding thousands of repositories was a controlled, risk-free process rather than a “big bang” switchover.</p><p></p><h2><strong>Lessons learned</strong></h2><p>Some lessons that we learned go hand-in-hand with the <a href="https://www.elastic.co/about/our-source-code">Elastic Source Code</a>:</p><ol><li><p><strong>Customer First: </strong>Platforms are built for users. So it’s important to take users’ needs as priority number one. This shapes the platform into efficiently designed infrastructure and applications that reduce friction with users, simplify the scaling of the platform and ease adoption.</p></li><li><p><strong>Space, Time: </strong>Sometimes the path of least resistance leads to <strong>shifting sands</strong>. We initially tried to optimize the existing sequential processing model, but this failed to resolve our issues; in fact, it only introduced more complexity and loose ends. The bold decision to <strong>rearchitect</strong> the platform with parallel processing required significant up-front effort. However, it ultimately paved the way for sustainable platform growth and virtually eliminated tedious daily administrative work.</p></li><li><p><strong>IT, Depends: </strong>A platform cannot operate in isolation; its success depends on how well it integrates with the broader ecosystem. In our case, integration with <strong>Backstage</strong> was critical, as it serves as the source of truth for seamless service onboarding. Similarly, connecting to <strong>Artifactory</strong> allowed us to manage private package updates efficiently, and the list of essential integrations goes on.</p></li><li><p><strong>Progress, SIMPLE Perfection: </strong>Throughout the implementation, we constantly pressure-tested our initial assumptions and adapted to new barriers as they emerged. Rather than getting paralyzed by perfectionism, we adopted an <strong>iterative approach</strong>, tackling challenges one by one and adjusting our migration strategy to meet real-world conditions.</p></li></ol><h2><strong>What’s next</strong></h2><p>The delivery of the platform enables us for more meaningful work that will help us improve the UX and efficiency of our platform. Some examples are:
</p><ul><li><p><strong>Increase and guardrail the adoption of auto-merge</strong></p></li></ul><p>The auto-merge feature significantly accelerates team velocity by eliminating tedious manual tasks. However, we need to make sure that strict <strong>guardrails</strong> are in place to ensure that this increased speed does not come at the expense of security.
</p><ul><li><p><strong>Improve observability around end-user experience</strong></p></li></ul><p>A critical priority for our roadmap is enhancing observability, not just at the platform level but also specifically from the <strong>end-user’s perspective</strong>. While capturing infrastructure metrics is straightforward, understanding the actual user experience requires deeper insights. We’re working to define core user-centric key performance indicators (KPIs) so our telemetry can detect friction points and performance issues <strong>before</strong> they escalate into user complaints.</p><ul><li><p><strong>Remove barriers for greater adoption</strong></p></li></ul><p>Looking ahead, our priority is to identify and remove any barriers hindering platform adoption. Whether this requires developing new integrations or deploying specific feature sets, we’re committed to data-driven planning. We’ve successfully built a platform designed for scale; our focus now shifts to <strong>maximizing its potential</strong>.
</p><h2><strong>The bigger picture</strong></h2><p>The dependency management workflows project demonstrates a broader principle: <strong>When you need to scale open source tools beyond their default deployment model, Kubernetes-native patterns provide a path forward</strong>.</p><p>By embracing:</p><ul><li><p>CRDs for configuration.</p></li><li><p>Operators for lifecycle management.</p></li><li><p>Event-driven architecture for responsiveness</p></li><li><p>GitOps for deployment.</p></li></ul><p>We built orchestration that scales independently of the number of repositories it manages. The performance of scanning one repository is the same whether we’re managing 100 or 1,000.</p><p>When a critical CVE is announced, we now have answers in minutes, not hours. That’s the difference between a bottleneck and a competitive advantage.</p><h2><strong>Acknowledgments</strong></h2><p>This platform builds on excellent open source tools:</p><ul><li><p><strong>Kubebuilder:</strong> The open source framework we used to kick-start our Kubernetes Operators that bootstrap and orchestrate our workflows. [<a href="https://github.com/kubernetes-sigs/kubebuilder">1</a>][<a href="https://book.kubebuilder.io/">2</a>]</p></li><li><p><strong>Backstage:</strong> The open source framework on which we’ve built our Service Catalog and which we use as our source of truth. [<a href="https://github.com/backstage/backstage">1</a>][<a href="https://backstage.io/">2</a>]</p></li><li><p><strong>Argo Workflows and Argo Events:</strong> The open source suite we used to orchestrate complex processes and add dynamic processing based on events. [<a href="https://github.com/argoproj/argo-workflows">1</a>][<a href="https://argo-workflows.readthedocs.io/en/stable/">2</a>][<a href="https://argoproj.github.io/argo-events/">3</a>][<a href="https://github.com/argoproj/argo-events">4</a>]</p></li><li><p><strong>Renovate CLI:</strong> The open source dependency management tool processing our repositories. [<a href="https://github.com/renovatebot/renovate">1</a>][<a href="https://docs.renovatebot.com/getting-started/running/">2</a>]</p></li></ul><p>* The AWS Fargate pricing model was used as a reference for of the cost of a single pod, although our workloads are not running necessarily on AWS and are running on full- blown Kubernetes clusters.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/dependency-management-kubernetes</guid>
    <category><![CDATA[Developer Experience]]></category>
    <dc:creator><![CDATA[Nikos Fotiou]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6033995d6660d149/6a170eb5839dfa63f1dcff9b/00519840e6eec7101c1fb096afcae976ee0c454e-1280x720.png" length="0" type="image/png"/>
    <pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Cookbook for a production-grade generative AI sandbox]]></title>
    <description><![CDATA[Exploring the recipe for a generative AI sandbox, giving developers a secure environment to deploy application prototypes while enabling privacy and innovation.]]></description>
    <content:encoded><![CDATA[<p>Building generative AI (GenAI) applications is all the rage, and c<em>ontext engineering</em>, that is, providing the prompt structure and data needed for a large language model (LLM) to return specific, relevant answers to a question without filling in the blanks itself, is one of the most popular patterns that has emerged in the past 24 months. One particular subset of context engineering, retrieval-augmented generation (RAG), is being used widely to bring additional context to LLM interactions by using the power of natural language-based search to surface the most relevant results in private datasets based on meaning rather than on keywords.</p><p>As context engineering is exploding, ensuring that rapid prototype projects don’t expose business- or mission-critical data to unauthorized recipients is a significant concern. For audiences interested in technology and policy alike, I've championed the concept of a <em>privacy-first GenAI sandbox</em>, which I’ll simply refer to as a sandbox from here on. In this article, the term <em>sandbox</em> refers to a self-service, secure prototyping space (much like a child's sandbox, where the wooden edges prevent sand from escaping), allowing organization members to test their custom context engineering applications safely, without risking exposure of confidential data.</p><h2>Production-grade GenAI sandboxes = enabling privacy <em>and</em> innovation</h2><p>GenAI, from text-generating tools, like ChatGPT, Claude, and Gemini, to image creators, such as Google’s Nano Banana, OpenAI’s DALL-E, and Midjourney, has sparked discussions everywhere: in classrooms, at dinner tables, in regulatory circles, in courts, and in boardrooms over the past two years.</p><p>I’ve had the privilege of sharing Elastic’s approach to context engineering, and particularly RAG, with customers, including developers and C-suite executives, and with contacts of mine, ranging from friends and family to legislators. Think of context engineering as a librarian that looks up and then serves contextual data to augment text, audio, or image GenAI apps that they don’t have in the data they were trained on for their intended tasks; for example, looking up sports scores and headlines to help a text-generation application answer the question, “What happened in the National Hockey League yesterday?”</p><p>Elasticsearch Labs has fabulous primers on context engineering <a href="https://www.elastic.co/search-labs/blog/context-engineering-overview">here</a> and RAG <a href="https://www.elastic.co/search-labs/blog/articles/retrieval-augmented-generation-rag">here</a>, if you’re unfamiliar with the concept and would like to do more reading.</p><p>A <em>privacy first</em> approach ensures that context engineering supplies the GenAI app with protected, selected, or delicate data, fostering responses that are better informed and more pertinent than what might be generated using solely public information. An example of this would be providing a GenAI-powered, interactive text chat experience (chatbot) for university students to obtain financial aid and scholarship information relevant to their personal background, without risking exposure of personally identifiable information (PII), such as their Social Security number or birthdate, to malicious actors extracting information via common vulnerabilities, as per the <a href="https://genai.owasp.org/resource/owasp-top-10-for-llm-applications-2025/">OWASP Top 10</a>, or the LLM itself.</p><p></p><p>The core tenets of the logic behind deploying a sandbox are as follows:</p><ol><li><p>Users will find a way to incorporate GenAI into their daily workflow, regardless of whether one’s organization provides the tools. Even in organizations where preventing such “shadow IT” is, realistically, impractical or impossible, providing and monitoring access to prevent disclosure of an organization’s sensitive data still remains imperative; a sandbox is just the place to turn such tools loose.</p></li><li><p>Providing a sandbox to deploy applications with Application Performance Monitoring (APM) and information security (InfoSec) best practices embedded allows an organization to derive insights into potential use cases for GenAI while also safeguarding privacy, enabling audit and accountability of GenAI use, and establishing centralized cost management.</p></li><li><p>An organization’s sandbox should allow either self-service or low-touch deployment of peer-reviewed GenAI applications to permit maximum experimentation with minimum friction by those inclined to develop their own applications. </p></li><li><p>If properly implemented and contained within the organization’s controlled perimeter, the sandbox allows leveraging data assets available to the organization without triggering the liabilities that could attach to unauthorized or unintended external sharing or other leakage of protected data such as PII – think California CCPA, or the EU/UK GDPR for instance.</p></li></ol><p>This article will not focus on building a GenAI app; there are numerous excellent examples here on Elasticsearch Labs. Instead, I’ll be focusing on the <em>recipe</em> necessary for deploying a sandbox that provides the security and availability needed to implement principle #3 above.</p><h3>Foundational ingredients</h3><p>For a sandbox to be considered <em>production grade</em>, the following foundational ingredients should be considered:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt220f947ab7e0786e/6a170c62a929cfb7bbae0a0a/c53aaa04503baf654ccee274f012d7c1ddc2f643-1380x1600.png" alt="Generative AI (GenAI) sandbox foundation" /><p>Let's explore why each ingredient plays a crucial role in our sandbox recipe. As we do, please note that brand-name decisions I’ve listed below are based on personal experience and aren’t an endorsement of one technology or another by Elastic. As with any recipe, these then form my preferred ingredients. You can, of course, substitute in each area to make the recipe to your liking:</p><h4>1. Containerization platform</h4><p>The first ingredient in our sandbox recipe is the selection of a <strong>containerization platform</strong>. These platforms, while conceptually similar to the virtual machines that have been a staple of enterprise IT for the past 15+ years, represent a significant evolution in how applications are packaged and deployed. They’re designed for rapid deployment, upgrades without service disruption, and native distribution across both on-premises and cloud computing environments, while also providing increased testability, validation of infrastructure, and auditability. The platform you choose, often managed through <strong>infrastructure as code</strong> (IaC) to ensure reproducibility and consistency, is the foundation that enables agility and scalability for your GenAI applications.</p><p>Key components of a containerization platform</p><p>A robust containerization platform is built on several key components:</p><ul><li><p><strong>Container runtime:</strong> The software that executes containers and manages their lifecycle. A popular example is <strong>Docker</strong>, which provides the tools to build, share, and run container images.</p></li><li><p><strong>Image build infrastructure:</strong> This is the process and tooling used to create container images from your application's source code. Tools like <strong>Dockerfiles</strong> provide a clear, repeatable way to define the environment, dependencies, and application code within an image, ensuring consistency across development, testing, and production environments.</p></li><li><p><strong>Orchestration engine:</strong> For a production-grade environment, you need a system to automate the deployment, scaling, and management of containers. <strong>Kubernetes</strong> (k8s) is the industry-standard for this, providing powerful features for load balancing, self-healing, and service discovery. More on that below in ingredient #2.</p></li></ul><p><strong>1. 1 Infrastructure as code:</strong></p><p>To ensure the reproducibility and maintainability of your sandbox, a <strong>containerization platform</strong> should be managed using <strong>IaC</strong> principles. This means that instead of manually configuring your platform, you define your infrastructure (for example, Kubernetes clusters, networking rules, security policies) in code files (for example, using <strong>Terraform</strong> or <strong>Pulumi</strong>). This approach provides several benefits:</p><ul><li><p><strong>Version control:</strong> Your infrastructure can be treated like any other code, allowing you to track changes, revert to previous versions, and collaborate with your team using Git.</p></li><li><p><strong>Consistency:</strong> IaC dramatically reduces manual errors and ensures that your sandbox environment can be recreated identically in any cloud or on-premises location.</p></li><li><p><strong>Automation:</strong> It enables you to automate the entire setup and teardown process, making it easy to create temporary sandboxes for specific projects or testing.</p></li></ul><h4>2. Hosting and orchestration</h4><p>As we introduced in the "Containerization platform" section, a powerful orchestration engine is needed to manage our containers at scale. For this, k8s is the de facto standard for orchestrating a production-grade sandbox. If you’re unfamiliar, check out the Cloud Native Computing Foundation (CNCF) primer on k8s available <a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/">here</a>. Whether running in the cloud or on-premises, Kubernetes provides the robust framework needed to deploy, scale, and manage the lifecycle of containerized applications. Major cloud providers, like Google Cloud (Google Kubernetes Engine [GKE]), Amazon Web Services (Elastic Kubernetes Service [EKS]), and Microsoft Azure (Azure Kubernetes Service [AKS]), all offer mature, managed Kubernetes services that handle the underlying complexity, including in particular contractually assured and independently certified compliance with statutory privacy and information security mandates, allowing your teams to focus on building and deploying applications.</p><p>For a GenAI sandbox, Kubernetes is particularly valuable because it can efficiently manage and scale GPU resources, which are often necessary for two key components of the GenAI stack: 1) privately hosted LLMs; and 2) the inference processes that power them (discussed in more detail in ingredients #6 and #7). Its ability to automate deployments and manage resources ensures that rapid prototypers can experiment with different models and applications without needing to become infrastructure experts, all within the secure and isolated area, called a <em>namespace</em> in k8s, that you define. This abstraction is key to the sandbox's success, empowering innovation while maintaining centralized control.</p><h4>3. Code repository / image repository</h4><p>A centralized code repository is an essential element of a secure and collaborative GenAI sandbox. It provides a single, controlled environment for developers to store, manage, and version their code, preventing the proliferation of sensitive information across disparate, unsecured locations. By establishing a centralized repository, organizations can enforce security policies, monitor for vulnerabilities, and maintain a clear audit trail of all code changes, which is critical for maintaining data privacy and integrity within the sandbox environment.</p><p>For instance, a service like GitHub, when integrated with your organization's identity and access management (IAM) and single sign-on (SSO) solutions (see ingredient #4 below), becomes a powerful tool for enforcing the principle of least privilege. This integration ensures that only authenticated and authorized developers can access specific code repositories. You can create teams and apply granular permissions, restricting access to sensitive projects and preventing unauthorized code modifications. This is especially important in a GenAI context where code might contain proprietary algorithms, sensitive data connectors, or even, in some cases, organization or user-level credentials or other confidential information.</p><p>Furthermore, modern repository platforms offer automated security scanning features. These tools continuously scan code for known vulnerabilities, insecure coding practices, and exposed secrets. If a developer accidentally commits a password or an API key, the system can automatically flag it and notify the security team. This proactive approach to security is essential for preventing data breaches, enforcing legal requirements and contractual commitments of confidentiality, and ensuring the overall integrity of the GenAI applications being developed to deploy in the sandbox. By mandating that all development occurs in a centralized and secured repository, you create a transparent, auditable, and secure foundation for innovation, allowing your developers the freedom to experiment without compromising organizational security.</p><h4>4. Identity and access management</h4><p>IAM is a core component of a secure, privacy-first grounded AI environment. It provides the foundation for ensuring that only authorized individuals and services can access sensitive data and powerful AI models. A robust IAM framework enforces the principle of least privilege, granting the minimum level of access necessary for a user or service to perform its function.</p><p><strong>4.1 Single sign-on:</strong></p><p>SSO streamlines user access by allowing users to authenticate once and gain access to multiple applications and services without re-entering their credentials. In a sandbox environment, SSO simplifies the user experience for developers, data scientists, and business users who need to interact with various components of the AI ecosystem, such as data repositories, modeling workbenches, and deployment pipelines. By centralizing authentication, SSO also enhances security by reducing the number of passwords that can be compromised and providing a single point for enforcing authentication policies. Importantly, it also lowers the barrier to entry for less-experienced developers to properly protect the data they are using in the sandbox, in turn preventing the inadvertent disclosure of sensitive information to insiders and outsiders alike.</p><p><strong>4.2 Role-based access control: </strong></p><p>Role-based access control (RBAC) is a method of restricting network access based on the roles of individual users within an organization. In the context of a GenAI sandbox, RBAC is used to define and enforce permissions for different user personas. For example, a data scientist role might have read/write access to specific datasets and the ability to apply machine learning models, while a business analyst role may only have read-only access to the outputs of those models. This ensures a clear separation of duties and prevents unauthorized access to or modification of sensitive data and AI assets.</p><p><strong>4.3 Attribute-based access control:</strong></p><p>Attribute-based access control (ABAC) provides a more granular and dynamic approach to access control than traditional RBAC. ABAC makes access decisions based on a combination of attributes of the user, the resource being accessed, and the environment. For instance, access to a particularly sensitive dataset could be restricted to users who are on the data scientist team (user attribute), accessing a resource tagged as PII (resource attribute), and are doing so from a corporate network during business hours (environment attributes). This level of granular control is critical in a GenAI sandbox for enforcing complex data governance and privacy requirements. We’ll come back to this later, when discussing the search AI datastore.</p><p><strong>4.4 Access auditability:</strong></p><p>A robust IAM framework also ensures that the granting, use, review and revocation of all access permissions is granularly logged, discoverable and auditable, so that in case of any suspected or confirmed incident, responders can quickly understand what happened, contain the incident, assess its extent, and comprehensively remedy its consequences. This is not only important for the organization’s own security, but also necessary to comply with any incident reporting and breach notice requirements that could be triggered.</p><h4>5. Secrets management</h4><p>Of all the ingredients in our recipe, secrets management is perhaps the most potent, yet most frequently overlooked. Much like a tiny pinch of saffron can dramatically alter a culinary dish, a single mishandled secret can have an outsized and devastating impact on your organization's security and reputation. In our context, a <em>secret</em> is any piece of sensitive information needed for our applications to function: API keys for first- or third-party services, database passwords, trust certificates, or tokens for authenticating to LLMs.</p><p>When these secrets are hard-coded into source code or left in plain-text configuration files, they create a massive vulnerability. A leaked API key or an exposed database credential can bypass all other security measures, providing a direct path for attackers to access sensitive data and systems. This is especially critical in a GenAI sandbox, where developers are frequently connecting to various data sources and external model providers. Without a robust secrets management strategy, you’re leaving the keys to your kingdom scattered across your digital landscape, turning your innovative sandbox into a potential source for a major data breach.</p><p>To properly secure these secrets, a dedicated secrets management platform is an essential ingredient. These tools provide a centralized, encrypted vault for storing secrets, with robust access control, auditing, and dynamic rotation capabilities. Whether you choose a self-hosted solution, like HashiCorp Vault, or a managed cloud service, such as Google Cloud's Secret Manager, or AWS Key Management Service (KMS), the principle is the same: Programmatically inject secrets into your applications at runtime. This practice ensures that secrets are never exposed in your code, keeping your most valuable credentials secure and your sandbox environment protected.</p><p>And this is more than just a best practice: since secret management technology is readily available and widely used, it forms part of the “state-of-the-art” which certain privacy laws and regulators reference as the benchmark against which an organization’s information security posture must be assessed. Failing to protect an organization’s most valuable secrets with the latest and greatest techniques available is not only a missed opportunity, it is also a potential case of regulatory non-compliance, as enforcement agencies and courts of law often recall.</p><h4>6. Private LLM deployment(s)</h4><p>Early in the advent of modern GenAI, the primary driver for using managed services, like Azure OpenAI, was the assurance that customer prompts and data would not be used to retrain public models. This was a crucial first step in enterprise adoption. However, as the field has matured, the conversation has shifted. While data privacy remains paramount, the decision to use private LLM instances, whether from major cloud providers or self-hosted, is now equally driven by the need for guaranteed throughput, predictable latency, and fine-grained control over the model's operational environment to support production-grade applications.</p><p>This critical ingredient comes in three distinct flavors, each with valid use cases and its own set of trade-offs:</p><p><strong>A. Cloud-hosted SaaS</strong></p><p>This is the most common and accessible approach. Services like OpenAI Enterprise, Azure OpenAI, Google Cloud's Vertex AI, and AWS Bedrock provide access to powerful, state-of-the-art models through a managed API.</p><ul><li><p><strong>Pros:</strong> This flavor offers the fastest time-to-market. The cloud provider handles all the underlying infrastructure, scaling, and maintenance, allowing teams to focus purely on application development. It provides a simple, pay-as-you-go model and access to a diverse model library of proprietary and open-source options.</p></li><li><p><strong>Cons:</strong> This approach offers the least control over the underlying infrastructure, which can lead to variability in performance during peak demand. It can also be more expensive at very high volumes, and it creates a dependency on the provider's roadmap and model availability. It also increases the potential vulnerability surface of the application, with data leaving the customer premises: a challenge for highly regulated and/or sovereignty-minded customers.</p></li></ul><p><strong>B. Cloud-hosted GPU + containerized LLMs</strong></p><p>This flavor involves running open-source LLMs (like models from Mistral or Meta's Llama series) on your own virtualized GPU infrastructure within a cloud provider. This is typically managed using the containerization and Kubernetes orchestration we've already discussed, often with high-performance inference servers like vLLM.</p><ul><li><p><strong>Pros:</strong> This approach provides a powerful balance of control and flexibility. You gain direct control over resource allocation, model versioning, and the serving configuration, allowing for significant performance tuning. In high-concurrency scenarios, a well-tuned inference server can dramatically increase throughput. For example, benchmarks have shown inference engines like vLLM delivering significantly higher tokens-per-second and lower latency compared to less production-oriented servers under heavy load [<a href="https://developers.redhat.com/articles/2025/09/30/vllm-or-llamacpp-choosing-right-llm-inference-engine-your-use-case">Red Hat, 2025</a>].</p></li><li><p><strong>Cons:</strong> This option carries a higher operational burden. Your team is now responsible for managing the GPU instances, container images, and the inference server configuration. It requires a deeper technical expertise in machine learning operations (MLOps) and infrastructure management to implement and maintain effectively.</p></li></ul><p><strong>C. On-premises GPUs + containerized LLMs</strong></p><p>The most controlled, and often most complex, approach involves deploying containerized LLMs on your own dedicated hardware within your own data centers. This setup is functionally similar to the second flavor but removes the reliance on a public cloud provider for the hardware layer.</p><ul><li><p><strong>Pros:</strong> This flavor offers maximum security, control, and data sovereignty. It’s the only option for organizations that require a completely <em>air-gapped environment</em>, where no data leaves the physical premises. For massive, predictable workloads, it can become more cost-effective in the long run by avoiding cloud data egress fees and per-transaction costs.</p></li><li><p><strong>Cons:</strong> The initial capital expenditure for purchasing and maintaining high-end GPU hardware is substantial. It requires a highly specialized team to manage the physical infrastructure, networking, and the entire software stack. This approach is more difficult to scale, as it requires the physical procurement and installation of new hardware.</p></li></ul><h4>7. Search AI data store</h4><p>If the LLM is the brain of our GenAI application, then the datastore is its heart, pumping relevant, context-rich information to be reasoned upon. For a RAG application to be truly effective, it cannot rely on a simple vector database alone. The grounding data is often complex, containing a mix of unstructured text, structured metadata, and a variety of data types. Therefore, the datastore you select must possess a unique set of characteristics to handle this complexity at scale.</p><p>Underpinning this entire process is the creation of <em>vector embeddings</em>, numerical representations of your data relative to the knowledge set of that embedding space. To enable semantic search, your data must first be converted into these numerical representations by an inference model. A flexible datastore should not only store these vectors but also be capable of hosting the inference process itself. Crucially, it should allow you to use your model of choice, whether it's a state-of-the-art multilingual model, a fine-tuned model for a specific domain like finance or law, a compact model built for very high-speed results, or even a model that can process images. By managing inference, the platform ensures that your data is consistently and efficiently vectorized, paving the way for the powerful search capabilities that follow.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfeba6cde3b0cc176/6a170c64e8fbce256f39fc97/dc05dfaf1bde3c7a74cf559b8c790a6e8e36be45-1600x900.png" alt="Search AI data store  for GenAI Sandbox" /><p>First, it must master <strong>hybrid search</strong>. The best retrieval systems don't force a choice between traditional keyword search, like BM25, which excels at finding specific keywords, and modern vector search, which excels at finding results using semantic meaning (that is, natural language). A truly capable datastore allows you to use both simultaneously in a single query. This ensures you can find documents that match exact product codes or acronyms while also finding documents that are conceptually related, providing the LLM with the most relevant possible context.</p><p></p><p>Second, it needs a sophisticated method for <strong>intelligent result reranking</strong>. When you run a hybrid search that combines multiple approaches, you need a way to merge the different result sets into a single, coherent ranking. Techniques like reciprocal rank fusion (RRF) are crucial here, as they intelligently combine the relevance scores from different queries to produce a final list that is more accurate and relevant than any single approach could deliver on its own.</p><p>Finally, a search AI-oriented datastore must be a <strong>unified engine with security built in</strong>. For enterprise RAG, it's not enough to just find similar vectors. You must be able to apply security and access controls to data <em>before</em> the search even happens. The aforementioned RBAC and ABAC capabilities allow prefiltering of content at search time, ensuring that the vector search is only performed on data a user is authorized to see. This mitigates risks of accidental or malicious circumvention of your access controls through the sandbox preserving demonstrable compliance with privacy and confidentiality requirements. This capability, which combines filtering, full-text search, and vector search in a single, scalable platform, is the defining characteristic of a datastore truly ready to power a secure, privacy-first GenAI sandbox.</p><h4>8. APM and security</h4><p>The final ingredient in our recipe ensures the health, security, and performance of the entire sandbox: a unified platform for APM and security information and event monitoring (SIEM). A key characteristic of a truly versatile search AI datastore is its ability to power the <em>R</em> in your RAG applications, while also acting as the standards-based repository for all logs, metrics, and traces generated by your infrastructure and applications. By consolidating this operational data into the same powerful datastore, you create a single pane of glass for observability and security.</p><p>This approach provides several critical capabilities. At the infrastructure level, you can monitor the performance and resource utilization of both the k8s clusters hosting your sandbox and the underlying GPUs that power your LLMs, allowing you to proactively identify bottlenecks or failures. At the application layer, APM provides detailed traces to diagnose latency issues or errors within your GenAI prototypes. For security, this centralized datastore becomes your SIEM, correlating login events, application logs, and network traffic to detect anomalous behavior or potential threats within the sandbox.</p><p>Most importantly, this unified platform allows you to gain deep insights into the usage of the GenAI applications themselves. By ingesting and analyzing the application telemetry, which should include the prompts being submitted by users wherever permissible, potentially with PII redacted, you can identify trends, understand what types of questions are being asked, and discover popular use cases. This provides an invaluable feedback loop for improving your RAG applications and demonstrates the power of using a single, scalable datastore to secure, monitor, and optimize your entire GenAI ecosystem.</p><h2>Cooking the recipe</h2><p>With all of the ingredients in place, let’s talk about the steps for assembling them into a production-grade sandbox.</p><p>As with any recipe book, let’s start with a photo of the cooked dish. Here’s a view of what a final architecture might look like:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte868ddaf52a782a4/6a170c65b339d59f9876a00b/de028ce2f5acd6f84e59ea67091472128a8a0143-1430x990.png" alt="GenAI sandbox recipe" /><p>The holistic environment depicted here consists of a Kubernetes cluster to host your sandboxed AI applications (with dev/preprod/prod namespaces for a continuous integration and continuous deployment [CI/CD] pipeline), an IAM infrastructure for authentication, a few GenAI applications, a repository for code and container images, and a wrapper of APM and cyber monitoring around the entire sandbox.</p><h3>Recipe step 0: Policy baseline</h3><p>Before you begin mixing any ingredients, every good chef performs their <em>mise en place</em>, that is, setting up their station for success. In our recipe, this means establishing clear policies for how the sandbox will be used. This is the foundational step, where you decide the rules of your kitchen. Will developers be allowed to use internal production data, or production data sanitized with techniques like pseudonymization and differential privacy, or life-like synthetic data, or only public data? Will the sandbox be a completely self-service platform, or a managed service with guardrails? Will application updates require a formal Change Review Board, or is a peer-review process sufficient? These questions are highly specific to each organization’s context and purposes. Answering them up front is critical, as these policy decisions will directly influence how you configure every other ingredient in the recipe.</p><h3>Recipe step 1: InfoSec baseline</h3><p>As stated in the “Ingredients” section, IAM is a nonnegotiable part of our recipe. Before letting anyone into the kitchen, you must secure the perimeter and ensure only authorized chefs wearing your approved uniform and compliant protective gear can access the tools and ingredients. This means working directly with your information security organization from day one to build the sandbox on a foundation of strong security principles. Access to your datastore, your code repository, your Kubernetes hosting environment, and the applications themselves must be restricted based on established best practices.</p><p>With your organization’s IAM policies enforced in the environment, a practical authentication flow might look like the one depicted in figure 3.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf78695fa1b2a2775/6a170c67b339d552b076a00f/7b1062f775ed2cf21e9386d1999933ba3f73efc4-1432x1488.png" alt="Authentication flow for GenAI sandbox" /><p>As you can see in the figure, no communication can occur between applications in the Kubernetes production namespace without first passing through an OAuth proxy, such as Vouch. This ensures every user is authenticated against a central provider, like Okta, which enforces policies such as two-factor authentication. In this model, critical user context, such as username and IP address, can be passed along with every request, enabling robust auditing and nonrepudiation at the application layer.</p><h3>Recipe step 2: Container configuration baseline</h3><p>Assuming that many of your rapid prototypers are passionate innovators but not necessarily seasoned software engineers or legally trained data compliance experts, it’s critical to provide a baseline configuration to ensure their success and security, without putting them at risk of breaching any rules or policies inadvertently. Think of this step as providing a master recipe card that guarantees consistency. At a minimum, you should provide clear documentation on how to build a container image, deploy it into the Kubernetes cluster, and test that all connectivity is secure.</p><p>Even better, you can create a “Clone This Starter App” template in your code repository. This gives developers a preconfigured, security-blessed starting point, complete with Dockerfiles and pipeline scripts, that they can immediately fork to begin tinkering, dramatically lowering the barrier to entry while enforcing best practices from the outset.</p><p>Additionally, many real life GenAI use cases will inevitably involve some form of PII processing, or can produce outputs that will materially impact individuals such as your employees, your consumers, or your customers’ staff. In such cases, more and more state, federal and international laws require completing various risk assessments before actual work can begin. These assessments can be cumbersome to conduct and are difficult to scale if they are carried out case by case. The “Clone This Starter App” approach also helps to prevent such compliance mandates from becoming bottlenecks to innovation, since under most legal mandates, the required assessments can be completed once for your template, and they need not be repeated for any clone that doesn’t exceed your initially defined parameters.</p><h3>Recipe step 3: Deploy user applications</h3><p>With your policies defined, your security baseline established, and your developer templates in place, it’s finally time to serve the dish. Whether you've chosen a self-service or managed deployment model, you can now confidently invite the rapid prototypers in your organization to start creating in the sandbox.</p><p>Because you’ve included APM and security logging (ingredient #8) from the beginning, you have the necessary observability to monitor application performance and user activity. This is where the magic happens: You can now learn from the applications people build, identify powerful new use cases, and gather real-world data to improve the platform, all while safeguarding organizational data. Coincidentally, this approach will also allow you to organically collect the information you might need to put on record, disclose to users, or share with auditors and regulators to demonstrate the transparency, accountability and explainability of your GenAI application, ticking many compliance boxes as you build (and not after the fact) – a textbook best practice of <em>Privacy by Design</em>.</p><h2>Where do you go from here?</h2><p>We've now walked through the entire cookbook, from selecting your fresh ingredients to following the recipe step by step. Most of the domains we've discussed (containerization, APM, IAM, and more) are culinary specialties in and of themselves.</p><h2>Conclusion</h2><p>This cookbook was designed to provide a clear recipe for building a production-grade GenAI sandbox. By carefully selecting each foundational ingredient, from your containerization platform and Kubernetes orchestration to your search AI datastore and unified APM, you ensure your final dish will be both successful and secure. Following the recipe ensures that this powerful environment is built on a foundation of security and thoughtful policy from day one.</p><p>The goal is to empower your rapid prototypers, not restrict them, and to foster a culture of responsible innovation. By providing a secure, observable, and well-equipped kitchen for experimentation, you get ahead of the curve, fostering a culture of responsible innovation. This proactive approach enables you to harness the creativity of your entire organization, transforming brilliant ideas into tangible prototypes while preventing the rise of shadow AI. You've cooked the meal; now you can enjoy the innovation it serves.</p><p>If you want to chat about this or anything else related to Elasticsearch, come join us in our <a href="https://discuss.elastic.co/">Discuss forum</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/generative-ai-sandbox-data-privacy</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/generative-ai-sandbox-data-privacy</guid>
    <category><![CDATA[Developer Experience]]></category>
    <dc:creator><![CDATA[Sean MacKirdy]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb517ed34cca3d88a/6a170c6947d49ce2322d8a50/e7ed91ecf91e7a1de7d9ff514a2c285f2cb3f65c-1000x628.png" length="0" type="image/png"/>
    <pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>